diff --git a/.env.example b/.env.example
index e746737ea4..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
@@ -540,6 +542,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 #
@@ -623,6 +627,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/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000..725ac8b6bd
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,3 @@
+# Force LF line endings for shell scripts and git hooks (required for cross-platform compatibility)
+.husky/* text eol=lf
+*.sh text eol=lf
diff --git a/.github/workflows/backend-review.yml b/.github/workflows/backend-review.yml
index 038c90627e..9dd3905c0e 100644
--- a/.github/workflows/backend-review.yml
+++ b/.github/workflows/backend-review.yml
@@ -97,6 +97,65 @@ jobs:
path: packages/api/dist
retention-days: 2
+ typecheck:
+ name: TypeScript type checks
+ needs: build
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js 20.19
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20.19'
+
+ - name: Restore node_modules cache
+ id: cache-node-modules
+ uses: actions/cache@v4
+ with:
+ path: |
+ node_modules
+ api/node_modules
+ packages/api/node_modules
+ packages/data-provider/node_modules
+ packages/data-schemas/node_modules
+ key: node-modules-backend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }}
+
+ - name: Install dependencies
+ if: steps.cache-node-modules.outputs.cache-hit != 'true'
+ run: npm ci
+
+ - name: Download data-provider build
+ uses: actions/download-artifact@v4
+ with:
+ name: build-data-provider
+ path: packages/data-provider/dist
+
+ - name: Download data-schemas build
+ uses: actions/download-artifact@v4
+ with:
+ name: build-data-schemas
+ path: packages/data-schemas/dist
+
+ - name: Download api build
+ uses: actions/download-artifact@v4
+ with:
+ name: build-api
+ path: packages/api/dist
+
+ - name: Type check data-provider
+ run: npx tsc --noEmit -p packages/data-provider/tsconfig.json
+
+ - name: Type check data-schemas
+ run: npx tsc --noEmit -p packages/data-schemas/tsconfig.json
+
+ - name: Type check @librechat/api
+ run: npx tsc --noEmit -p packages/api/tsconfig.json
+
+ - name: Type check @librechat/client
+ run: npx tsc --noEmit -p packages/client/tsconfig.json
+
circular-deps:
name: Circular dependency checks
needs: build
diff --git a/.gitignore b/.gitignore
index 86d4a3ddae..e302d15a46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,6 +63,7 @@ bower_components/
.clineignore
.cursor
.aider*
+.bg-shell/
# Floobits
.floo
@@ -129,6 +130,7 @@ helm/**/charts/
helm/**/.values.yaml
!/client/src/@types/i18next.d.ts
+!/client/src/@types/react.d.ts
# SAML Idp cert
*.cert
@@ -143,7 +145,6 @@ helm/**/.values.yaml
/.codeium
*.local.md
-
# Removed Windows wrapper files per user request
hive-mind-prompt-*.txt
@@ -154,16 +155,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
@@ -171,5 +172,8 @@ coordination/orchestration/*
*.sqlite-journal
*.sqlite-wal
claude-flow
+.playwright-mcp/*
# Removed Windows wrapper files per user request
hive-mind-prompt-*.txt
+CLAUDE.md
+.gsd
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/AGENTS.md b/AGENTS.md
index ec44607aa7..ceb2b988dc 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,166 +1 @@
-# LibreChat
-
-## Project Overview
-
-LibreChat is a monorepo with the following key workspaces:
-
-| Workspace | Language | Side | Dependency | Purpose |
-|---|---|---|---|---|
-| `/api` | JS (legacy) | Backend | `packages/api`, `packages/data-schemas`, `packages/data-provider`, `@librechat/agents` | Express server — minimize changes here |
-| `/packages/api` | **TypeScript** | Backend | `packages/data-schemas`, `packages/data-provider` | New backend code lives here (TS only, consumed by `/api`) |
-| `/packages/data-schemas` | TypeScript | Backend | `packages/data-provider` | Database models/schemas, shareable across backend projects |
-| `/packages/data-provider` | TypeScript | Shared | — | Shared API types, endpoints, data-service — used by both frontend and backend |
-| `/client` | TypeScript/React | Frontend | `packages/data-provider`, `packages/client` | Frontend SPA |
-| `/packages/client` | TypeScript | Frontend | `packages/data-provider` | Shared frontend utilities |
-
-The source code for `@librechat/agents` (major backend dependency, same team) is at `/home/danny/agentus`.
-
----
-
-## Workspace Boundaries
-
-- **All new backend code must be TypeScript** in `/packages/api`.
-- Keep `/api` changes to the absolute minimum (thin JS wrappers calling into `/packages/api`).
-- Database-specific shared logic goes in `/packages/data-schemas`.
-- Frontend/backend shared API logic (endpoints, types, data-service) goes in `/packages/data-provider`.
-- Build data-provider from project root: `npm run build:data-provider`.
-
----
-
-## Code Style
-
-### Structure and Clarity
-
-- **Never-nesting**: early returns, flat code, minimal indentation. Break complex operations into well-named helpers.
-- **Functional first**: pure functions, immutable data, `map`/`filter`/`reduce` over imperative loops. Only reach for OOP when it clearly improves domain modeling or state encapsulation.
-- **No dynamic imports** unless absolutely necessary.
-
-### DRY
-
-- Extract repeated logic into utility functions.
-- Reusable hooks / higher-order components for UI patterns.
-- Parameterized helpers instead of near-duplicate functions.
-- Constants for repeated values; configuration objects over duplicated init code.
-- Shared validators, centralized error handling, single source of truth for business rules.
-- Shared typing system with interfaces/types extending common base definitions.
-- Abstraction layers for external API interactions.
-
-### Iteration and Performance
-
-- **Minimize looping** — especially over shared data structures like message arrays, which are iterated frequently throughout the codebase. Every additional pass adds up at scale.
-- Consolidate sequential O(n) operations into a single pass whenever possible; never loop over the same collection twice if the work can be combined.
-- Choose data structures that reduce the need to iterate (e.g., `Map`/`Set` for lookups instead of `Array.find`/`Array.includes`).
-- Avoid unnecessary object creation; consider space-time tradeoffs.
-- Prevent memory leaks: careful with closures, dispose resources/event listeners, no circular references.
-
-### Type Safety
-
-- **Never use `any`**. Explicit types for all parameters, return values, and variables.
-- **Limit `unknown`** — avoid `unknown`, `Record`, and `as unknown as T` assertions. A `Record` almost always signals a missing explicit type definition.
-- **Don't duplicate types** — before defining a new type, check whether it already exists in the project (especially `packages/data-provider`). Reuse and extend existing types rather than creating redundant definitions.
-- Use union types, generics, and interfaces appropriately.
-- All TypeScript and ESLint warnings/errors must be addressed — do not leave unresolved diagnostics.
-
-### Comments and Documentation
-
-- Write self-documenting code; no inline comments narrating what code does.
-- JSDoc only for complex/non-obvious logic or intellisense on public APIs.
-- Single-line JSDoc for brief docs, multi-line for complex cases.
-- Avoid standalone `//` comments unless absolutely necessary.
-
-### Import Order
-
-Imports are organized into three sections:
-
-1. **Package imports** — sorted shortest to longest line length (`react` always first).
-2. **`import type` imports** — sorted longest to shortest (package types first, then local types; length resets between sub-groups).
-3. **Local/project imports** — sorted longest to shortest.
-
-Multi-line imports count total character length across all lines. Consolidate value imports from the same module. Always use standalone `import type { ... }` — never inline `type` inside value imports.
-
-### JS/TS Loop Preferences
-
-- **Limit looping as much as possible.** Prefer single-pass transformations and avoid re-iterating the same data.
-- `for (let i = 0; ...)` for performance-critical or index-dependent operations.
-- `for...of` for simple array iteration.
-- `for...in` only for object property enumeration.
-
----
-
-## Frontend Rules (`client/src/**/*`)
-
-### Localization
-
-- All user-facing text must use `useLocalize()`.
-- Only update English keys in `client/src/locales/en/translation.json` (other languages are automated externally).
-- Semantic key prefixes: `com_ui_`, `com_assistants_`, etc.
-
-### Components
-
-- TypeScript for all React components with proper type imports.
-- Semantic HTML with ARIA labels (`role`, `aria-label`) for accessibility.
-- Group related components in feature directories (e.g., `SidePanel/Memories/`).
-- Use index files for clean exports.
-
-### Data Management
-
-- Feature hooks: `client/src/data-provider/[Feature]/queries.ts` → `[Feature]/index.ts` → `client/src/data-provider/index.ts`.
-- React Query (`@tanstack/react-query`) for all API interactions; proper query invalidation on mutations.
-- QueryKeys and MutationKeys in `packages/data-provider/src/keys.ts`.
-
-### Data-Provider Integration
-
-- Endpoints: `packages/data-provider/src/api-endpoints.ts`
-- Data service: `packages/data-provider/src/data-service.ts`
-- Types: `packages/data-provider/src/types/queries.ts`
-- Use `encodeURIComponent` for dynamic URL parameters.
-
-### Performance
-
-- Prioritize memory and speed efficiency at scale.
-- Cursor pagination for large datasets.
-- Proper dependency arrays to avoid unnecessary re-renders.
-- Leverage React Query caching and background refetching.
-
----
-
-## Development Commands
-
-| Command | Purpose |
-|---|---|
-| `npm run smart-reinstall` | Install deps (if lockfile changed) + build via Turborepo |
-| `npm run reinstall` | Clean install — wipe `node_modules` and reinstall from scratch |
-| `npm run backend` | Start the backend server |
-| `npm run backend:dev` | Start backend with file watching (development) |
-| `npm run build` | Build all compiled code via Turborepo (parallel, cached) |
-| `npm run frontend` | Build all compiled code sequentially (legacy fallback) |
-| `npm run frontend:dev` | Start frontend dev server with HMR (port 3090, requires backend running) |
-| `npm run build:data-provider` | Rebuild `packages/data-provider` after changes |
-
-- Node.js: v20.19.0+ or ^22.12.0 or >= 23.0.0
-- Database: MongoDB
-- Backend runs on `http://localhost:3080/`; frontend dev server on `http://localhost:3090/`
-
----
-
-## Testing
-
-- Framework: **Jest**, run per-workspace.
-- 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.
-
-### 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.
-
----
-
-## Formatting
-
-Fix all formatting lint errors (trailing spaces, tabs, newlines, indentation) using auto-fix when available. All TypeScript/ESLint warnings and errors **must** be resolved.
+CLAUDE.md
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 120000
index 47dc3e3d86..0000000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1 +0,0 @@
-AGENTS.md
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000000..81362cfc57
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,172 @@
+# LibreChat
+
+## Project Overview
+
+LibreChat is a monorepo with the following key workspaces:
+
+| Workspace | Language | Side | Dependency | Purpose |
+|---|---|---|---|---|
+| `/api` | JS (legacy) | Backend | `packages/api`, `packages/data-schemas`, `packages/data-provider`, `@librechat/agents` | Express server — minimize changes here |
+| `/packages/api` | **TypeScript** | Backend | `packages/data-schemas`, `packages/data-provider` | New backend code lives here (TS only, consumed by `/api`) |
+| `/packages/data-schemas` | TypeScript | Backend | `packages/data-provider` | Database models/schemas, shareable across backend projects |
+| `/packages/data-provider` | TypeScript | Shared | — | Shared API types, endpoints, data-service — used by both frontend and backend |
+| `/client` | TypeScript/React | Frontend | `packages/data-provider`, `packages/client` | Frontend SPA |
+| `/packages/client` | TypeScript | Frontend | `packages/data-provider` | Shared frontend utilities |
+
+The source code for `@librechat/agents` (major backend dependency, same team) is at `/home/danny/agentus`.
+
+---
+
+## Workspace Boundaries
+
+- **All new backend code must be TypeScript** in `/packages/api`.
+- Keep `/api` changes to the absolute minimum (thin JS wrappers calling into `/packages/api`).
+- Database-specific shared logic goes in `/packages/data-schemas`.
+- Frontend/backend shared API logic (endpoints, types, data-service) goes in `/packages/data-provider`.
+- Build data-provider from project root: `npm run build:data-provider`.
+
+---
+
+## Code Style
+
+### Naming and File Organization
+
+- **Single-word file names** whenever possible (e.g., `permissions.ts`, `capabilities.ts`, `service.ts`).
+- When multiple words are needed, prefer grouping related modules under a **single-word directory** rather than using multi-word file names (e.g., `admin/capabilities.ts` not `adminCapabilities.ts`).
+- The directory already provides context — `app/service.ts` not `app/appConfigService.ts`.
+
+### Structure and Clarity
+
+- **Never-nesting**: early returns, flat code, minimal indentation. Break complex operations into well-named helpers.
+- **Functional first**: pure functions, immutable data, `map`/`filter`/`reduce` over imperative loops. Only reach for OOP when it clearly improves domain modeling or state encapsulation.
+- **No dynamic imports** unless absolutely necessary.
+
+### DRY
+
+- Extract repeated logic into utility functions.
+- Reusable hooks / higher-order components for UI patterns.
+- Parameterized helpers instead of near-duplicate functions.
+- Constants for repeated values; configuration objects over duplicated init code.
+- Shared validators, centralized error handling, single source of truth for business rules.
+- Shared typing system with interfaces/types extending common base definitions.
+- Abstraction layers for external API interactions.
+
+### Iteration and Performance
+
+- **Minimize looping** — especially over shared data structures like message arrays, which are iterated frequently throughout the codebase. Every additional pass adds up at scale.
+- Consolidate sequential O(n) operations into a single pass whenever possible; never loop over the same collection twice if the work can be combined.
+- Choose data structures that reduce the need to iterate (e.g., `Map`/`Set` for lookups instead of `Array.find`/`Array.includes`).
+- Avoid unnecessary object creation; consider space-time tradeoffs.
+- Prevent memory leaks: careful with closures, dispose resources/event listeners, no circular references.
+
+### Type Safety
+
+- **Never use `any`**. Explicit types for all parameters, return values, and variables.
+- **Limit `unknown`** — avoid `unknown`, `Record`, and `as unknown as T` assertions. A `Record` almost always signals a missing explicit type definition.
+- **Don't duplicate types** — before defining a new type, check whether it already exists in the project (especially `packages/data-provider`). Reuse and extend existing types rather than creating redundant definitions.
+- Use union types, generics, and interfaces appropriately.
+- All TypeScript and ESLint warnings/errors must be addressed — do not leave unresolved diagnostics.
+
+### Comments and Documentation
+
+- Write self-documenting code; no inline comments narrating what code does.
+- JSDoc only for complex/non-obvious logic or intellisense on public APIs.
+- Single-line JSDoc for brief docs, multi-line for complex cases.
+- Avoid standalone `//` comments unless absolutely necessary.
+
+### Import Order
+
+Imports are organized into three sections:
+
+1. **Package imports** — sorted shortest to longest line length (`react` always first).
+2. **`import type` imports** — sorted longest to shortest (package types first, then local types; length resets between sub-groups).
+3. **Local/project imports** — sorted longest to shortest.
+
+Multi-line imports count total character length across all lines. Consolidate value imports from the same module. Always use standalone `import type { ... }` — never inline `type` inside value imports.
+
+### JS/TS Loop Preferences
+
+- **Limit looping as much as possible.** Prefer single-pass transformations and avoid re-iterating the same data.
+- `for (let i = 0; ...)` for performance-critical or index-dependent operations.
+- `for...of` for simple array iteration.
+- `for...in` only for object property enumeration.
+
+---
+
+## Frontend Rules (`client/src/**/*`)
+
+### Localization
+
+- All user-facing text must use `useLocalize()`.
+- Only update English keys in `client/src/locales/en/translation.json` (other languages are automated externally).
+- Semantic key prefixes: `com_ui_`, `com_assistants_`, etc.
+
+### Components
+
+- TypeScript for all React components with proper type imports.
+- Semantic HTML with ARIA labels (`role`, `aria-label`) for accessibility.
+- Group related components in feature directories (e.g., `SidePanel/Memories/`).
+- Use index files for clean exports.
+
+### Data Management
+
+- Feature hooks: `client/src/data-provider/[Feature]/queries.ts` → `[Feature]/index.ts` → `client/src/data-provider/index.ts`.
+- React Query (`@tanstack/react-query`) for all API interactions; proper query invalidation on mutations.
+- QueryKeys and MutationKeys in `packages/data-provider/src/keys.ts`.
+
+### Data-Provider Integration
+
+- Endpoints: `packages/data-provider/src/api-endpoints.ts`
+- Data service: `packages/data-provider/src/data-service.ts`
+- Types: `packages/data-provider/src/types/queries.ts`
+- Use `encodeURIComponent` for dynamic URL parameters.
+
+### Performance
+
+- Prioritize memory and speed efficiency at scale.
+- Cursor pagination for large datasets.
+- Proper dependency arrays to avoid unnecessary re-renders.
+- Leverage React Query caching and background refetching.
+
+---
+
+## Development Commands
+
+| Command | Purpose |
+|---|---|
+| `npm run smart-reinstall` | Install deps (if lockfile changed) + build via Turborepo |
+| `npm run reinstall` | Clean install — wipe `node_modules` and reinstall from scratch |
+| `npm run backend` | Start the backend server |
+| `npm run backend:dev` | Start backend with file watching (development) |
+| `npm run build` | Build all compiled code via Turborepo (parallel, cached) |
+| `npm run frontend` | Build all compiled code sequentially (legacy fallback) |
+| `npm run frontend:dev` | Start frontend dev server with HMR (port 3090, requires backend running) |
+| `npm run build:data-provider` | Rebuild `packages/data-provider` after changes |
+
+- Node.js: v20.19.0+ or ^22.12.0 or >= 23.0.0
+- Database: MongoDB
+- Backend runs on `http://localhost:3080/`; frontend dev server on `http://localhost:3090/`
+
+---
+
+## Testing
+
+- Framework: **Jest**, run per-workspace.
+- 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.
+
+### 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.
+
+---
+
+## Formatting
+
+Fix all formatting lint errors (trailing spaces, tabs, newlines, indentation) using auto-fix when available. All TypeScript/ESLint warnings and errors **must** be resolved.
diff --git a/Dockerfile b/Dockerfile
index bbff8133da..19d275eb31 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,9 @@
-# v0.8.3
+# v0.8.4
# 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 53810b5f0a..bf5570f386 100644
--- a/Dockerfile.multi
+++ b/Dockerfile.multi
@@ -1,12 +1,12 @@
# Dockerfile.multi
-# v0.8.3
+# v0.8.4
# Set configurable max-old-space-size with default
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
diff --git a/README.md b/README.md
index e82b3ebc2c..a7f68d9a92 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,11 @@
+
+ English ·
+ 中文
+
+
-
+
diff --git a/README.zh.md b/README.zh.md
new file mode 100644
index 0000000000..7f74057413
--- /dev/null
+++ b/README.zh.md
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+
+
+
+
+ English ·
+ 中文
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# ✨ 功能
+
+- 🖥️ **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 历史
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+## ✨ 贡献
+
+欢迎任何形式的贡献、建议、错误报告和修复!
+
+对于新功能、组件或扩展,请在发送 PR 前开启 issue 进行讨论。
+
+如果您想帮助我们将 LibreChat 翻译成您的母语,我们非常欢迎!改进翻译不仅能让全球用户更轻松地使用 LibreChat,还能提升整体用户体验。请查看我们的[翻译指南](https://www.librechat.ai/docs/translation)。
+
+---
+
+## 💖 感谢所有贡献者
+
+
+
+
+
+---
+
+## 🎉 特别鸣谢
+
+感谢 [Locize](https://locize.com) 提供的翻译管理工具,支持 LibreChat 的多语言功能。
+
+
+
+
+
+
diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js
index 8f931f8a5e..905cadfd23 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,
@@ -12,35 +13,27 @@ const {
} = require('@librechat/api');
const {
Constants,
- ErrorTypes,
FileSources,
ContentTypes,
excludedKeys,
EModelEndpoint,
+ mergeFileConfig,
isParamEndpoint,
isAgentsEndpoint,
isEphemeralAgentId,
supportsBalanceCheck,
isBedrockDocumentType,
+ getEndpointFileConfig,
} = 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 = {}) {
this.apiKey = apiKey;
this.sender = options.sender ?? 'AI';
- this.contextStrategy = null;
this.currentDateString = new Date().toLocaleDateString('en-us', {
year: 'numeric',
month: 'long',
@@ -80,6 +73,10 @@ class BaseClient {
this.currentMessages = [];
/** @type {import('librechat-data-provider').VisionModes | undefined} */
this.visionMode;
+ /** @type {import('librechat-data-provider').FileConfig | undefined} */
+ this._mergedFileConfig;
+ /** @type {import('librechat-data-provider').EndpointFileConfig | undefined} */
+ this._endpointFileConfig;
}
setOptions() {
@@ -339,45 +336,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;
@@ -448,154 +406,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} */
@@ -664,17 +474,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) {
@@ -686,7 +492,12 @@ class BaseClient {
}
delete userMessage.image_urls;
}
- userMessagePromise = this.saveMessageToDatabase(userMessage, saveOptions, user);
+ userMessagePromise = this.saveMessageToDatabase(userMessage, saveOptions, user).catch(
+ (err) => {
+ logger.error('[BaseClient] Failed to save user message:', err);
+ return {};
+ },
+ );
this.savedMessageIds.add(userMessage.messageId);
if (typeof opts?.getReqData === 'function') {
opts.getReqData({
@@ -700,18 +511,28 @@ 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,
+ balanceConfig,
+ upsertBalanceFields: db.upsertBalanceFields,
+ },
+ );
}
const { completion, metadata } = await this.sendCompletion(payload, opts);
@@ -764,12 +585,7 @@ class BaseClient {
responseMessage.text = completion.join('');
}
- if (
- tokenCountMap &&
- this.recordTokenUsage &&
- this.getTokenCountForResponse &&
- this.getTokenCount
- ) {
+ if (tokenCountMap && this.recordTokenUsage && this.getTokenCountForResponse) {
let completionTokens;
/**
@@ -782,13 +598,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;
@@ -815,6 +624,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);
}
@@ -827,6 +657,10 @@ class BaseClient {
}
}
+ if (this.contextMeta) {
+ responseMessage.contextMeta = this.contextMeta;
+ }
+
responseMessage.databasePromise = this.saveMessageToDatabase(
responseMessage,
saveOptions,
@@ -837,79 +671,10 @@ 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 });
- const messages = (await getMessages({ conversationId })) ?? [];
+ const messages = (await db.getMessages({ conversationId })) ?? [];
if (messages.length === 0) {
return [];
@@ -932,10 +697,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;
}
}
@@ -960,16 +739,30 @@ class BaseClient {
* @param {string | null} user
*/
async saveMessageToDatabase(message, endpointOptions, user = null) {
+ // Snapshot options before any await; disposeClient may set client.options = null
+ // while this method is suspended at an I/O boundary, but the local reference
+ // remains valid (disposeClient nulls the property, not the object itself).
+ const options = this.options;
+ if (!options) {
+ logger.error('[BaseClient] saveMessageToDatabase: client disposed before save, skipping');
+ return {};
+ }
+
if (this.user && user !== this.user) {
throw new Error('User mismatch.');
}
- const hasAddedConvo = this.options?.req?.body?.addedConvo != null;
- const savedMessage = await saveMessage(
- this.options?.req,
+ const hasAddedConvo = options?.req?.body?.addedConvo != null;
+ const reqCtx = {
+ userId: options?.req?.user?.id,
+ isTemporary: options?.req?.body?.isTemporary,
+ interfaceConfig: options?.req?.config?.interfaceConfig,
+ };
+ const savedMessage = await db.saveMessage(
+ reqCtx,
{
...message,
- endpoint: this.options.endpoint,
+ endpoint: options.endpoint,
unfinished: false,
user,
...(hasAddedConvo && { addedConvo: true }),
@@ -983,20 +776,20 @@ class BaseClient {
const fieldsToKeep = {
conversationId: message.conversationId,
- endpoint: this.options.endpoint,
- endpointType: this.options.endpointType,
+ endpoint: options.endpoint,
+ endpointType: options.endpointType,
...endpointOptions,
};
const existingConvo =
this.fetchedConvo === true
? null
- : await getConvo(this.options?.req?.user?.id, message.conversationId);
+ : await db.getConvo(options?.req?.user?.id, message.conversationId);
const unsetFields = {};
const exceptions = new Set(['spec', 'iconURL']);
const hasNonEphemeralAgent =
- isAgentsEndpoint(this.options.endpoint) &&
+ isAgentsEndpoint(options.endpoint) &&
endpointOptions?.agent_id &&
!isEphemeralAgentId(endpointOptions.agent_id);
if (hasNonEphemeralAgent) {
@@ -1018,7 +811,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 +824,35 @@ class BaseClient {
* @param {Partial} message
*/
async updateMessageInDatabase(message) {
- await updateMessage(this.options.req, message);
+ 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;
}
/**
@@ -1088,20 +909,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;
}
@@ -1257,6 +1093,7 @@ class BaseClient {
provider: this.options.agent?.provider ?? this.options.endpoint,
endpoint: this.options.agent?.endpoint ?? this.options.endpoint,
useResponsesApi: this.options.agent?.model_parameters?.useResponsesApi,
+ model: this.modelOptions?.model ?? this.model,
},
getStrategyFunctions,
);
@@ -1329,6 +1166,16 @@ class BaseClient {
const provider = this.options.agent?.provider ?? this.options.endpoint;
const isBedrock = provider === EModelEndpoint.bedrock;
+ if (!this._mergedFileConfig && this.options.req?.config?.fileConfig) {
+ this._mergedFileConfig = mergeFileConfig(this.options.req.config.fileConfig);
+ const endpoint = this.options.agent?.endpoint ?? this.options.endpoint;
+ this._endpointFileConfig = getEndpointFileConfig({
+ fileConfig: this._mergedFileConfig,
+ endpoint,
+ endpointType: this.options.endpointType,
+ });
+ }
+
for (const file of attachments) {
/** @type {FileSources} */
const source = file.source ?? FileSources.local;
@@ -1355,6 +1202,14 @@ class BaseClient {
} else if (file.type.startsWith('audio/')) {
categorizedAttachments.audios.push(file);
allFiles.push(file);
+ } else if (
+ file.type &&
+ this._mergedFileConfig &&
+ this._endpointFileConfig?.supportedMimeTypes &&
+ this._mergedFileConfig.checkType(file.type, this._endpointFileConfig.supportedMimeTypes)
+ ) {
+ categorizedAttachments.documents.push(file);
+ allFiles.push(file);
}
}
@@ -1431,7 +1286,7 @@ class BaseClient {
return message;
}
- const files = await getFiles(
+ const files = await db.getFiles(
{
file_id: { $in: fileIds },
},
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..3ce910948c 100644
--- a/api/app/clients/specs/BaseClient.test.js
+++ b/api/app/clients/specs/BaseClient.test.js
@@ -38,7 +38,7 @@ jest.mock('~/models', () => ({
updateFileUsage: jest.fn(),
}));
-const { getConvo, saveConvo } = require('~/models');
+const { getConvo, saveConvo, saveMessage } = require('~/models');
jest.mock('@librechat/agents', () => {
const actual = jest.requireActual('@librechat/agents');
@@ -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', () => {
@@ -793,6 +906,52 @@ describe('BaseClient', () => {
);
});
+ test('saveMessageToDatabase returns early when this.options is null (client disposed)', async () => {
+ const savedOptions = TestClient.options;
+ TestClient.options = null;
+ saveMessage.mockClear();
+
+ const result = await TestClient.saveMessageToDatabase(
+ { messageId: 'msg-1', conversationId: 'conv-1', isCreatedByUser: true, text: 'hi' },
+ {},
+ null,
+ );
+
+ expect(result).toEqual({});
+ expect(saveMessage).not.toHaveBeenCalled();
+
+ TestClient.options = savedOptions;
+ });
+
+ test('saveMessageToDatabase uses snapshot of options, immune to mid-await disposal', async () => {
+ const savedOptions = TestClient.options;
+ saveMessage.mockClear();
+ saveConvo.mockClear();
+
+ // Make db.saveMessage yield, simulating I/O suspension during which disposal occurs
+ saveMessage.mockImplementation(async (_reqCtx, msgData) => {
+ // Simulate disposeClient nullifying client.options while awaiting
+ TestClient.options = null;
+ return msgData;
+ });
+ saveConvo.mockResolvedValue({ conversationId: 'conv-1' });
+
+ const result = await TestClient.saveMessageToDatabase(
+ { messageId: 'msg-1', conversationId: 'conv-1', isCreatedByUser: true, text: 'hi' },
+ { endpoint: 'openAI' },
+ null,
+ );
+
+ // Should complete without TypeError, using the snapshotted options
+ expect(result).toHaveProperty('message');
+ expect(result).toHaveProperty('conversation');
+ expect(saveMessage).toHaveBeenCalled();
+
+ TestClient.options = savedOptions;
+ saveMessage.mockReset();
+ saveConvo.mockReset();
+ });
+
test('userMessagePromise is awaited before saving response message', async () => {
// Mock the saveMessageToDatabase method
TestClient.saveMessageToDatabase = jest.fn().mockImplementation(() => {
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/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/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');
+ });
+ });
+});
diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js
index d82a0d6930..8adb43f945 100644
--- a/api/app/clients/tools/util/handleTools.js
+++ b/api/app/clients/tools/util/handleTools.js
@@ -14,7 +14,6 @@ const {
buildImageToolContext,
buildWebSearchContext,
} = require('@librechat/api');
-const { getMCPServersRegistry } = require('~/config');
const {
Tools,
Constants,
@@ -39,13 +38,14 @@ const {
createGeminiImageTool,
createOpenAIImageTools,
} = require('../');
-const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process');
+const { createMCPTool, createMCPTools, resolveConfigServers } = require('~/server/services/MCP');
const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch');
+const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process');
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 { getMCPServersRegistry } = require('~/config');
+const { getRoleByName } = require('~/models');
/**
* Validates the availability and authentication of tools for a user based on environment variables or user-specific plugin authentication values.
@@ -256,6 +256,12 @@ const loadTools = async ({
const toolContextMap = {};
const requestedMCPTools = {};
+ /** Resolve config-source servers for the current user/tenant context */
+ let configServers;
+ if (tools.some((tool) => tool && mcpToolPattern.test(tool))) {
+ configServers = await resolveConfigServers(options.req);
+ }
+
for (const tool of tools) {
if (tool === Tools.execute_code) {
requestedTools[tool] = async () => {
@@ -341,7 +347,7 @@ const loadTools = async ({
continue;
}
const serverConfig = serverName
- ? await getMCPServersRegistry().getServerConfig(serverName, user)
+ ? await getMCPServersRegistry().getServerConfig(serverName, user, configServers)
: null;
if (!serverConfig) {
logger.warn(
@@ -419,6 +425,7 @@ const loadTools = async ({
let index = -1;
const failedMCPServers = new Set();
const safeUser = createSafeUser(options.req?.user);
+
for (const [serverName, toolConfigs] of Object.entries(requestedMCPTools)) {
index++;
/** @type {LCAvailableTools} */
@@ -433,6 +440,7 @@ const loadTools = async ({
signal,
user: safeUser,
userMCPAuthMap,
+ configServers,
res: options.res,
streamId: options.req?._resumableStreamId || null,
model: agent?.model ?? model,
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/db/index.js b/api/db/index.js
index 5c29902f69..f4359c8adf 100644
--- a/api/db/index.js
+++ b/api/db/index.js
@@ -1,8 +1,13 @@
const mongoose = require('mongoose');
const { createModels } = require('@librechat/data-schemas');
const { connectDb } = require('./connect');
-const indexSync = require('./indexSync');
+// createModels MUST run before requiring indexSync.
+// indexSync.js captures mongoose.models.Message and mongoose.models.Conversation
+// at module load time. If those models are not registered first, all MeiliSearch
+// sync operations will silently fail on every startup.
createModels(mongoose);
+const indexSync = require('./indexSync');
+
module.exports = { connectDb, indexSync };
diff --git a/api/db/index.spec.js b/api/db/index.spec.js
new file mode 100644
index 0000000000..e1ebe176dc
--- /dev/null
+++ b/api/db/index.spec.js
@@ -0,0 +1,26 @@
+describe('api/db/index.js', () => {
+ test('createModels is called before indexSync is loaded', () => {
+ jest.resetModules();
+
+ const callOrder = [];
+
+ jest.mock('@librechat/data-schemas', () => ({
+ createModels: jest.fn((m) => {
+ callOrder.push('createModels');
+ m.models.Message = { name: 'Message' };
+ m.models.Conversation = { name: 'Conversation' };
+ }),
+ }));
+
+ jest.mock('./indexSync', () => {
+ callOrder.push('indexSync');
+ return jest.fn();
+ });
+
+ jest.mock('./connect', () => ({ connectDb: jest.fn() }));
+
+ require('./index');
+
+ expect(callOrder).toEqual(['createModels', 'indexSync']);
+ });
+});
diff --git a/api/db/indexSync.js b/api/db/indexSync.js
index 130cde77b8..13059033fb 100644
--- a/api/db/indexSync.js
+++ b/api/db/indexSync.js
@@ -6,9 +6,6 @@ const { isEnabled, FlowStateManager } = require('@librechat/api');
const { getLogStores } = require('~/cache');
const { batchResetMeiliFlags } = require('./utils');
-const Conversation = mongoose.models.Conversation;
-const Message = mongoose.models.Message;
-
const searchEnabled = isEnabled(process.env.SEARCH);
const indexingDisabled = isEnabled(process.env.MEILI_NO_SYNC);
let currentTimeout = null;
@@ -200,6 +197,14 @@ async function performSync(flowManager, flowId, flowType) {
return { messagesSync: false, convosSync: false };
}
+ const Message = mongoose.models.Message;
+ const Conversation = mongoose.models.Conversation;
+ if (!Message || !Conversation) {
+ throw new Error(
+ '[indexSync] Models not registered. Ensure createModels() has been called before indexSync.',
+ );
+ }
+
const client = MeiliSearchClient.getInstance();
const { status } = await client.health();
@@ -349,6 +354,13 @@ async function indexSync() {
logger.debug('[indexSync] Creating indices...');
currentTimeout = setTimeout(async () => {
try {
+ const Message = mongoose.models.Message;
+ const Conversation = mongoose.models.Conversation;
+ if (!Message || !Conversation) {
+ throw new Error(
+ '[indexSync] Models not registered. Ensure createModels() has been called before indexSync.',
+ );
+ }
await Message.syncWithMeili();
await Conversation.syncWithMeili();
} catch (err) {
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/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/models/Agent.js b/api/models/Agent.js
deleted file mode 100644
index 663285183a..0000000000
--- a/api/models/Agent.js
+++ /dev/null
@@ -1,931 +0,0 @@
-const mongoose = require('mongoose');
-const crypto = require('node:crypto');
-const { logger } = require('@librechat/data-schemas');
-const { getCustomEndpointConfig } = require('@librechat/api');
-const {
- Tools,
- SystemRoles,
- ResourceType,
- actionDelimiter,
- isAgentsEndpoint,
- isEphemeralAgentId,
- encodeEphemeralAgentId,
-} = require('librechat-data-provider');
-const { mcp_all, mcp_delimiter } = require('librechat-data-provider').Constants;
-const {
- removeAgentFromAllProjects,
- removeAgentIdsFromProject,
- addAgentIdsToProject,
-} = require('./Project');
-const { 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;
- }
-
- // 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
- ) {
- 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;
- }, {});
-
- // Step 1: Atomically remove file IDs using $pull
- const pullOps = {};
- const resourcesToCheck = new Set();
- for (const [resource, fileIds] of Object.entries(filesByResource)) {
- const fileIdsPath = `tool_resources.${resource}.file_ids`;
- pullOps[fileIdsPath] = { $in: fileIds };
- resourcesToCheck.add(resource);
- }
-
- const updatePullData = { $pull: pullOps };
- 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 removeAgentFromAllProjects(agent.id);
- 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 all agents created by a specific user.
- * @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.
- */
-const deleteUserAgents = async (userId) => {
- try {
- const userAgents = await getAgents({ author: userId });
-
- if (userAgents.length === 0) {
- return;
- }
-
- const agentIds = userAgents.map((agent) => agent.id);
- const agentObjectIds = userAgents.map((agent) => agent._id);
-
- for (const agentId of agentIds) {
- await removeAgentFromAllProjects(agentId);
- }
-
- await AclEntry.deleteMany({
- resourceType: { $in: [ResourceType.AGENT, ResourceType.REMOTE_AGENT] },
- resourceId: { $in: agentObjectIds },
- });
-
- 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({ author: userId });
- } 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,
- projectIds: 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,
- };
-};
-
-/**
- * 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.$pull = { projectIds: { $in: 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.
- * @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,
- updateAgentProjects,
- 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 47a6c2bbf5..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 }, { $pull: { 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/Project.js b/api/models/Project.js
deleted file mode 100644
index 8fd1e556f9..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,
- { $pull: { promptGroupIds: { $in: 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({}, { $pull: { 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,
- { $pull: { agentIds: { $in: 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({}, { $pull: { 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
deleted file mode 100644
index bde911b23a..0000000000
--- a/api/models/Prompt.js
+++ /dev/null
@@ -1,708 +0,0 @@
-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 { removeAllPermissions } = require('~/server/services/PermissionService');
-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
- */
-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,
- },
- },
- ];
-};
-
-/**
- * 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,
- },
- ];
-};
-
-/**
- * Get all prompt groups with filters
- * @param {ServerRequest} req
- * @param {TPromptGroupsWithFilterRequest} filter
- * @returns {Promise}
- */
-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 promptGroupsPipeline = createAllGroupsPipeline(combinedQuery);
- return await PromptGroup.aggregate(promptGroupsPipeline).exec();
- } 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);
-
- 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 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 };
- delete projectQuery.author;
- combinedQuery = searchSharedOnly ? projectQuery : { $or: [projectQuery, query] };
- }
- }
-
- 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 promptGroups = promptGroupsResults;
- const totalPromptGroups =
- totalPromptGroupsResults.length > 0 ? totalPromptGroupsResults[0].total : 0;
-
- 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);
- await removeGroupFromAllProjects(_id);
-
- 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;
-
- // 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'));
- const { updatedAt, _id } = cursor;
-
- const cursorCondition = {
- $or: [
- { updatedAt: { $lt: new Date(updatedAt) } },
- { updatedAt: new Date(updatedAt), _id: { $gt: new 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);
- }
- }
-
- // Build aggregation pipeline
- const pipeline = [{ $match: baseQuery }, { $sort: { updatedAt: -1, _id: 1 } }];
-
- // Only apply limit if pagination is requested
- if (isPaginated) {
- pipeline.push({ $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 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;
- },
- );
-
- // Generate next cursor only if paginated
- 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 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 };
- } 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 });
- await removeGroupFromAllProjects(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 all prompts and prompt groups created by a specific user.
- * @param {ServerRequest} req - The server request object.
- * @param {string} userId - The ID of the user whose prompts and prompt groups are to be deleted.
- */
- deleteUserPrompts: async (req, userId) => {
- try {
- const promptGroups = await getAllPromptGroups(req, { author: new ObjectId(userId) });
-
- if (promptGroups.length === 0) {
- return;
- }
-
- const groupIds = promptGroups.map((group) => group._id);
-
- for (const groupId of groupIds) {
- await removeGroupFromAllProjects(groupId);
- }
-
- await AclEntry.deleteMany({
- resourceType: ResourceType.PROMPTGROUP,
- resourceId: { $in: groupIds },
- });
-
- await PromptGroup.deleteMany({ author: new ObjectId(userId) });
- await Prompt.deleteMany({ author: new ObjectId(userId) });
- } 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 = {};
- if (data.removeProjectIds) {
- for (const projectId of data.removeProjectIds) {
- await removeGroupIdsFromProject(projectId, [filter._id]);
- }
-
- updateOps.$pull = { projectIds: { $in: 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, {
- 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 e00a1a518c..0000000000
--- a/api/models/Prompt.spec.js
+++ /dev/null
@@ -1,564 +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, Project;
-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;
- Project = dbModels.Project;
-
- 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',
- }),
- };
-
- await Project.create({
- name: 'Global',
- description: 'Global project',
- promptGroupIds: [],
- });
-}
-
-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());
- });
- });
-});
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/Role.spec.js b/api/models/Role.spec.js
deleted file mode 100644
index 0ec2f831e2..0000000000
--- a/api/models/Role.spec.js
+++ /dev/null
@@ -1,511 +0,0 @@
-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');
-
-// Mock the cache
-jest.mock('~/cache/getLogStores', () =>
- jest.fn().mockReturnValue({
- get: jest.fn(),
- set: jest.fn(),
- del: jest.fn(),
- }),
-);
-
-let mongoServer;
-
-beforeAll(async () => {
- mongoServer = await MongoMemoryServer.create();
- const mongoUri = mongoServer.getUri();
- await mongoose.connect(mongoUri);
-});
-
-afterAll(async () => {
- await mongoose.disconnect();
- await mongoServer.stop();
-});
-
-beforeEach(async () => {
- await Role.deleteMany({});
- getLogStores.mockClear();
-});
-
-describe('updateAccessPermissions', () => {
- it('should update permissions when changes are needed', async () => {
- await new Role({
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.PROMPTS]: {
- CREATE: true,
- USE: true,
- SHARE: false,
- },
- },
- }).save();
-
- await updateAccessPermissions(SystemRoles.USER, {
- [PermissionTypes.PROMPTS]: {
- CREATE: true,
- USE: true,
- SHARE: true,
- },
- });
-
- const updatedRole = await getRoleByName(SystemRoles.USER);
- expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({
- CREATE: true,
- USE: true,
- SHARE: true,
- });
- });
-
- it('should not update permissions when no changes are needed', async () => {
- await new Role({
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.PROMPTS]: {
- CREATE: true,
- USE: true,
- SHARE: false,
- },
- },
- }).save();
-
- await updateAccessPermissions(SystemRoles.USER, {
- [PermissionTypes.PROMPTS]: {
- CREATE: true,
- USE: true,
- SHARE: false,
- },
- });
-
- const updatedRole = await getRoleByName(SystemRoles.USER);
- expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({
- CREATE: true,
- USE: true,
- SHARE: false,
- });
- });
-
- it('should handle non-existent roles', async () => {
- await updateAccessPermissions('NON_EXISTENT_ROLE', {
- [PermissionTypes.PROMPTS]: { CREATE: true },
- });
- const role = await Role.findOne({ name: 'NON_EXISTENT_ROLE' });
- expect(role).toBeNull();
- });
-
- it('should update only specified permissions', async () => {
- await new Role({
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.PROMPTS]: {
- CREATE: true,
- USE: true,
- SHARE: false,
- },
- },
- }).save();
-
- await updateAccessPermissions(SystemRoles.USER, {
- [PermissionTypes.PROMPTS]: { SHARE: true },
- });
-
- const updatedRole = await getRoleByName(SystemRoles.USER);
- expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({
- CREATE: true,
- USE: true,
- SHARE: true,
- });
- });
-
- it('should handle partial updates', async () => {
- await new Role({
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.PROMPTS]: {
- CREATE: true,
- USE: true,
- SHARE: false,
- },
- },
- }).save();
-
- await updateAccessPermissions(SystemRoles.USER, {
- [PermissionTypes.PROMPTS]: { USE: false },
- });
-
- const updatedRole = await getRoleByName(SystemRoles.USER);
- expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({
- CREATE: true,
- USE: false,
- SHARE: false,
- });
- });
-
- it('should update multiple permission types at once', async () => {
- await new Role({
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, SHARE: false },
- [PermissionTypes.BOOKMARKS]: { USE: true },
- },
- }).save();
-
- await updateAccessPermissions(SystemRoles.USER, {
- [PermissionTypes.PROMPTS]: { USE: false, SHARE: true },
- [PermissionTypes.BOOKMARKS]: { USE: false },
- });
-
- const updatedRole = await getRoleByName(SystemRoles.USER);
- expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({
- CREATE: true,
- USE: false,
- SHARE: true,
- });
- expect(updatedRole.permissions[PermissionTypes.BOOKMARKS]).toEqual({ USE: false });
- });
-
- it('should handle updates for a single permission type', async () => {
- await new Role({
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, SHARE: false },
- },
- }).save();
-
- await updateAccessPermissions(SystemRoles.USER, {
- [PermissionTypes.PROMPTS]: { USE: false, SHARE: true },
- });
-
- const updatedRole = await getRoleByName(SystemRoles.USER);
- expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({
- CREATE: true,
- USE: false,
- SHARE: true,
- });
- });
-
- it('should update MULTI_CONVO permissions', async () => {
- await new Role({
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.MULTI_CONVO]: { USE: false },
- },
- }).save();
-
- await updateAccessPermissions(SystemRoles.USER, {
- [PermissionTypes.MULTI_CONVO]: { USE: true },
- });
-
- const updatedRole = await getRoleByName(SystemRoles.USER);
- expect(updatedRole.permissions[PermissionTypes.MULTI_CONVO]).toEqual({ USE: true });
- });
-
- it('should update MULTI_CONVO permissions along with other permission types', async () => {
- await new Role({
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, SHARE: false },
- [PermissionTypes.MULTI_CONVO]: { USE: false },
- },
- }).save();
-
- await updateAccessPermissions(SystemRoles.USER, {
- [PermissionTypes.PROMPTS]: { SHARE: true },
- [PermissionTypes.MULTI_CONVO]: { USE: true },
- });
-
- const updatedRole = await getRoleByName(SystemRoles.USER);
- expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({
- CREATE: true,
- USE: true,
- SHARE: true,
- });
- expect(updatedRole.permissions[PermissionTypes.MULTI_CONVO]).toEqual({ USE: true });
- });
-
- it('should inherit SHARED_GLOBAL value into SHARE when SHARE is absent from both DB and update', async () => {
- // Simulates the startup backfill path: caller sends SHARE_PUBLIC but not SHARE;
- // migration should inherit SHARED_GLOBAL to preserve the deployment's sharing intent.
- await Role.collection.insertOne({
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.PROMPTS]: { USE: true, CREATE: true, SHARED_GLOBAL: true },
- [PermissionTypes.AGENTS]: { USE: true, CREATE: true, SHARED_GLOBAL: false },
- },
- });
-
- await updateAccessPermissions(SystemRoles.USER, {
- // No explicit SHARE — migration should inherit from SHARED_GLOBAL
- [PermissionTypes.PROMPTS]: { SHARE_PUBLIC: false },
- [PermissionTypes.AGENTS]: { SHARE_PUBLIC: false },
- });
-
- const updatedRole = await getRoleByName(SystemRoles.USER);
-
- // SHARED_GLOBAL=true → SHARE=true (inherited)
- expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARE).toBe(true);
- // SHARED_GLOBAL=false → SHARE=false (inherited)
- expect(updatedRole.permissions[PermissionTypes.AGENTS].SHARE).toBe(false);
- // SHARED_GLOBAL cleaned up
- expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARED_GLOBAL).toBeUndefined();
- expect(updatedRole.permissions[PermissionTypes.AGENTS].SHARED_GLOBAL).toBeUndefined();
- });
-
- it('should respect explicit SHARE in update payload and not override it with SHARED_GLOBAL', async () => {
- // Caller explicitly passes SHARE: false even though SHARED_GLOBAL=true in DB.
- // The explicit intent must win; migration must not silently overwrite it.
- await Role.collection.insertOne({
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.PROMPTS]: { USE: true, SHARED_GLOBAL: true },
- },
- });
-
- await updateAccessPermissions(SystemRoles.USER, {
- [PermissionTypes.PROMPTS]: { SHARE: false }, // explicit false — should be preserved
- });
-
- const updatedRole = await getRoleByName(SystemRoles.USER);
-
- expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARE).toBe(false);
- expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARED_GLOBAL).toBeUndefined();
- });
-
- it('should migrate SHARED_GLOBAL to SHARE even when the permType is not in the update payload', async () => {
- // Bug #2 regression: cleanup block removes SHARED_GLOBAL but migration block only
- // runs when the permType is in the update payload. Without the fix, SHARE would be
- // lost when any other permType (e.g. MULTI_CONVO) is the only thing being updated.
- await Role.collection.insertOne({
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.PROMPTS]: {
- USE: true,
- SHARED_GLOBAL: true, // legacy — NO SHARE present
- },
- [PermissionTypes.MULTI_CONVO]: { USE: false },
- },
- });
-
- // Only update MULTI_CONVO — PROMPTS is intentionally absent from the payload
- await updateAccessPermissions(SystemRoles.USER, {
- [PermissionTypes.MULTI_CONVO]: { USE: true },
- });
-
- const updatedRole = await getRoleByName(SystemRoles.USER);
-
- // SHARE should have been inherited from SHARED_GLOBAL, not silently dropped
- expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARE).toBe(true);
- // SHARED_GLOBAL should be removed
- expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARED_GLOBAL).toBeUndefined();
- // Original USE should be untouched
- expect(updatedRole.permissions[PermissionTypes.PROMPTS].USE).toBe(true);
- // The actual update should have applied
- expect(updatedRole.permissions[PermissionTypes.MULTI_CONVO].USE).toBe(true);
- });
-
- it('should remove orphaned SHARED_GLOBAL when SHARE already exists and permType is not in update', async () => {
- // Safe cleanup case: SHARE already set, SHARED_GLOBAL is just orphaned noise.
- // SHARE must not be changed; SHARED_GLOBAL must be removed.
- await Role.collection.insertOne({
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.PROMPTS]: {
- USE: true,
- SHARE: true, // already migrated
- SHARED_GLOBAL: true, // orphaned
- },
- [PermissionTypes.MULTI_CONVO]: { USE: false },
- },
- });
-
- await updateAccessPermissions(SystemRoles.USER, {
- [PermissionTypes.MULTI_CONVO]: { USE: true },
- });
-
- const updatedRole = await getRoleByName(SystemRoles.USER);
-
- expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARED_GLOBAL).toBeUndefined();
- expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARE).toBe(true);
- expect(updatedRole.permissions[PermissionTypes.MULTI_CONVO].USE).toBe(true);
- });
-
- it('should not update MULTI_CONVO permissions when no changes are needed', async () => {
- await new Role({
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.MULTI_CONVO]: { USE: true },
- },
- }).save();
-
- await updateAccessPermissions(SystemRoles.USER, {
- [PermissionTypes.MULTI_CONVO]: { USE: true },
- });
-
- const updatedRole = await getRoleByName(SystemRoles.USER);
- expect(updatedRole.permissions[PermissionTypes.MULTI_CONVO]).toEqual({ USE: true });
- });
-});
-
-describe('initializeRoles', () => {
- beforeEach(async () => {
- await Role.deleteMany({});
- });
-
- it('should create default roles if they do not exist', async () => {
- await initializeRoles();
-
- const adminRole = await getRoleByName(SystemRoles.ADMIN);
- const userRole = await getRoleByName(SystemRoles.USER);
-
- expect(adminRole).toBeTruthy();
- expect(userRole).toBeTruthy();
-
- // Check if all permission types exist in the permissions field
- Object.values(PermissionTypes).forEach((permType) => {
- expect(adminRole.permissions[permType]).toBeDefined();
- expect(userRole.permissions[permType]).toBeDefined();
- });
-
- // 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);
- });
-
- it('should not modify existing permissions for existing roles', async () => {
- const customUserRole = {
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.PROMPTS]: {
- [Permissions.USE]: false,
- [Permissions.CREATE]: true,
- [Permissions.SHARE]: true,
- },
- [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
- },
- };
-
- await new Role(customUserRole).save();
- await initializeRoles();
-
- const userRole = await getRoleByName(SystemRoles.USER);
- expect(userRole.permissions[PermissionTypes.PROMPTS]).toEqual(
- customUserRole.permissions[PermissionTypes.PROMPTS],
- );
- expect(userRole.permissions[PermissionTypes.BOOKMARKS]).toEqual(
- customUserRole.permissions[PermissionTypes.BOOKMARKS],
- );
- expect(userRole.permissions[PermissionTypes.AGENTS]).toBeDefined();
- });
-
- it('should add new permission types to existing roles', async () => {
- const partialUserRole = {
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.PROMPTS]:
- roleDefaults[SystemRoles.USER].permissions[PermissionTypes.PROMPTS],
- [PermissionTypes.BOOKMARKS]:
- roleDefaults[SystemRoles.USER].permissions[PermissionTypes.BOOKMARKS],
- },
- };
-
- await new Role(partialUserRole).save();
- await 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();
- });
-
- it('should handle multiple runs without duplicating or modifying data', async () => {
- await initializeRoles();
- await initializeRoles();
-
- const adminRoles = await Role.find({ name: SystemRoles.ADMIN });
- const userRoles = await Role.find({ name: SystemRoles.USER });
-
- expect(adminRoles).toHaveLength(1);
- expect(userRoles).toHaveLength(1);
-
- const adminPerms = adminRoles[0].toObject().permissions;
- const userPerms = userRoles[0].toObject().permissions;
- Object.values(PermissionTypes).forEach((permType) => {
- expect(adminPerms[permType]).toBeDefined();
- expect(userPerms[permType]).toBeDefined();
- });
- });
-
- it('should update roles with missing permission types from roleDefaults', async () => {
- const partialAdminRole = {
- name: SystemRoles.ADMIN,
- permissions: {
- [PermissionTypes.PROMPTS]: {
- [Permissions.USE]: false,
- [Permissions.CREATE]: false,
- [Permissions.SHARE]: false,
- },
- [PermissionTypes.BOOKMARKS]:
- roleDefaults[SystemRoles.ADMIN].permissions[PermissionTypes.BOOKMARKS],
- },
- };
-
- await new Role(partialAdminRole).save();
- await initializeRoles();
-
- const adminRole = await getRoleByName(SystemRoles.ADMIN);
- expect(adminRole.permissions[PermissionTypes.PROMPTS]).toEqual(
- 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();
- });
-
- it('should include MULTI_CONVO permissions when creating default roles', async () => {
- await initializeRoles();
-
- const adminRole = await getRoleByName(SystemRoles.ADMIN);
- const userRole = await getRoleByName(SystemRoles.USER);
-
- expect(adminRole.permissions[PermissionTypes.MULTI_CONVO]).toBeDefined();
- expect(userRole.permissions[PermissionTypes.MULTI_CONVO]).toBeDefined();
- expect(adminRole.permissions[PermissionTypes.MULTI_CONVO].USE).toBe(
- roleDefaults[SystemRoles.ADMIN].permissions[PermissionTypes.MULTI_CONVO].USE,
- );
- expect(userRole.permissions[PermissionTypes.MULTI_CONVO].USE).toBe(
- roleDefaults[SystemRoles.USER].permissions[PermissionTypes.MULTI_CONVO].USE,
- );
- });
-
- it('should add MULTI_CONVO permissions to existing roles without them', async () => {
- const partialUserRole = {
- name: SystemRoles.USER,
- permissions: {
- [PermissionTypes.PROMPTS]:
- roleDefaults[SystemRoles.USER].permissions[PermissionTypes.PROMPTS],
- [PermissionTypes.BOOKMARKS]:
- roleDefaults[SystemRoles.USER].permissions[PermissionTypes.BOOKMARKS],
- },
- };
-
- await new Role(partialUserRole).save();
- await initializeRoles();
-
- const userRole = await getRoleByName(SystemRoles.USER);
- expect(userRole.permissions[PermissionTypes.MULTI_CONVO]).toBeDefined();
- expect(userRole.permissions[PermissionTypes.MULTI_CONVO].USE).toBeDefined();
- });
-});
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..2a1cb222f9 100644
--- a/api/models/index.js
+++ b/api/models/index.js
@@ -1,48 +1,22 @@
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();
await methods.seedDefaultRoles();
await methods.ensureDefaultCategories();
+ await methods.seedSystemGrants();
};
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/package.json b/api/package.json
index 89a5183ddd..86b0f22c0b 100644
--- a/api/package.json
+++ b/api/package.json
@@ -1,6 +1,6 @@
{
"name": "@librechat/backend",
- "version": "v0.8.3",
+ "version": "v0.8.4",
"description": "",
"scripts": {
"start": "echo 'please run this from the root directory'",
@@ -35,7 +35,7 @@
"homepage": "https://librechat.ai",
"dependencies": {
"@anthropic-ai/vertex-sdk": "^0.14.3",
- "@aws-sdk/client-bedrock-runtime": "^3.980.0",
+ "@aws-sdk/client-bedrock-runtime": "^3.1013.0",
"@aws-sdk/client-s3": "^3.980.0",
"@aws-sdk/s3-request-presigner": "^3.758.0",
"@azure/identity": "^4.7.0",
@@ -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.63",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7",
@@ -52,7 +52,7 @@
"@node-saml/passport-saml": "^5.1.0",
"@smithy/node-http-handler": "^4.4.5",
"ai-tokenizer": "^1.0.6",
- "axios": "^1.13.5",
+ "axios": "1.13.6",
"bcryptjs": "^2.4.3",
"compression": "^1.8.1",
"connect-redis": "^8.1.0",
@@ -70,7 +70,7 @@
"file-type": "^21.3.2",
"firebase": "^11.0.2",
"form-data": "^4.0.4",
- "handlebars": "^4.7.7",
+ "handlebars": "^4.7.9",
"https-proxy-agent": "^7.0.6",
"ioredis": "^5.3.2",
"js-yaml": "^4.1.1",
@@ -91,7 +91,7 @@
"multer": "^2.1.1",
"nanoid": "^3.3.7",
"node-fetch": "^2.7.0",
- "nodemailer": "^7.0.11",
+ "nodemailer": "^8.0.4",
"ollama": "^0.5.0",
"openai": "5.8.2",
"openid-client": "^6.5.0",
@@ -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/api/server/cleanup.js b/api/server/cleanup.js
index 364c02cd8a..c27814292d 100644
--- a/api/server/cleanup.js
+++ b/api/server/cleanup.js
@@ -123,9 +123,6 @@ function disposeClient(client) {
if (client.maxContextTokens) {
client.maxContextTokens = null;
}
- if (client.contextStrategy) {
- client.contextStrategy = null;
- }
if (client.currentDateString) {
client.currentDateString = null;
}
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/ModelController.js b/api/server/controllers/ModelController.js
index 805d9eef27..4738d45111 100644
--- a/api/server/controllers/ModelController.js
+++ b/api/server/controllers/ModelController.js
@@ -1,40 +1,12 @@
const { logger } = require('@librechat/data-schemas');
-const { CacheKeys } = require('librechat-data-provider');
const { loadDefaultModels, loadConfigModels } = require('~/server/services/Config');
-const { getLogStores } = require('~/cache');
-/**
- * @param {ServerRequest} req
- * @returns {Promise} The models config.
- */
-const getModelsConfig = async (req) => {
- const cache = getLogStores(CacheKeys.CONFIG_STORE);
- let modelsConfig = await cache.get(CacheKeys.MODELS_CONFIG);
- if (!modelsConfig) {
- modelsConfig = await loadModels(req);
- }
+const getModelsConfig = (req) => loadModels(req);
- return modelsConfig;
-};
-
-/**
- * Loads the models from the config.
- * @param {ServerRequest} req - The Express request object.
- * @returns {Promise} The models config.
- */
async function loadModels(req) {
- const cache = getLogStores(CacheKeys.CONFIG_STORE);
- const cachedModelsConfig = await cache.get(CacheKeys.MODELS_CONFIG);
- if (cachedModelsConfig) {
- return cachedModelsConfig;
- }
const defaultModelsConfig = await loadDefaultModels(req);
const customModelsConfig = await loadConfigModels(req);
-
- const modelConfig = { ...defaultModelsConfig, ...customModelsConfig };
-
- await cache.set(CacheKeys.MODELS_CONFIG, modelConfig);
- return modelConfig;
+ return { ...defaultModelsConfig, ...customModelsConfig };
}
async function modelController(req, res) {
diff --git a/api/server/controllers/PermissionsController.js b/api/server/controllers/PermissionsController.js
index 51993d083c..1f200fce83 100644
--- a/api/server/controllers/PermissionsController.js
+++ b/api/server/controllers/PermissionsController.js
@@ -9,22 +9,17 @@ const { enrichRemoteAgentPrincipals, backfillRemoteAgentPermissions } = require(
const {
bulkUpdateResourcePermissions,
ensureGroupPrincipalExists,
+ getResourcePermissionsMap,
+ findAccessibleResources,
getEffectivePermissions,
ensurePrincipalExists,
getAvailableRoles,
- findAccessibleResources,
- getResourcePermissionsMap,
} = require('~/server/services/PermissionService');
-const {
- searchPrincipals: searchLocalPrincipals,
- sortPrincipalsByRelevance,
- calculateRelevanceScore,
-} = require('~/models');
const {
entraIdPrincipalFeatureEnabled,
searchEntraIdPrincipals,
} = require('~/server/services/GraphApiService');
-const { AclEntry, AccessRole } = require('~/db/models');
+const db = require('~/models');
/**
* Generic controller for resource permission endpoints
@@ -155,6 +150,18 @@ 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) {
+ db.removeAgentFromUserFavorites(resourceId, revokedUserIds).catch((err) => {
+ logger.error('[removeRevokedAgentFromFavorites] Error cleaning up favorites', err);
+ });
+ }
+
/** @type {TUpdateResourcePermissionsResponse} */
const response = {
message: 'Permissions updated successfully',
@@ -185,8 +192,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 db.aggregateAclEntries([
// Match ACL entries for this resource
{
$match: {
@@ -282,7 +288,12 @@ const getResourcePermissions = async (req, res) => {
}
if (resourceType === ResourceType.REMOTE_AGENT) {
- const enricherDeps = { AclEntry, AccessRole, logger };
+ const enricherDeps = {
+ aggregateAclEntries: db.aggregateAclEntries,
+ bulkWriteAclEntries: db.bulkWriteAclEntries,
+ findRoleByIdentifier: db.findRoleByIdentifier,
+ logger,
+ };
const enrichResult = await enrichRemoteAgentPrincipals(enricherDeps, resourceId, principals);
principals = enrichResult.principals;
backfillRemoteAgentPermissions(enricherDeps, resourceId, enrichResult.entriesToBackfill);
@@ -399,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);
@@ -455,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/PluginController.js b/api/server/controllers/PluginController.js
index 279ffb15fd..c5d5c5b888 100644
--- a/api/server/controllers/PluginController.js
+++ b/api/server/controllers/PluginController.js
@@ -1,61 +1,37 @@
const { logger } = require('@librechat/data-schemas');
-const { CacheKeys } = require('librechat-data-provider');
const { getToolkitKey, checkPluginAuth, filterUniquePlugins } = require('@librechat/api');
const { getCachedTools, setCachedTools } = require('~/server/services/Config');
const { availableTools, toolkits } = require('~/app/clients/tools');
const { getAppConfig } = require('~/server/services/Config');
-const { getLogStores } = require('~/cache');
const getAvailablePluginsController = async (req, res) => {
try {
- const cache = getLogStores(CacheKeys.TOOL_CACHE);
- const cachedPlugins = await cache.get(CacheKeys.PLUGINS);
- if (cachedPlugins) {
- res.status(200).json(cachedPlugins);
- return;
- }
-
- const appConfig = await getAppConfig({ role: req.user?.role });
- /** @type {{ filteredTools: string[], includedTools: string[] }} */
+ const appConfig = await getAppConfig({ role: req.user?.role, tenantId: req.user?.tenantId });
const { filteredTools = [], includedTools = [] } = appConfig;
- /** @type {import('@librechat/api').LCManifestTool[]} */
- const pluginManifest = availableTools;
- const uniquePlugins = filterUniquePlugins(pluginManifest);
- let authenticatedPlugins = [];
+ const uniquePlugins = filterUniquePlugins(availableTools);
+ const includeSet = new Set(includedTools);
+ const filterSet = new Set(filteredTools);
+
+ /** includedTools takes precedence — filteredTools ignored when both are set. */
+ const plugins = [];
for (const plugin of uniquePlugins) {
- authenticatedPlugins.push(
- checkPluginAuth(plugin) ? { ...plugin, authenticated: true } : plugin,
- );
+ if (includeSet.size > 0) {
+ if (!includeSet.has(plugin.pluginKey)) {
+ continue;
+ }
+ } else if (filterSet.has(plugin.pluginKey)) {
+ continue;
+ }
+ plugins.push(checkPluginAuth(plugin) ? { ...plugin, authenticated: true } : plugin);
}
- let plugins = authenticatedPlugins;
-
- if (includedTools.length > 0) {
- plugins = plugins.filter((plugin) => includedTools.includes(plugin.pluginKey));
- } else {
- plugins = plugins.filter((plugin) => !filteredTools.includes(plugin.pluginKey));
- }
-
- await cache.set(CacheKeys.PLUGINS, plugins);
res.status(200).json(plugins);
} catch (error) {
res.status(500).json({ message: error.message });
}
};
-/**
- * Retrieves and returns a list of available tools, either from a cache or by reading a plugin manifest file.
- *
- * This function first attempts to retrieve the list of tools from a cache. If the tools are not found in the cache,
- * it reads a plugin manifest file, filters for unique plugins, and determines if each plugin is authenticated.
- * Only plugins that are marked as available in the application's local state are included in the final list.
- * The resulting list of tools is then cached and sent to the client.
- *
- * @param {object} req - The request object, containing information about the HTTP request.
- * @param {object} res - The response object, used to send back the desired HTTP response.
- * @returns {Promise} A promise that resolves when the function has completed.
- */
const getAvailableTools = async (req, res) => {
try {
const userId = req.user?.id;
@@ -63,18 +39,10 @@ const getAvailableTools = async (req, res) => {
logger.warn('[getAvailableTools] User ID not found in request');
return res.status(401).json({ message: 'Unauthorized' });
}
- const cache = getLogStores(CacheKeys.TOOL_CACHE);
- const cachedToolsArray = await cache.get(CacheKeys.TOOLS);
- const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role }));
+ const appConfig =
+ req.config ?? (await getAppConfig({ role: req.user?.role, tenantId: req.user?.tenantId }));
- // Return early if we have cached tools
- if (cachedToolsArray != null) {
- res.status(200).json(cachedToolsArray);
- return;
- }
-
- /** @type {Record | null} Get tool definitions to filter which tools are actually available */
let toolDefinitions = await getCachedTools();
if (toolDefinitions == null && appConfig?.availableTools != null) {
@@ -83,26 +51,17 @@ const getAvailableTools = async (req, res) => {
toolDefinitions = appConfig.availableTools;
}
- /** @type {import('@librechat/api').LCManifestTool[]} */
- let pluginManifest = availableTools;
+ const uniquePlugins = filterUniquePlugins(availableTools);
+ const toolDefKeysList = toolDefinitions ? Object.keys(toolDefinitions) : null;
+ const toolDefKeys = toolDefKeysList ? new Set(toolDefKeysList) : null;
- /** @type {TPlugin[]} Deduplicate and authenticate plugins */
- const uniquePlugins = filterUniquePlugins(pluginManifest);
- const authenticatedPlugins = uniquePlugins.map((plugin) => {
- if (checkPluginAuth(plugin)) {
- return { ...plugin, authenticated: true };
- } else {
- return plugin;
- }
- });
-
- /** Filter plugins based on availability */
const toolsOutput = [];
- for (const plugin of authenticatedPlugins) {
- const isToolDefined = toolDefinitions?.[plugin.pluginKey] !== undefined;
+ for (const plugin of uniquePlugins) {
+ const isToolDefined = toolDefKeys?.has(plugin.pluginKey) === true;
const isToolkit =
plugin.toolkit === true &&
- Object.keys(toolDefinitions ?? {}).some(
+ toolDefKeysList != null &&
+ toolDefKeysList.some(
(key) => getToolkitKey({ toolkits, toolName: key }) === plugin.pluginKey,
);
@@ -110,13 +69,10 @@ const getAvailableTools = async (req, res) => {
continue;
}
- toolsOutput.push(plugin);
+ toolsOutput.push(checkPluginAuth(plugin) ? { ...plugin, authenticated: true } : plugin);
}
- const finalTools = filterUniquePlugins(toolsOutput);
- await cache.set(CacheKeys.TOOLS, finalTools);
-
- res.status(200).json(finalTools);
+ res.status(200).json(toolsOutput);
} catch (error) {
logger.error('[getAvailableTools]', error);
res.status(500).json({ message: error.message });
diff --git a/api/server/controllers/PluginController.spec.js b/api/server/controllers/PluginController.spec.js
index 06a51a3bd6..9288680567 100644
--- a/api/server/controllers/PluginController.spec.js
+++ b/api/server/controllers/PluginController.spec.js
@@ -1,6 +1,4 @@
-const { CacheKeys } = require('librechat-data-provider');
const { getCachedTools, getAppConfig } = require('~/server/services/Config');
-const { getLogStores } = require('~/cache');
jest.mock('@librechat/data-schemas', () => ({
logger: {
@@ -19,22 +17,15 @@ jest.mock('~/server/services/Config', () => ({
setCachedTools: jest.fn(),
}));
-// loadAndFormatTools mock removed - no longer used in PluginController
-// getMCPManager mock removed - no longer used in PluginController
-
jest.mock('~/app/clients/tools', () => ({
availableTools: [],
toolkits: [],
}));
-jest.mock('~/cache', () => ({
- getLogStores: jest.fn(),
-}));
-
const { getAvailableTools, getAvailablePluginsController } = require('./PluginController');
describe('PluginController', () => {
- let mockReq, mockRes, mockCache;
+ let mockReq, mockRes;
beforeEach(() => {
jest.clearAllMocks();
@@ -46,17 +37,12 @@ describe('PluginController', () => {
},
};
mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn() };
- mockCache = { get: jest.fn(), set: jest.fn() };
- getLogStores.mockReturnValue(mockCache);
- // Clear availableTools and toolkits arrays before each test
require('~/app/clients/tools').availableTools.length = 0;
require('~/app/clients/tools').toolkits.length = 0;
- // Reset getCachedTools mock to ensure clean state
getCachedTools.mockReset();
- // Reset getAppConfig mock to ensure clean state with default values
getAppConfig.mockReset();
getAppConfig.mockResolvedValue({
filteredTools: [],
@@ -64,31 +50,8 @@ describe('PluginController', () => {
});
});
- describe('cache namespace', () => {
- it('getAvailablePluginsController should use TOOL_CACHE namespace', async () => {
- mockCache.get.mockResolvedValue([]);
- await getAvailablePluginsController(mockReq, mockRes);
- expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
- });
-
- it('getAvailableTools should use TOOL_CACHE namespace', async () => {
- mockCache.get.mockResolvedValue([]);
- await getAvailableTools(mockReq, mockRes);
- expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
- });
-
- it('should NOT use CONFIG_STORE namespace for tool/plugin operations', async () => {
- mockCache.get.mockResolvedValue([]);
- await getAvailablePluginsController(mockReq, mockRes);
- await getAvailableTools(mockReq, mockRes);
- const allCalls = getLogStores.mock.calls.flat();
- expect(allCalls).not.toContain(CacheKeys.CONFIG_STORE);
- });
- });
-
describe('getAvailablePluginsController', () => {
it('should use filterUniquePlugins to remove duplicate plugins', async () => {
- // Add plugins with duplicates to availableTools
const mockPlugins = [
{ name: 'Plugin1', pluginKey: 'key1', description: 'First' },
{ name: 'Plugin1', pluginKey: 'key1', description: 'First duplicate' },
@@ -97,9 +60,6 @@ describe('PluginController', () => {
require('~/app/clients/tools').availableTools.push(...mockPlugins);
- mockCache.get.mockResolvedValue(null);
-
- // Configure getAppConfig to return the expected config
getAppConfig.mockResolvedValueOnce({
filteredTools: [],
includedTools: [],
@@ -109,21 +69,16 @@ describe('PluginController', () => {
expect(mockRes.status).toHaveBeenCalledWith(200);
const responseData = mockRes.json.mock.calls[0][0];
- // The real filterUniquePlugins should have removed the duplicate
expect(responseData).toHaveLength(2);
expect(responseData[0].pluginKey).toBe('key1');
expect(responseData[1].pluginKey).toBe('key2');
});
it('should use checkPluginAuth to verify plugin authentication', async () => {
- // checkPluginAuth returns false for plugins without authConfig
- // so authenticated property won't be added
const mockPlugin = { name: 'Plugin1', pluginKey: 'key1', description: 'First' };
require('~/app/clients/tools').availableTools.push(mockPlugin);
- mockCache.get.mockResolvedValue(null);
- // Configure getAppConfig to return the expected config
getAppConfig.mockResolvedValueOnce({
filteredTools: [],
includedTools: [],
@@ -132,23 +87,9 @@ describe('PluginController', () => {
await getAvailablePluginsController(mockReq, mockRes);
const responseData = mockRes.json.mock.calls[0][0];
- // The real checkPluginAuth returns false for plugins without authConfig, so authenticated property is not added
expect(responseData[0].authenticated).toBeUndefined();
});
- it('should return cached plugins when available', async () => {
- const cachedPlugins = [
- { name: 'CachedPlugin', pluginKey: 'cached', description: 'Cached plugin' },
- ];
-
- mockCache.get.mockResolvedValue(cachedPlugins);
-
- await getAvailablePluginsController(mockReq, mockRes);
-
- // When cache is hit, we return immediately without processing
- expect(mockRes.json).toHaveBeenCalledWith(cachedPlugins);
- });
-
it('should filter plugins based on includedTools', async () => {
const mockPlugins = [
{ name: 'Plugin1', pluginKey: 'key1', description: 'First' },
@@ -156,9 +97,7 @@ describe('PluginController', () => {
];
require('~/app/clients/tools').availableTools.push(...mockPlugins);
- mockCache.get.mockResolvedValue(null);
- // Configure getAppConfig to return config with includedTools
getAppConfig.mockResolvedValueOnce({
filteredTools: [],
includedTools: ['key1'],
@@ -170,6 +109,47 @@ describe('PluginController', () => {
expect(responseData).toHaveLength(1);
expect(responseData[0].pluginKey).toBe('key1');
});
+
+ it('should exclude plugins in filteredTools', async () => {
+ const mockPlugins = [
+ { name: 'Plugin1', pluginKey: 'key1', description: 'First' },
+ { name: 'Plugin2', pluginKey: 'key2', description: 'Second' },
+ ];
+
+ require('~/app/clients/tools').availableTools.push(...mockPlugins);
+
+ getAppConfig.mockResolvedValueOnce({
+ filteredTools: ['key2'],
+ includedTools: [],
+ });
+
+ await getAvailablePluginsController(mockReq, mockRes);
+
+ const responseData = mockRes.json.mock.calls[0][0];
+ expect(responseData).toHaveLength(1);
+ expect(responseData[0].pluginKey).toBe('key1');
+ });
+
+ it('should ignore filteredTools when includedTools is set', async () => {
+ const mockPlugins = [
+ { name: 'Plugin1', pluginKey: 'key1', description: 'First' },
+ { name: 'Plugin2', pluginKey: 'key2', description: 'Second' },
+ { name: 'Plugin3', pluginKey: 'key3', description: 'Third' },
+ ];
+
+ require('~/app/clients/tools').availableTools.push(...mockPlugins);
+
+ getAppConfig.mockResolvedValueOnce({
+ includedTools: ['key1', 'key2'],
+ filteredTools: ['key2'],
+ });
+
+ await getAvailablePluginsController(mockReq, mockRes);
+
+ const responseData = mockRes.json.mock.calls[0][0];
+ expect(responseData).toHaveLength(2);
+ expect(responseData.map((p) => p.pluginKey)).toEqual(['key1', 'key2']);
+ });
});
describe('getAvailableTools', () => {
@@ -185,12 +165,11 @@ describe('PluginController', () => {
},
};
- const mockCachedPlugins = [
+ require('~/app/clients/tools').availableTools.push(
{ name: 'user-tool', pluginKey: 'user-tool', description: 'Duplicate user tool' },
{ name: 'ManifestTool', pluginKey: 'manifest-tool', description: 'Manifest tool' },
- ];
+ );
- mockCache.get.mockResolvedValue(mockCachedPlugins);
getCachedTools.mockResolvedValueOnce(mockUserTools);
mockReq.config = {
mcpConfig: null,
@@ -202,24 +181,19 @@ describe('PluginController', () => {
expect(mockRes.status).toHaveBeenCalledWith(200);
const responseData = mockRes.json.mock.calls[0][0];
expect(Array.isArray(responseData)).toBe(true);
- // The real filterUniquePlugins should have deduplicated tools with same pluginKey
const userToolCount = responseData.filter((tool) => tool.pluginKey === 'user-tool').length;
expect(userToolCount).toBe(1);
});
it('should use checkPluginAuth to verify authentication status', async () => {
- // Add a plugin to availableTools that will be checked
const mockPlugin = {
name: 'Tool1',
pluginKey: 'tool1',
description: 'Tool 1',
- // No authConfig means checkPluginAuth returns false
};
require('~/app/clients/tools').availableTools.push(mockPlugin);
- mockCache.get.mockResolvedValue(null);
- // getCachedTools returns the tool definitions
getCachedTools.mockResolvedValueOnce({
tool1: {
type: 'function',
@@ -242,7 +216,6 @@ describe('PluginController', () => {
expect(Array.isArray(responseData)).toBe(true);
const tool = responseData.find((t) => t.pluginKey === 'tool1');
expect(tool).toBeDefined();
- // The real checkPluginAuth returns false for plugins without authConfig, so authenticated property is not added
expect(tool.authenticated).toBeUndefined();
});
@@ -256,15 +229,12 @@ describe('PluginController', () => {
require('~/app/clients/tools').availableTools.push(mockToolkit);
- // Mock toolkits to have a mapping
require('~/app/clients/tools').toolkits.push({
name: 'Toolkit1',
pluginKey: 'toolkit1',
tools: ['toolkit1_function'],
});
- mockCache.get.mockResolvedValue(null);
- // getCachedTools returns the tool definitions
getCachedTools.mockResolvedValueOnce({
toolkit1_function: {
type: 'function',
@@ -292,7 +262,7 @@ describe('PluginController', () => {
describe('helper function integration', () => {
it('should handle error cases gracefully', async () => {
- mockCache.get.mockRejectedValue(new Error('Cache error'));
+ getCachedTools.mockRejectedValue(new Error('Cache error'));
await getAvailableTools(mockReq, mockRes);
@@ -302,17 +272,7 @@ describe('PluginController', () => {
});
describe('edge cases with undefined/null values', () => {
- it('should handle undefined cache gracefully', async () => {
- getLogStores.mockReturnValue(undefined);
-
- await getAvailableTools(mockReq, mockRes);
-
- expect(mockRes.status).toHaveBeenCalledWith(500);
- });
-
- it('should handle null cachedTools and cachedUserTools', async () => {
- mockCache.get.mockResolvedValue(null);
- // getCachedTools returns empty object instead of null
+ it('should handle null cachedTools', async () => {
getCachedTools.mockResolvedValueOnce({});
mockReq.config = {
mcpConfig: null,
@@ -321,51 +281,40 @@ describe('PluginController', () => {
await getAvailableTools(mockReq, mockRes);
- // Should handle null values gracefully
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([]);
});
it('should handle when getCachedTools returns undefined', async () => {
- mockCache.get.mockResolvedValue(null);
mockReq.config = {
mcpConfig: null,
paths: { structuredTools: '/mock/path' },
};
- // Mock getCachedTools to return undefined
getCachedTools.mockReset();
getCachedTools.mockResolvedValueOnce(undefined);
await getAvailableTools(mockReq, mockRes);
- // Should handle undefined values gracefully
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([]);
});
it('should handle empty toolDefinitions object', async () => {
- mockCache.get.mockResolvedValue(null);
- // Reset getCachedTools to ensure clean state
getCachedTools.mockReset();
getCachedTools.mockResolvedValue({});
- mockReq.config = {}; // No mcpConfig at all
+ mockReq.config = {};
- // Ensure no plugins are available
require('~/app/clients/tools').availableTools.length = 0;
await getAvailableTools(mockReq, mockRes);
- // With empty tool definitions, no tools should be in the final output
expect(mockRes.json).toHaveBeenCalledWith([]);
});
it('should handle undefined filteredTools and includedTools', async () => {
mockReq.config = {};
- mockCache.get.mockResolvedValue(null);
- // Configure getAppConfig to return config with undefined properties
- // The controller will use default values [] for filteredTools and includedTools
getAppConfig.mockResolvedValueOnce({});
await getAvailablePluginsController(mockReq, mockRes);
@@ -382,13 +331,8 @@ describe('PluginController', () => {
toolkit: true,
};
- // No need to mock app.locals anymore as it's not used
-
- // Add the toolkit to availableTools
require('~/app/clients/tools').availableTools.push(mockToolkit);
- mockCache.get.mockResolvedValue(null);
- // getCachedTools returns empty object to avoid null reference error
getCachedTools.mockResolvedValueOnce({});
mockReq.config = {
mcpConfig: null,
@@ -397,43 +341,32 @@ describe('PluginController', () => {
await getAvailableTools(mockReq, mockRes);
- // Should handle null toolDefinitions gracefully
expect(mockRes.status).toHaveBeenCalledWith(200);
});
- it('should handle undefined toolDefinitions when checking isToolDefined (traversaal_search bug)', async () => {
- // This test reproduces the bug where toolDefinitions is undefined
- // and accessing toolDefinitions[plugin.pluginKey] causes a TypeError
+ it('should handle undefined toolDefinitions when checking isToolDefined', async () => {
const mockPlugin = {
name: 'Traversaal Search',
pluginKey: 'traversaal_search',
description: 'Search plugin',
};
- // Add the plugin to availableTools
require('~/app/clients/tools').availableTools.push(mockPlugin);
- mockCache.get.mockResolvedValue(null);
-
mockReq.config = {
mcpConfig: null,
paths: { structuredTools: '/mock/path' },
};
- // CRITICAL: getCachedTools returns undefined
- // This is what causes the bug when trying to access toolDefinitions[plugin.pluginKey]
getCachedTools.mockResolvedValueOnce(undefined);
- // This should not throw an error with the optional chaining fix
await getAvailableTools(mockReq, mockRes);
- // Should handle undefined toolDefinitions gracefully and return empty array
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([]);
});
it('should re-initialize tools from appConfig when cache returns null', async () => {
- // Setup: Initial state with tools in appConfig
const mockAppTools = {
tool1: {
type: 'function',
@@ -453,15 +386,12 @@ describe('PluginController', () => {
},
};
- // Add matching plugins to availableTools
require('~/app/clients/tools').availableTools.push(
{ name: 'Tool 1', pluginKey: 'tool1', description: 'Tool 1' },
{ name: 'Tool 2', pluginKey: 'tool2', description: 'Tool 2' },
);
- // Simulate cache cleared state (returns null)
- mockCache.get.mockResolvedValue(null);
- getCachedTools.mockResolvedValueOnce(null); // Global tools (cache cleared)
+ getCachedTools.mockResolvedValueOnce(null);
mockReq.config = {
filteredTools: [],
@@ -469,15 +399,12 @@ describe('PluginController', () => {
availableTools: mockAppTools,
};
- // Mock setCachedTools to verify it's called to re-initialize
const { setCachedTools } = require('~/server/services/Config');
await getAvailableTools(mockReq, mockRes);
- // Should have re-initialized the cache with tools from appConfig
expect(setCachedTools).toHaveBeenCalledWith(mockAppTools);
- // Should still return tools successfully
expect(mockRes.status).toHaveBeenCalledWith(200);
const responseData = mockRes.json.mock.calls[0][0];
expect(responseData).toHaveLength(2);
@@ -486,29 +413,22 @@ describe('PluginController', () => {
});
it('should handle cache clear without appConfig.availableTools gracefully', async () => {
- // Setup: appConfig without availableTools
getAppConfig.mockResolvedValue({
filteredTools: [],
includedTools: [],
- // No availableTools property
});
- // Clear availableTools array
require('~/app/clients/tools').availableTools.length = 0;
- // Cache returns null (cleared state)
- mockCache.get.mockResolvedValue(null);
- getCachedTools.mockResolvedValueOnce(null); // Global tools (cache cleared)
+ getCachedTools.mockResolvedValueOnce(null);
mockReq.config = {
filteredTools: [],
includedTools: [],
- // No availableTools
};
await getAvailableTools(mockReq, mockRes);
- // Should handle gracefully without crashing
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([]);
});
diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js
index 6d5df0ac8d..16b68968d9 100644
--- a/api/server/controllers/UserController.js
+++ b/api/server/controllers/UserController.js
@@ -1,54 +1,32 @@
+const mongoose = require('mongoose');
const { logger, webSearchKeys } = require('@librechat/data-schemas');
-const { Tools, CacheKeys, Constants, FileSources } = require('librechat-data-provider');
const {
+ getNewS3URL,
+ needsRefresh,
MCPOAuthHandler,
MCPTokenStorage,
normalizeHttpError,
extractWebSearchEnvVars,
} = require('@librechat/api');
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');
+ Tools,
+ CacheKeys,
+ Constants,
+ FileSources,
+ ResourceType,
+} = require('librechat-data-provider');
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');
-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 { getLogStores } = require('~/cache');
+const db = require('~/models');
const getUserController = async (req, res) => {
- const appConfig = await getAppConfig({ role: req.user?.role });
+ const appConfig = await getAppConfig({ role: req.user?.role, tenantId: req.user?.tenantId });
/** @type {IUser} */
const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user };
/**
@@ -66,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);
@@ -77,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' });
}
@@ -90,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' });
}
@@ -103,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,
@@ -113,13 +91,86 @@ 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;
+ const AclEntry = mongoose.models.AclEntry;
+ if (!MCPServer) {
+ return;
+ }
+
+ const userObjectId = new mongoose.Types.ObjectId(userId);
+ const soleOwnedIds = await db.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 appConfig = await getAppConfig({ role: req.user?.role, tenantId: req.user?.tenantId });
const { user } = req;
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) {
@@ -243,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',
);
@@ -259,37 +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(req, user.id); // delete user prompts
- 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 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) {
@@ -349,7 +397,7 @@ const maybeUninstallOAuthMCP = async (userId, pluginKey, appConfig) => {
const clientTokenData = await MCPTokenStorage.getClientInfoAndMetadata({
userId,
serverName,
- findToken,
+ findToken: db.findToken,
});
if (clientTokenData == null) {
return;
@@ -360,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
@@ -419,7 +467,7 @@ const maybeUninstallOAuthMCP = async (userId, pluginKey, appConfig) => {
userId,
serverName,
deleteToken: async (filter) => {
- await Token.deleteOne(filter);
+ await db.deleteTokens(filter);
},
});
@@ -439,4 +487,5 @@ module.exports = {
verifyEmailController,
updateUserPluginsController,
resendVerificationController,
+ deleteUserMcpServers,
};
diff --git a/api/server/controllers/UserController.spec.js b/api/server/controllers/UserController.spec.js
new file mode 100644
index 0000000000..4a96072062
--- /dev/null
+++ b/api/server/controllers/UserController.spec.js
@@ -0,0 +1,225 @@
+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', () => {
+ 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(),
+ deleteUserPluginAuth: jest.fn().mockResolvedValue(undefined),
+}));
+
+jest.mock('~/server/services/AuthService', () => ({
+ verifyEmail: jest.fn(),
+ resendVerificationEmail: jest.fn(),
+}));
+
+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(),
+}));
+
+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('~/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/__tests__/PermissionsController.spec.js b/api/server/controllers/__tests__/PermissionsController.spec.js
new file mode 100644
index 0000000000..a8d9518455
--- /dev/null
+++ b/api/server/controllers/__tests__/PermissionsController.spec.js
@@ -0,0 +1,242 @@
+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(),
+}));
+
+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', () => ({
+ entraIdPrincipalFeatureEnabled: jest.fn(() => false),
+ searchEntraIdPrincipals: jest.fn(),
+}));
+
+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: [],
+ });
+
+ mockRemoveAgentFromUserFavorites.mockResolvedValue(undefined);
+ });
+
+ 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(mockRemoveAgentFromUserFavorites).toHaveBeenCalledWith(agentObjectId, [revokedUserId]);
+ });
+
+ 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(mockRemoveAgentFromUserFavorites).toHaveBeenCalledWith(agentObjectId, [revokedUserId]);
+ });
+
+ 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(mockRemoveAgentFromUserFavorites).toHaveBeenCalledWith(agentObjectId, [validId]);
+ });
+
+ 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(mockRemoveAgentFromUserFavorites).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(mockRemoveAgentFromUserFavorites).not.toHaveBeenCalled();
+ });
+
+ it('handles agent not found gracefully', async () => {
+ mockRemoveAgentFromUserFavorites.mockResolvedValue(undefined);
+
+ 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(mockRemoveAgentFromUserFavorites).toHaveBeenCalled();
+ expect(res.status).toHaveBeenCalledWith(200);
+ });
+
+ 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 },
+ 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/api/server/controllers/__tests__/deleteUser.spec.js b/api/server/controllers/__tests__/deleteUser.spec.js
index d0f54a046f..a382a6cdc7 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,20 @@ 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(),
+ getSoleOwnedResourceIds: jest.fn().mockResolvedValue([]),
}));
jest.mock('~/server/services/PluginService', () => ({
@@ -91,11 +93,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),
}));
@@ -104,18 +101,6 @@ 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(),
}));
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/controllers/agents/__tests__/openai.spec.js b/api/server/controllers/agents/__tests__/openai.spec.js
index 50c61b7288..c959be6cf4 100644
--- a/api/server/controllers/agents/__tests__/openai.spec.js
+++ b/api/server/controllers/agents/__tests__/openai.spec.js
@@ -3,6 +3,7 @@
* Tests that recordCollectedUsage is called correctly for token spending
*/
+const mockProcessStream = jest.fn().mockResolvedValue(undefined);
const mockSpendTokens = jest.fn().mockResolvedValue({});
const mockSpendStructuredTokens = jest.fn().mockResolvedValue({});
const mockRecordCollectedUsage = jest
@@ -35,7 +36,7 @@ jest.mock('@librechat/agents', () => ({
jest.mock('@librechat/api', () => ({
writeSSE: jest.fn(),
createRun: jest.fn().mockResolvedValue({
- processStream: jest.fn().mockResolvedValue(undefined),
+ processStream: mockProcessStream,
}),
createChunk: jest.fn().mockReturnValue({}),
buildToolSet: jest.fn().mockReturnValue(new Set()),
@@ -68,6 +69,7 @@ jest.mock('@librechat/api', () => ({
toolCalls: new Map(),
usage: { promptTokens: 100, completionTokens: 50, reasoningTokens: 0 },
}),
+ resolveRecursionLimit: jest.fn().mockReturnValue(50),
createToolExecuteHandler: jest.fn().mockReturnValue({ handle: jest.fn() }),
isChatCompletionValidationFailure: jest.fn().mockReturnValue(false),
}));
@@ -77,43 +79,25 @@ 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', () => ({
- getMultiplier: mockGetMultiplier,
- getCacheMultiplier: mockGetCacheMultiplier,
-}));
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', () => ({
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,12 @@ jest.mock('~/models', () => ({
getCodeGeneratedFiles: jest.fn(),
updateBalance: mockUpdateBalance,
bulkInsertTransactions: mockBulkInsertTransactions,
+ spendTokens: mockSpendTokens,
+ spendStructuredTokens: mockSpendStructuredTokens,
+ getMultiplier: mockGetMultiplier,
+ getCacheMultiplier: mockGetCacheMultiplier,
+ getConvoFiles: jest.fn().mockResolvedValue([]),
+ getConvo: jest.fn().mockResolvedValue(null),
}));
describe('OpenAIChatCompletionController', () => {
@@ -163,7 +153,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();
});
@@ -180,7 +170,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',
@@ -198,7 +188,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',
@@ -216,7 +206,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',
@@ -298,4 +288,36 @@ describe('OpenAIChatCompletionController', () => {
);
});
});
+
+ describe('recursionLimit resolution', () => {
+ it('should pass resolveRecursionLimit result to processStream config', async () => {
+ const { resolveRecursionLimit } = require('@librechat/api');
+ resolveRecursionLimit.mockReturnValueOnce(75);
+
+ await OpenAIChatCompletionController(req, res);
+
+ expect(mockProcessStream).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ recursionLimit: 75 }),
+ expect.anything(),
+ );
+ });
+
+ it('should call resolveRecursionLimit with agentsEConfig and agent', async () => {
+ const { resolveRecursionLimit } = require('@librechat/api');
+ const { getAgent } = require('~/models');
+ const mockAgent = { id: 'agent-123', name: 'Test', recursion_limit: 200 };
+ getAgent.mockResolvedValueOnce(mockAgent);
+
+ req.config = {
+ endpoints: {
+ agents: { recursionLimit: 100, maxRecursionLimit: 150, allowedProviders: [] },
+ },
+ };
+
+ await OpenAIChatCompletionController(req, res);
+
+ expect(resolveRecursionLimit).toHaveBeenCalledWith(req.config.endpoints.agents, mockAgent);
+ });
+ });
});
diff --git a/api/server/controllers/agents/__tests__/responses.unit.spec.js b/api/server/controllers/agents/__tests__/responses.unit.spec.js
index e34f0ccf73..26f5f5d30b 100644
--- a/api/server/controllers/agents/__tests__/responses.unit.spec.js
+++ b/api/server/controllers/agents/__tests__/responses.unit.spec.js
@@ -101,46 +101,33 @@ 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', () => ({
- getMultiplier: mockGetMultiplier,
- getCacheMultiplier: mockGetCacheMultiplier,
-}));
-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([]),
}));
-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 +139,13 @@ jest.mock('~/models', () => ({
getCodeGeneratedFiles: jest.fn(),
updateBalance: mockUpdateBalance,
bulkInsertTransactions: mockBulkInsertTransactions,
+ spendTokens: mockSpendTokens,
+ spendStructuredTokens: mockSpendStructuredTokens,
+ getMultiplier: mockGetMultiplier,
+ getCacheMultiplier: mockGetCacheMultiplier,
+ getConvoFiles: jest.fn().mockResolvedValue([]),
+ saveConvo: jest.fn().mockResolvedValue({}),
+ getConvo: jest.fn().mockResolvedValue(null),
}));
describe('createResponse controller', () => {
@@ -191,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();
});
@@ -218,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',
@@ -241,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',
@@ -264,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',
@@ -386,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', {
@@ -424,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/__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/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 c454bd65cf..3c1f91bd60 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,
@@ -21,9 +21,13 @@ const {
recordCollectedUsage,
GenerationJobManager,
getTransactionsConfig,
+ resolveRecursionLimit,
createMemoryProcessor,
+ loadAgent: loadAgentFn,
createMultiAgentMapper,
filterMalformedContentParts,
+ countFormattedMessageTokens,
+ hydrateMissingIndexTokenCounts,
} = require('@librechat/api');
const {
Callback,
@@ -45,18 +49,16 @@ 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 { resolveConfigServers } = require('~/server/services/MCP');
+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);
@@ -64,9 +66,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;
@@ -218,7 +217,6 @@ class AgentClient extends BaseClient {
}))
: []),
];
-
if (this.options.attachments) {
const attachments = await this.options.attachments;
const latestMessage = orderedMessages[orderedMessages.length - 1];
@@ -245,6 +243,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,
@@ -264,12 +267,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 */
@@ -283,17 +288,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.
@@ -323,23 +348,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,
};
@@ -357,6 +379,9 @@ class AgentClient extends BaseClient {
*/
const ephemeralAgent = this.options.req.body.ephemeralAgent;
const mcpManager = getMCPManager();
+
+ const configServers = await resolveConfigServers(this.options.req);
+
await Promise.all(
allAgents.map(({ agent, agentId }) =>
applyContextToAgent({
@@ -364,6 +389,7 @@ class AgentClient extends BaseClient {
agentId,
logger,
mcpManager,
+ configServers,
sharedRunContext,
ephemeralAgent: agentId === this.options.agent.id ? ephemeralAgent : undefined,
}),
@@ -413,7 +439,7 @@ class AgentClient extends BaseClient {
user,
permissionType: PermissionTypes.MEMORIES,
permissions: [Permissions.USE],
- getRoleByName,
+ getRoleByName: db.getRoleByName,
});
if (!hasAccess) {
@@ -473,9 +499,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,10 +657,10 @@ class AgentClient extends BaseClient {
}) {
const result = 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: this.user ?? this.options.req.user?.id,
@@ -667,39 +693,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());
}
/**
@@ -740,18 +734,41 @@ class AgentClient extends BaseClient {
},
user: createSafeUser(this.options.req.user),
},
- recursionLimit: agentsEConfig?.recursionLimit ?? 50,
+ recursionLimit: resolveRecursionLimit(agentsEConfig, this.options.agent),
signal: abortController.signal,
streamMode: 'values',
version: 'v2',
};
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
@@ -765,17 +782,6 @@ class AgentClient extends BaseClient {
agents.push(...this.agentConfigs.values());
}
- if (agents[0].recursion_limit && typeof agents[0].recursion_limit === 'number') {
- config.recursionLimit = agents[0].recursion_limit;
- }
-
- if (
- agentsEConfig?.maxRecursionLimit &&
- config.recursionLimit > agentsEConfig?.maxRecursionLimit
- ) {
- config.recursionLimit = agentsEConfig?.maxRecursionLimit;
- }
-
// TODO: needs to be added as part of AgentContext initialization
// const noSystemModelRegex = [/\b(o1-preview|o1-mini|amazon\.titan-text)\b/gi];
// const noSystemMessages = noSystemModelRegex.some((regex) =>
@@ -805,16 +811,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) {
@@ -845,6 +867,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) => {
@@ -875,6 +898,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) {
@@ -1060,6 +1095,7 @@ class AgentClient extends BaseClient {
titlePrompt: endpointConfig?.titlePrompt,
titlePromptTemplate: endpointConfig?.titlePromptTemplate,
chainOptions: {
+ runName: 'TitleRun',
signal: abortController.signal,
callbacks: [
{
@@ -1134,7 +1170,7 @@ class AgentClient extends BaseClient {
context = 'message',
}) {
try {
- await spendTokens(
+ await db.spendTokens(
{
model,
context,
@@ -1153,7 +1189,7 @@ class AgentClient extends BaseClient {
'reasoning_tokens' in usage &&
typeof usage.reasoning_tokens === 'number'
) {
- await spendTokens(
+ await db.spendTokens(
{
model,
balance,
@@ -1181,16 +1217,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 42481e1644..1595f652f7 100644
--- a/api/server/controllers/agents/client.test.js
+++ b/api/server/controllers/agents/client.test.js
@@ -15,13 +15,19 @@ 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('~/server/services/MCP', () => ({
+ resolveConfigServers: jest.fn().mockResolvedValue({}),
+}));
+
+jest.mock('~/models', () => ({
+ getAgent: jest.fn(),
getRoleByName: jest.fn(),
}));
@@ -1313,7 +1319,7 @@ describe('AgentClient - titleConvo', () => {
});
// Verify formatInstructionsForContext was called with correct server names
- expect(mockFormatInstructions).toHaveBeenCalledWith(['server1', 'server2']);
+ expect(mockFormatInstructions).toHaveBeenCalledWith(['server1', 'server2'], {});
// Verify the instructions do NOT contain [object Promise]
expect(client.options.agent.instructions).not.toContain('[object Promise]');
@@ -1353,10 +1359,10 @@ describe('AgentClient - titleConvo', () => {
});
// Verify formatInstructionsForContext was called with ephemeral server names
- expect(mockFormatInstructions).toHaveBeenCalledWith([
- 'ephemeral-server1',
- 'ephemeral-server2',
- ]);
+ expect(mockFormatInstructions).toHaveBeenCalledWith(
+ ['ephemeral-server1', 'ephemeral-server2'],
+ {},
+ );
// Verify no [object Promise] in instructions
expect(client.options.agent.instructions).not.toContain('[object Promise]');
@@ -1816,7 +1822,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);
@@ -2138,7 +2144,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 +2201,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/filterAuthorizedTools.spec.js b/api/server/controllers/agents/filterAuthorizedTools.spec.js
index 259e41fb0d..e6b41aef16 100644
--- a/api/server/controllers/agents/filterAuthorizedTools.spec.js
+++ b/api/server/controllers/agents/filterAuthorizedTools.spec.js
@@ -22,8 +22,8 @@ jest.mock('~/config', () => ({
})),
}));
-jest.mock('~/models/Project', () => ({
- getProjectByName: jest.fn().mockResolvedValue(null),
+jest.mock('~/server/services/MCP', () => ({
+ resolveConfigServers: jest.fn().mockResolvedValue({}),
}));
jest.mock('~/server/services/Files/strategies', () => ({
@@ -34,23 +34,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 +46,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(() => ({
@@ -232,7 +227,27 @@ describe('MCP Tool Authorization', () => {
availableTools,
});
- expect(mockGetAllServerConfigs).toHaveBeenCalledWith('specific-user-id');
+ expect(mockGetAllServerConfigs).toHaveBeenCalledWith('specific-user-id', undefined);
+ });
+
+ test('should pass configServers to getAllServerConfigs and allow config-override servers', async () => {
+ const configServers = {
+ 'config-override-server': { type: 'sse', url: 'https://override.example.com' },
+ };
+ mockGetAllServerConfigs.mockResolvedValue({
+ 'config-override-server': configServers['config-override-server'],
+ });
+
+ const result = await filterAuthorizedTools({
+ tools: [`tool${d}config-override-server`, `tool${d}unauthorizedServer`],
+ userId,
+ availableTools,
+ configServers,
+ });
+
+ expect(mockGetAllServerConfigs).toHaveBeenCalledWith(userId, configServers);
+ expect(result).toContain(`tool${d}config-override-server`);
+ expect(result).not.toContain(`tool${d}unauthorizedServer`);
});
test('should only call getAllServerConfigs once even with multiple MCP tools', async () => {
diff --git a/api/server/controllers/agents/openai.js b/api/server/controllers/agents/openai.js
index 189cb29d8d..9fa3af82c3 100644
--- a/api/server/controllers/agents/openai.js
+++ b/api/server/controllers/agents/openai.js
@@ -15,19 +15,21 @@ const {
createErrorResponse,
recordCollectedUsage,
getTransactionsConfig,
+ resolveRecursionLimit,
createToolExecuteHandler,
buildNonStreamingResponse,
createOpenAIStreamTracker,
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 { 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 +141,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,
@@ -185,7 +187,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');
}
}
@@ -193,10 +195,8 @@ const OpenAIChatCompletionController = async (req, res) => {
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,
- );
+ const agentsEConfig = appConfig?.endpoints?.[EModelEndpoint.agents];
+ const allowedProviders = new Set(agentsEConfig?.allowedProviders);
// Create tool loader
const loadTools = createToolLoader(abortController.signal);
@@ -221,7 +221,7 @@ const OpenAIChatCompletionController = async (req, res) => {
isInitialAgent: true,
},
{
- getConvoFiles,
+ getConvoFiles: db.getConvoFiles,
getFiles: db.getFiles,
getUserKey: db.getUserKey,
getMessages: db.getMessages,
@@ -286,14 +286,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
@@ -436,24 +438,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
@@ -466,7 +474,9 @@ const OpenAIChatCompletionController = async (req, res) => {
agents: [primaryConfig],
messages: formattedMessages,
indexTokenCountMap,
+ initialSummary,
runId: responseId,
+ summarizationConfig,
signal: abortController.signal,
customHandlers: handlers,
requestBody: {
@@ -480,7 +490,6 @@ const OpenAIChatCompletionController = async (req, res) => {
throw new Error('Failed to create agent run');
}
- // Process the stream
const config = {
runName: 'AgentRun',
configurable: {
@@ -493,6 +502,7 @@ const OpenAIChatCompletionController = async (req, res) => {
},
...(userMCPAuthMap != null && { userMCPAuthMap }),
},
+ recursionLimit: resolveRecursionLimit(agentsEConfig, agent),
signal: abortController.signal,
streamMode: 'values',
version: 'v2',
@@ -511,9 +521,9 @@ const OpenAIChatCompletionController = async (req, res) => {
const transactionsConfig = getTransactionsConfig(appConfig);
recordCollectedUsage(
{
- spendTokens,
- spendStructuredTokens,
- pricing: { getMultiplier, getCacheMultiplier },
+ spendTokens: db.spendTokens,
+ spendStructuredTokens: db.spendStructuredTokens,
+ pricing: { getMultiplier: db.getMultiplier, getCacheMultiplier: db.getCacheMultiplier },
bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance },
},
{
@@ -627,7 +637,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 +680,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..009c5b262c 100644
--- a/api/server/controllers/agents/recordCollectedUsage.spec.js
+++ b/api/server/controllers/agents/recordCollectedUsage.spec.js
@@ -18,17 +18,11 @@ 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),
-}));
-
-jest.mock('~/models/tx', () => ({
getMultiplier: mockGetMultiplier,
getCacheMultiplier: mockGetCacheMultiplier,
-}));
-
-jest.mock('~/models', () => ({
updateBalance: mockUpdateBalance,
bulkInsertTransactions: mockBulkInsertTransactions,
}));
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..7abddf5e2f 100644
--- a/api/server/controllers/agents/responses.js
+++ b/api/server/controllers/agents/responses.js
@@ -32,14 +32,13 @@ 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');
-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 +213,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,
@@ -277,9 +280,10 @@ 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 getAgent({ id: agentId });
+ const agent = await db.getAgent({ id: agentId });
if (!agent) {
return sendResponsesErrorResponse(
res,
@@ -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');
}
}
@@ -355,7 +359,7 @@ const createResponse = async (req, res) => {
isInitialAgent: true,
},
{
- getConvoFiles,
+ getConvoFiles: db.getConvoFiles,
getFiles: db.getFiles,
getUserKey: db.getUserKey,
getMessages: db.getMessages,
@@ -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: {
@@ -525,9 +536,9 @@ const createResponse = async (req, res) => {
const transactionsConfig = getTransactionsConfig(req.config);
recordCollectedUsage(
{
- spendTokens,
- spendStructuredTokens,
- pricing: { getMultiplier, getCacheMultiplier },
+ spendTokens: db.spendTokens,
+ spendStructuredTokens: db.spendStructuredTokens,
+ pricing: { getMultiplier: db.getMultiplier, getCacheMultiplier: db.getCacheMultiplier },
bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance },
},
{
@@ -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: {
@@ -680,9 +698,9 @@ const createResponse = async (req, res) => {
const transactionsConfig = getTransactionsConfig(req.config);
recordCollectedUsage(
{
- spendTokens,
- spendStructuredTokens,
- pricing: { getMultiplier, getCacheMultiplier },
+ spendTokens: db.spendTokens,
+ spendStructuredTokens: db.spendStructuredTokens,
+ pricing: { getMultiplier: db.getMultiplier, getCacheMultiplier: db.getCacheMultiplier },
bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance },
},
{
@@ -782,7 +800,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 +850,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 309873e56c..e365b232e4 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,
@@ -25,15 +26,6 @@ const {
actionDelimiter,
removeNullishValues,
} = require('librechat-data-provider');
-const {
- getListAgentsByAccess,
- countPromotedAgents,
- revertAgentVersion,
- createAgent,
- updateAgent,
- deleteAgent,
- getAgent,
-} = require('~/models/Agent');
const {
findPubliclyAccessibleResources,
getResourcePermissionsMap,
@@ -42,15 +34,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 { resolveConfigServers } = require('~/server/services/MCP');
const { getMCPServersRegistry } = require('~/config');
const { getLogStores } = require('~/cache');
+const db = require('~/models');
const systemTools = {
[Tools.execute_code]: true,
@@ -76,9 +67,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 [];
@@ -113,9 +102,16 @@ const validateEdgeAgentAccess = async (edges, userId, userRole) => {
* @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
+ * @param {Record} [params.configServers] - Config-source MCP servers resolved from appConfig overrides
* @returns {Promise} Only the authorized subset of tools
*/
-const filterAuthorizedTools = async ({ tools, userId, availableTools, existingTools }) => {
+const filterAuthorizedTools = async ({
+ tools,
+ userId,
+ availableTools,
+ existingTools,
+ configServers,
+}) => {
const filteredTools = [];
let mcpServerConfigs;
let registryUnavailable = false;
@@ -133,7 +129,8 @@ const filterAuthorizedTools = async ({ tools, userId, availableTools, existingTo
if (mcpServerConfigs === undefined) {
try {
- mcpServerConfigs = (await getMCPServersRegistry().getAllServerConfigs(userId)) ?? {};
+ mcpServerConfigs =
+ (await getMCPServersRegistry().getAllServerConfigs(userId, configServers)) ?? {};
} catch (e) {
logger.warn(
'[filterAuthorizedTools] MCP registry unavailable, filtering all MCP tools',
@@ -204,10 +201,19 @@ const createAgentHandler = async (req, res) => {
agentData.author = userId;
agentData.tools = [];
- const availableTools = (await getCachedTools()) ?? {};
- agentData.tools = await filterAuthorizedTools({ tools, userId, availableTools });
+ const hasMCPTools = tools.some((t) => t?.includes(Constants.mcp_delimiter));
+ const [availableTools, configServers] = await Promise.all([
+ getCachedTools().then((t) => t ?? {}),
+ hasMCPTools ? resolveConfigServers(req) : Promise.resolve(undefined),
+ ]);
+ agentData.tools = await filterAuthorizedTools({
+ tools,
+ userId,
+ availableTools,
+ configServers,
+ });
- const agent = await createAgent(agentData);
+ const agent = await db.createAgent(agentData);
try {
await Promise.all([
@@ -267,7 +273,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' });
@@ -288,9 +294,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 +317,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
@@ -372,7 +372,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' });
@@ -394,11 +394,15 @@ const updateAgentHandler = async (req, res) => {
);
if (newMCPTools.length > 0) {
- const availableTools = (await getCachedTools()) ?? {};
+ const [availableTools, configServers] = await Promise.all([
+ getCachedTools().then((t) => t ?? {}),
+ resolveConfigServers(req),
+ ]);
const approvedNew = await filterAuthorizedTools({
tools: newMCPTools,
userId: req.user.id,
availableTools,
+ configServers,
});
const rejectedSet = new Set(newMCPTools.filter((t) => !approvedNew.includes(t)));
if (rejectedSet.size > 0) {
@@ -409,7 +413,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;
@@ -459,7 +463,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',
@@ -507,7 +511,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 = [];
/**
@@ -526,7 +530,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,
@@ -551,16 +555,20 @@ const duplicateAgentHandler = async (req, res) => {
newAgentData.actions = agentActions;
if (newAgentData.tools?.length) {
- const availableTools = (await getCachedTools()) ?? {};
+ const [availableTools, configServers] = await Promise.all([
+ getCachedTools().then((t) => t ?? {}),
+ resolveConfigServers(req),
+ ]);
newAgentData.tools = await filterAuthorizedTools({
tools: newAgentData.tools,
userId,
availableTools,
existingTools: newAgentData.tools,
+ configServers,
});
}
- const newAgent = await createAgent(newAgentData);
+ const newAgent = await db.createAgent(newAgentData);
try {
await Promise.all([
@@ -613,11 +621,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);
@@ -692,7 +700,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,
@@ -702,7 +710,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);
@@ -714,7 +722,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,
@@ -779,7 +787,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' });
@@ -811,7 +819,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);
}
@@ -824,7 +832,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,
});
@@ -880,7 +888,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' });
@@ -888,18 +896,22 @@ 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()) ?? {};
+ const [availableTools, configServers] = await Promise.all([
+ getCachedTools().then((t) => t ?? {}),
+ resolveConfigServers(req),
+ ]);
const filteredTools = await filterAuthorizedTools({
tools: updatedAgent.tools,
userId: req.user.id,
availableTools,
existingTools: updatedAgent.tools,
+ configServers,
});
if (filteredTools.length !== updatedAgent.tools.length) {
- updatedAgent = await updateAgent(
+ updatedAgent = await db.updateAgent(
{ id },
{ tools: filteredTools },
{ updatingUserId: req.user.id },
@@ -929,8 +941,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 ede4ea416a..455cea2e7c 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(),
}));
@@ -26,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(),
}));
@@ -34,15 +39,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([]),
@@ -51,9 +47,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 = {
@@ -77,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}
@@ -176,7 +181,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 +199,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 +449,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 +470,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 +482,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,26 +541,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();
- // Note: updateAgentProjects requires more setup, so we just verify the handler doesn't crash
- });
-
test('should validate tool_resources in updates', async () => {
mockReq.user.id = existingAgentAuthorId.toString();
mockReq.params.id = existingAgentId;
diff --git a/api/server/controllers/assistants/chatV1.js b/api/server/controllers/assistants/chatV1.js
index 804594d0bf..631831e617 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,15 @@ 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,
+ upsertBalanceFields,
+ getTransactions,
+ getMultiplier,
+ getConvo,
+} = require('~/models');
+const { logViolation, getLogStores } = require('~/cache');
const { getOpenAIClient } = require('./helpers');
/**
@@ -275,16 +286,26 @@ 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,
+ balanceConfig,
+ upsertBalanceFields,
+ },
+ );
};
const { openai: _openai } = await getOpenAIClient({
diff --git a/api/server/controllers/assistants/chatV2.js b/api/server/controllers/assistants/chatV2.js
index 414681d6dc..237af1b11a 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,15 @@ 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,
+ upsertBalanceFields,
+ createAutoRefillTransaction,
+} = require('~/models');
+const { logViolation, getLogStores } = require('~/cache');
const { getOpenAIClient } = require('./helpers');
/**
@@ -148,16 +159,26 @@ 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,
+ balanceConfig,
+ upsertBalanceFields,
+ },
+ );
};
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/helpers.js b/api/server/controllers/assistants/helpers.js
index 9183680f1e..4630bfe7ef 100644
--- a/api/server/controllers/assistants/helpers.js
+++ b/api/server/controllers/assistants/helpers.js
@@ -1,13 +1,14 @@
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 { hasCapability } = require('~/server/middleware/roles/capabilities');
const { getEndpointsConfig } = require('~/server/services/Config');
/**
@@ -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/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/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');
+ });
+ });
});
diff --git a/api/server/controllers/auth/oauth.js b/api/server/controllers/auth/oauth.js
index 80c2ced002..917e9e2bef 100644
--- a/api/server/controllers/auth/oauth.js
+++ b/api/server/controllers/auth/oauth.js
@@ -47,9 +47,15 @@ function createOAuthHandler(redirectUri = domains.client) {
const refreshToken =
req.user.tokenset?.refresh_token || req.user.federatedTokens?.refresh_token;
- const exchangeCode = await generateAdminExchangeCode(cache, req.user, token, refreshToken);
-
const callbackUrl = new URL(redirectUri);
+ const exchangeCode = await generateAdminExchangeCode(
+ cache,
+ req.user,
+ token,
+ refreshToken,
+ callbackUrl.origin,
+ req.pkceChallenge,
+ );
callbackUrl.searchParams.set('code', exchangeCode);
logger.info(`[OAuth] Admin panel redirect with exchange code for user: ${req.user.email}`);
return res.redirect(callbackUrl.toString());
diff --git a/api/server/controllers/mcp.js b/api/server/controllers/mcp.js
index 729f01da9d..e31bb93bc6 100644
--- a/api/server/controllers/mcp.js
+++ b/api/server/controllers/mcp.js
@@ -14,6 +14,7 @@ const {
isMCPInspectionFailedError,
} = require('@librechat/api');
const { Constants, MCPServerUserInputSchema } = require('librechat-data-provider');
+const { resolveConfigServers, resolveAllMcpConfigs } = require('~/server/services/MCP');
const { cacheMCPServerTools, getMCPServerTools } = require('~/server/services/Config');
const { getMCPManager, getMCPServersRegistry } = require('~/config');
@@ -57,7 +58,7 @@ function handleMCPError(error, res) {
}
/**
- * Get all MCP tools available to the user
+ * Get all MCP tools available to the user.
*/
const getMCPTools = async (req, res) => {
try {
@@ -67,10 +68,10 @@ const getMCPTools = async (req, res) => {
return res.status(401).json({ message: 'Unauthorized' });
}
- const mcpConfig = await getMCPServersRegistry().getAllServerConfigs(userId);
- const configuredServers = mcpConfig ? Object.keys(mcpConfig) : [];
+ const mcpConfig = await resolveAllMcpConfigs(userId, req.user);
+ const configuredServers = Object.keys(mcpConfig);
- if (!mcpConfig || Object.keys(mcpConfig).length == 0) {
+ if (!configuredServers.length) {
return res.status(200).json({ servers: {} });
}
@@ -115,14 +116,11 @@ const getMCPTools = async (req, res) => {
try {
const serverTools = serverToolsMap.get(serverName);
- // Get server config once
const serverConfig = mcpConfig[serverName];
- const rawServerConfig = await getMCPServersRegistry().getServerConfig(serverName, userId);
- // Initialize server object with all server-level data
const server = {
name: serverName,
- icon: rawServerConfig?.iconPath || '',
+ icon: serverConfig?.iconPath || '',
authenticated: true,
authConfig: [],
tools: [],
@@ -183,7 +181,7 @@ const getMCPServersList = async (req, res) => {
return res.status(401).json({ message: 'Unauthorized' });
}
- const serverConfigs = await getMCPServersRegistry().getAllServerConfigs(userId);
+ const serverConfigs = await resolveAllMcpConfigs(userId, req.user);
return res.json(redactAllServerSecrets(serverConfigs));
} catch (error) {
logger.error('[getMCPServersList]', error);
@@ -237,7 +235,12 @@ const getMCPServerById = async (req, res) => {
if (!serverName) {
return res.status(400).json({ message: 'Server name is required' });
}
- const parsedConfig = await getMCPServersRegistry().getServerConfig(serverName, userId);
+ const configServers = await resolveConfigServers(req);
+ const parsedConfig = await getMCPServersRegistry().getServerConfig(
+ serverName,
+ userId,
+ configServers,
+ );
if (!parsedConfig) {
return res.status(404).json({ message: 'MCP server not found' });
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..ff023b4504 100644
--- a/api/server/experimental.js
+++ b/api/server/experimental.js
@@ -19,19 +19,21 @@ const {
performStartupChecks,
handleJsonParseError,
initializeFileStorage,
+ preAuthTenantMiddleware,
} = require('@librechat/api');
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 optionalJwtAuth = require('./middleware/optionalJwtAuth');
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 +224,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');
@@ -312,7 +314,7 @@ if (cluster.isMaster) {
app.use('/api/endpoints', routes.endpoints);
app.use('/api/balance', routes.balance);
app.use('/api/models', routes.models);
- app.use('/api/config', routes.config);
+ app.use('/api/config', preAuthTenantMiddleware, optionalJwtAuth, routes.config);
app.use('/api/assistants', routes.assistants);
app.use('/api/files', await routes.files.initialize());
app.use('/images/', createValidateImageRequest(appConfig.secureImageLinks), routes.staticRoute);
diff --git a/api/server/index.js b/api/server/index.js
index f034f10236..d26a203c0a 100644
--- a/api/server/index.js
+++ b/api/server/index.js
@@ -8,8 +8,8 @@ const express = require('express');
const passport = require('passport');
const compression = require('compression');
const cookieParser = require('cookie-parser');
-const { logger } = require('@librechat/data-schemas');
const mongoSanitize = require('express-mongo-sanitize');
+const { logger, runAsSystem } = require('@librechat/data-schemas');
const {
isEnabled,
apiNotFound,
@@ -20,19 +20,22 @@ const {
GenerationJobManager,
createStreamServices,
initializeFileStorage,
+ updateInterfacePermissions,
+ preAuthTenantMiddleware,
} = 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 } = require('~/models/interface');
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 optionalJwtAuth = require('./middleware/optionalJwtAuth');
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 ?? {};
@@ -58,11 +61,20 @@ const startServer = async () => {
app.disable('x-powered-by');
app.set('trust proxy', trusted_proxy);
- await seedDatabase();
- const appConfig = await getAppConfig();
+ if (isEnabled(process.env.TENANT_ISOLATION_STRICT)) {
+ logger.warn(
+ '[Security] TENANT_ISOLATION_STRICT is active. Ensure your reverse proxy strips or sets ' +
+ 'the X-Tenant-Id header — untrusted clients must not be able to set it directly.',
+ );
+ }
+
+ await runAsSystem(seedDatabase);
+ const appConfig = await getAppConfig({ baseOnly: true });
initializeFileStorage(appConfig);
- await performStartupChecks(appConfig);
- await updateInterfacePermissions(appConfig);
+ await runAsSystem(async () => {
+ await performStartupChecks(appConfig);
+ await updateInterfacePermissions({ appConfig, getRoleByName, updateAccessPermissions });
+ });
const indexPath = path.join(appConfig.paths.dist, 'index.html');
let indexHTML = fs.readFileSync(indexPath, 'utf8');
@@ -133,10 +145,20 @@ const startServer = async () => {
await configureSocialLogins(app);
}
- app.use('/oauth', routes.oauth);
+ /* Per-request capability cache — must be registered before any route that calls hasCapability */
+ app.use(capabilityContextMiddleware);
+
+ /* Pre-auth tenant context for unauthenticated routes that need tenant scoping.
+ * The reverse proxy / auth gateway sets `X-Tenant-Id` header for multi-tenant deployments. */
+ app.use('/oauth', preAuthTenantMiddleware, routes.oauth);
/* API Endpoints */
- app.use('/api/auth', routes.auth);
+ app.use('/api/auth', preAuthTenantMiddleware, routes.auth);
app.use('/api/admin', routes.adminAuth);
+ app.use('/api/admin/config', routes.adminConfig);
+ app.use('/api/admin/grants', routes.adminGrants);
+ app.use('/api/admin/groups', routes.adminGroups);
+ app.use('/api/admin/roles', routes.adminRoles);
+ app.use('/api/admin/users', routes.adminUsers);
app.use('/api/actions', routes.actions);
app.use('/api/keys', routes.keys);
app.use('/api/api-keys', routes.apiKeys);
@@ -150,11 +172,11 @@ const startServer = async () => {
app.use('/api/endpoints', routes.endpoints);
app.use('/api/balance', routes.balance);
app.use('/api/models', routes.models);
- app.use('/api/config', routes.config);
+ app.use('/api/config', preAuthTenantMiddleware, optionalJwtAuth, routes.config);
app.use('/api/assistants', routes.assistants);
app.use('/api/files', await routes.files.initialize());
app.use('/images/', createValidateImageRequest(appConfig.secureImageLinks), routes.staticRoute);
- app.use('/api/share', routes.share);
+ app.use('/api/share', preAuthTenantMiddleware, routes.share);
app.use('/api/roles', routes.roles);
app.use('/api/agents', routes.agents);
app.use('/api/banner', routes.banner);
@@ -200,8 +222,10 @@ const startServer = async () => {
logger.info(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`);
}
- await initializeMCPs();
- await initializeOAuthReconnectManager();
+ await runAsSystem(async () => {
+ await initializeMCPs();
+ await initializeOAuthReconnectManager();
+ });
await checkMigrations();
// Configure stream services (auto-detects Redis from USE_REDIS env var)
diff --git a/api/server/middleware/__tests__/requireJwtAuth.spec.js b/api/server/middleware/__tests__/requireJwtAuth.spec.js
new file mode 100644
index 0000000000..bc288e5dab
--- /dev/null
+++ b/api/server/middleware/__tests__/requireJwtAuth.spec.js
@@ -0,0 +1,116 @@
+/**
+ * Integration test: verifies that requireJwtAuth chains tenantContextMiddleware
+ * after successful passport authentication, so ALS tenant context is set for
+ * all downstream middleware and route handlers.
+ *
+ * requireJwtAuth must chain tenantContextMiddleware after passport populates
+ * req.user (not at global app.use() scope where req.user is undefined).
+ * If the chaining is removed, these tests fail.
+ */
+
+const { getTenantId } = require('@librechat/data-schemas');
+
+// ── Mocks ──────────────────────────────────────────────────────────────
+
+let mockPassportError = null;
+
+jest.mock('passport', () => ({
+ authenticate: jest.fn(() => {
+ return (req, _res, done) => {
+ if (mockPassportError) {
+ return done(mockPassportError);
+ }
+ if (req._mockUser) {
+ req.user = req._mockUser;
+ }
+ done();
+ };
+ }),
+}));
+
+// Mock @librechat/api — the real tenantContextMiddleware is TS and cannot be
+// required directly from CJS tests. This thin wrapper mirrors the real logic
+// (read req.user.tenantId, call tenantStorage.run) using the same data-schemas
+// primitives. The real implementation is covered by packages/api tenant.spec.ts.
+jest.mock('@librechat/api', () => {
+ const { tenantStorage } = require('@librechat/data-schemas');
+ return {
+ isEnabled: jest.fn(() => false),
+ tenantContextMiddleware: (req, res, next) => {
+ const tenantId = req.user?.tenantId;
+ if (!tenantId) {
+ return next();
+ }
+ return tenantStorage.run({ tenantId }, async () => next());
+ },
+ };
+});
+
+// ── Helpers ─────────────────────────────────────────────────────────────
+
+const requireJwtAuth = require('../requireJwtAuth');
+
+function mockReq(user) {
+ return { headers: {}, _mockUser: user };
+}
+
+function mockRes() {
+ return { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis() };
+}
+
+/** Runs requireJwtAuth and returns the tenantId observed inside next(). */
+function runAuth(user) {
+ return new Promise((resolve) => {
+ const req = mockReq(user);
+ const res = mockRes();
+ requireJwtAuth(req, res, () => {
+ resolve(getTenantId());
+ });
+ });
+}
+
+// ── Tests ──────────────────────────────────────────────────────────────
+
+describe('requireJwtAuth tenant context chaining', () => {
+ afterEach(() => {
+ mockPassportError = null;
+ });
+
+ it('forwards passport errors to next() without entering tenant middleware', async () => {
+ mockPassportError = new Error('JWT signature invalid');
+ const req = mockReq(undefined);
+ const res = mockRes();
+ const err = await new Promise((resolve) => {
+ requireJwtAuth(req, res, (e) => resolve(e));
+ });
+ expect(err).toBeInstanceOf(Error);
+ expect(err.message).toBe('JWT signature invalid');
+ expect(getTenantId()).toBeUndefined();
+ });
+
+ it('sets ALS tenant context after passport auth succeeds', async () => {
+ const tenantId = await runAuth({ tenantId: 'tenant-abc', role: 'user' });
+ expect(tenantId).toBe('tenant-abc');
+ });
+
+ it('ALS tenant context is NOT set when user has no tenantId', async () => {
+ const tenantId = await runAuth({ role: 'user' });
+ expect(tenantId).toBeUndefined();
+ });
+
+ it('ALS tenant context is NOT set when user is undefined', async () => {
+ const tenantId = await runAuth(undefined);
+ expect(tenantId).toBeUndefined();
+ });
+
+ it('concurrent requests get isolated tenant contexts', async () => {
+ const results = await Promise.all(
+ ['tenant-1', 'tenant-2', 'tenant-3'].map((tid) => runAuth({ tenantId: tid, role: 'user' })),
+ );
+ expect(results).toEqual(['tenant-1', 'tenant-2', 'tenant-3']);
+ });
+
+ it('ALS context is not set at top-level scope (outside any request)', () => {
+ expect(getTenantId()).toBeUndefined();
+ });
+});
diff --git a/api/server/middleware/__tests__/validateModel.spec.js b/api/server/middleware/__tests__/validateModel.spec.js
new file mode 100644
index 0000000000..634baeed11
--- /dev/null
+++ b/api/server/middleware/__tests__/validateModel.spec.js
@@ -0,0 +1,178 @@
+const { ViolationTypes } = require('librechat-data-provider');
+
+jest.mock('@librechat/api', () => ({
+ handleError: jest.fn(),
+}));
+
+jest.mock('~/server/controllers/ModelController', () => ({
+ getModelsConfig: jest.fn(),
+}));
+
+jest.mock('~/server/services/Config', () => ({
+ getEndpointsConfig: jest.fn(),
+}));
+
+jest.mock('~/cache', () => ({
+ logViolation: jest.fn(),
+}));
+
+const { handleError } = require('@librechat/api');
+const { getModelsConfig } = require('~/server/controllers/ModelController');
+const { getEndpointsConfig } = require('~/server/services/Config');
+const { logViolation } = require('~/cache');
+const validateModel = require('../validateModel');
+
+describe('validateModel', () => {
+ let req, res, next;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ req = { body: { model: 'gpt-4o', endpoint: 'openAI' } };
+ res = {};
+ next = jest.fn();
+ getEndpointsConfig.mockResolvedValue({
+ openAI: { userProvide: false },
+ });
+ getModelsConfig.mockResolvedValue({
+ openAI: ['gpt-4o', 'gpt-4o-mini'],
+ });
+ });
+
+ describe('format validation', () => {
+ it('rejects missing model', async () => {
+ req.body.model = undefined;
+ await validateModel(req, res, next);
+ expect(handleError).toHaveBeenCalledWith(res, { text: 'Model not provided' });
+ expect(next).not.toHaveBeenCalled();
+ });
+
+ it('rejects non-string model', async () => {
+ req.body.model = 12345;
+ await validateModel(req, res, next);
+ expect(handleError).toHaveBeenCalledWith(res, { text: 'Model not provided' });
+ expect(next).not.toHaveBeenCalled();
+ });
+
+ it('rejects model exceeding 256 chars', async () => {
+ req.body.model = 'a'.repeat(257);
+ await validateModel(req, res, next);
+ expect(handleError).toHaveBeenCalledWith(res, { text: 'Invalid model identifier' });
+ });
+
+ it('rejects model with leading special character', async () => {
+ req.body.model = '.bad-model';
+ await validateModel(req, res, next);
+ expect(handleError).toHaveBeenCalledWith(res, { text: 'Invalid model identifier' });
+ });
+
+ it('rejects model with script injection', async () => {
+ req.body.model = '';
+ await validateModel(req, res, next);
+ expect(handleError).toHaveBeenCalledWith(res, { text: 'Invalid model identifier' });
+ });
+
+ it('trims whitespace before validation', async () => {
+ req.body.model = ' gpt-4o ';
+ getModelsConfig.mockResolvedValue({ openAI: ['gpt-4o'] });
+ await validateModel(req, res, next);
+ expect(next).toHaveBeenCalled();
+ expect(handleError).not.toHaveBeenCalled();
+ });
+
+ it('rejects model with spaces in the middle', async () => {
+ req.body.model = 'gpt 4o';
+ await validateModel(req, res, next);
+ expect(handleError).toHaveBeenCalledWith(res, { text: 'Invalid model identifier' });
+ });
+
+ it('accepts standard model IDs', async () => {
+ const validModels = [
+ 'gpt-4o',
+ 'claude-3-5-sonnet-20241022',
+ 'us.amazon.nova-pro-v1:0',
+ 'qwen/qwen3.6-plus-preview:free',
+ 'Meta-Llama-3-8B-Instruct-4bit',
+ ];
+ for (const model of validModels) {
+ jest.clearAllMocks();
+ req.body.model = model;
+ getEndpointsConfig.mockResolvedValue({ openAI: { userProvide: false } });
+ getModelsConfig.mockResolvedValue({ openAI: [model] });
+ next.mockClear();
+
+ await validateModel(req, res, next);
+ expect(next).toHaveBeenCalled();
+ expect(handleError).not.toHaveBeenCalled();
+ }
+ });
+ });
+
+ describe('userProvide early-return', () => {
+ it('calls next() immediately for userProvide endpoints without checking model list', async () => {
+ getEndpointsConfig.mockResolvedValue({
+ openAI: { userProvide: true },
+ });
+ req.body.model = 'any-model-from-user-key';
+
+ await validateModel(req, res, next);
+
+ expect(next).toHaveBeenCalled();
+ expect(getModelsConfig).not.toHaveBeenCalled();
+ });
+
+ it('does not call getModelsConfig for userProvide endpoints', async () => {
+ getEndpointsConfig.mockResolvedValue({
+ CustomEndpoint: { userProvide: true },
+ });
+ req.body = { model: 'custom-model', endpoint: 'CustomEndpoint' };
+
+ await validateModel(req, res, next);
+
+ expect(getModelsConfig).not.toHaveBeenCalled();
+ expect(next).toHaveBeenCalled();
+ });
+ });
+
+ describe('system endpoint list validation', () => {
+ it('rejects a model not in the available list', async () => {
+ req.body.model = 'not-in-list';
+
+ await validateModel(req, res, next);
+
+ expect(logViolation).toHaveBeenCalledWith(
+ req,
+ res,
+ ViolationTypes.ILLEGAL_MODEL_REQUEST,
+ expect.any(Object),
+ expect.anything(),
+ );
+ expect(handleError).toHaveBeenCalledWith(res, { text: 'Illegal model request' });
+ expect(next).not.toHaveBeenCalled();
+ });
+
+ it('accepts a model in the available list', async () => {
+ req.body.model = 'gpt-4o';
+
+ await validateModel(req, res, next);
+
+ expect(next).toHaveBeenCalled();
+ expect(handleError).not.toHaveBeenCalled();
+ });
+
+ it('rejects when endpoint has no models loaded', async () => {
+ getModelsConfig.mockResolvedValue({ openAI: undefined });
+
+ await validateModel(req, res, next);
+
+ expect(handleError).toHaveBeenCalledWith(res, { text: 'Endpoint models not loaded' });
+ });
+
+ it('rejects when modelsConfig is null', async () => {
+ getModelsConfig.mockResolvedValue(null);
+
+ await validateModel(req, res, next);
+
+ expect(handleError).toHaveBeenCalledWith(res, { text: 'Models not loaded' });
+ });
+ });
+});
diff --git a/api/server/middleware/abortMiddleware.js b/api/server/middleware/abortMiddleware.js
index d39b0104a8..e0c5ae0ff0 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,
@@ -7,14 +8,11 @@ const {
recordCollectedUsage,
sanitizeMessageForTransmit,
} = 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');
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,20 +121,24 @@ 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(
- req,
+ await db.saveMessage(
+ {
+ userId: req?.user?.id,
+ isTemporary: req?.body?.isTemporary,
+ interfaceConfig: req?.config?.interfaceConfig,
+ },
{ ...responseMessage, user: userId },
{ context: 'api/server/middleware/abortMiddleware.js' },
);
// 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 795814a928..a4ce85674b 100644
--- a/api/server/middleware/abortMiddleware.spec.js
+++ b/api/server/middleware/abortMiddleware.spec.js
@@ -20,16 +20,6 @@ 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: {
debug: jest.fn(),
@@ -73,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/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/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/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/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/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/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/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..024d6abbe3 100644
--- a/api/server/middleware/assistants/validateAuthor.js
+++ b/api/server/middleware/assistants/validateAuthor.js
@@ -1,5 +1,6 @@
-const { SystemRoles } = require('librechat-data-provider');
-const { getAssistant } = require('~/models/Assistant');
+const { logger, SystemCapabilities } = require('@librechat/data-schemas');
+const { hasCapability } = require('~/server/middleware/roles/capabilities');
+const { getAssistant } = require('~/models');
/**
* Checks if the assistant is supported or excluded
@@ -12,10 +13,6 @@ const { getAssistant } = require('~/models/Assistant');
* @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/checkBan.js b/api/server/middleware/checkBan.js
index 79804a84e1..5d1b60297f 100644
--- a/api/server/middleware/checkBan.js
+++ b/api/server/middleware/checkBan.js
@@ -1,16 +1,23 @@
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 });
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.
*
@@ -64,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);
}
@@ -94,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/server/middleware/checkDomainAllowed.js b/api/server/middleware/checkDomainAllowed.js
index 754eb9c127..f7a3f00e68 100644
--- a/api/server/middleware/checkDomainAllowed.js
+++ b/api/server/middleware/checkDomainAllowed.js
@@ -18,6 +18,7 @@ const checkDomainAllowed = async (req, res, next) => {
const email = req?.user?.email;
const appConfig = await getAppConfig({
role: req?.user?.role,
+ tenantId: req?.user?.tenantId,
});
if (email && !isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
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/config/app.js b/api/server/middleware/config/app.js
index bca3c8f71d..fb5f89b229 100644
--- a/api/server/middleware/config/app.js
+++ b/api/server/middleware/config/app.js
@@ -4,7 +4,9 @@ const { getAppConfig } = require('~/server/services/Config');
const configMiddleware = async (req, res, next) => {
try {
const userRole = req.user?.role;
- req.config = await getAppConfig({ role: userRole });
+ const userId = req.user?.id;
+ const tenantId = req.user?.tenantId;
+ req.config = await getAppConfig({ role: userRole, userId, tenantId });
next();
} catch (error) {
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/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/index.js b/api/server/middleware/limiters/index.js
index ab110443dc..a38188d2a6 100644
--- a/api/server/middleware/limiters/index.js
+++ b/api/server/middleware/limiters/index.js
@@ -8,6 +8,7 @@ const forkLimiters = require('./forkLimiters');
const registerLimiter = require('./registerLimiter');
const toolCallLimiter = require('./toolCallLimiter');
const messageLimiters = require('./messageLimiters');
+const promptUsageLimiter = require('./promptUsageLimiter');
const verifyEmailLimiter = require('./verifyEmailLimiter');
const resetPasswordLimiter = require('./resetPasswordLimiter');
@@ -16,6 +17,7 @@ module.exports = {
...importLimiters,
...messageLimiters,
...forkLimiters,
+ ...promptUsageLimiter,
loginLimiter,
registerLimiter,
toolCallLimiter,
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/promptUsageLimiter.js b/api/server/middleware/limiters/promptUsageLimiter.js
new file mode 100644
index 0000000000..38bdeed636
--- /dev/null
+++ b/api/server/middleware/limiters/promptUsageLimiter.js
@@ -0,0 +1,17 @@
+const rateLimit = require('express-rate-limit');
+const { limiterCache } = require('@librechat/api');
+
+const PROMPT_USAGE_WINDOW_MS = 60 * 1000; // 1 minute
+const PROMPT_USAGE_MAX = 30; // 30 usage increments per user per minute
+
+const promptUsageLimiter = rateLimit({
+ windowMs: PROMPT_USAGE_WINDOW_MS,
+ max: PROMPT_USAGE_MAX,
+ handler: (_req, res) => {
+ res.status(429).json({ message: 'Too many prompt usage requests. Try again later' });
+ },
+ keyGenerator: (req) => req.user?.id,
+ store: limiterCache('prompt_usage_limiter'),
+});
+
+module.exports = { promptUsageLimiter };
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/middleware/optionalJwtAuth.js b/api/server/middleware/optionalJwtAuth.js
index 2f59fdda4a..d46478d36e 100644
--- a/api/server/middleware/optionalJwtAuth.js
+++ b/api/server/middleware/optionalJwtAuth.js
@@ -1,9 +1,10 @@
const cookies = require('cookie');
const passport = require('passport');
-const { isEnabled } = require('@librechat/api');
+const { isEnabled, tenantContextMiddleware } = require('@librechat/api');
// This middleware does not require authentication,
-// but if the user is authenticated, it will set the user object.
+// but if the user is authenticated, it will set the user object
+// and establish tenant ALS context.
const optionalJwtAuth = (req, res, next) => {
const cookieHeader = req.headers.cookie;
const tokenProvider = cookieHeader ? cookies.parse(cookieHeader).token_provider : null;
@@ -13,6 +14,7 @@ const optionalJwtAuth = (req, res, next) => {
}
if (user) {
req.user = user;
+ return tenantContextMiddleware(req, res, next);
}
next();
};
diff --git a/api/server/middleware/requireJwtAuth.js b/api/server/middleware/requireJwtAuth.js
index 16b107aefc..b13e991b23 100644
--- a/api/server/middleware/requireJwtAuth.js
+++ b/api/server/middleware/requireJwtAuth.js
@@ -1,20 +1,29 @@
const cookies = require('cookie');
const passport = require('passport');
-const { isEnabled } = require('@librechat/api');
+const { isEnabled, tenantContextMiddleware } = require('@librechat/api');
/**
- * Custom Middleware to handle JWT authentication, with support for OpenID token reuse
- * Switches between JWT and OpenID authentication based on cookies and environment settings
+ * Custom Middleware to handle JWT authentication, with support for OpenID token reuse.
+ * Switches between JWT and OpenID authentication based on cookies and environment settings.
+ *
+ * After successful authentication (req.user populated), automatically chains into
+ * `tenantContextMiddleware` to propagate `req.user.tenantId` into AsyncLocalStorage
+ * for downstream Mongoose tenant isolation.
*/
const requireJwtAuth = (req, res, next) => {
const cookieHeader = req.headers.cookie;
const tokenProvider = cookieHeader ? cookies.parse(cookieHeader).token_provider : null;
- if (tokenProvider === 'openid' && isEnabled(process.env.OPENID_REUSE_TOKENS)) {
- return passport.authenticate('openidJwt', { session: false })(req, res, next);
- }
+ const strategy =
+ tokenProvider === 'openid' && isEnabled(process.env.OPENID_REUSE_TOKENS) ? 'openidJwt' : 'jwt';
- return passport.authenticate('jwt', { session: false })(req, res, next);
+ passport.authenticate(strategy, { session: false })(req, res, (err) => {
+ if (err) {
+ return next(err);
+ }
+ // req.user is now populated by passport — set up tenant ALS context
+ tenantContextMiddleware(req, res, next);
+ });
};
module.exports = requireJwtAuth;
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/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..f97d4b72b4 100644
--- a/api/server/middleware/roles/index.js
+++ b/api/server/middleware/roles/index.js
@@ -1,3 +1,15 @@
+/**
+ * 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 = {
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/middleware/validateModel.js b/api/server/middleware/validateModel.js
index 40f6e67bfb..71a931f0d1 100644
--- a/api/server/middleware/validateModel.js
+++ b/api/server/middleware/validateModel.js
@@ -1,7 +1,12 @@
const { handleError } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { getModelsConfig } = require('~/server/controllers/ModelController');
+const { getEndpointsConfig } = require('~/server/services/Config');
const { logViolation } = require('~/cache');
+
+const MAX_MODEL_STRING_LENGTH = 256;
+const MODEL_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_.:/@+-]*$/;
+
/**
* Validates the model of the request.
*
@@ -11,11 +16,27 @@ const { logViolation } = require('~/cache');
* @param {Function} next - The Express next function.
*/
const validateModel = async (req, res, next) => {
- const { model, endpoint } = req.body;
- if (!model) {
+ const { endpoint } = req.body;
+ const rawModel = req.body.model;
+
+ if (!rawModel || typeof rawModel !== 'string') {
return handleError(res, { text: 'Model not provided' });
}
+ const model = rawModel.trim();
+ if (!model || model.length > MAX_MODEL_STRING_LENGTH || !MODEL_PATTERN.test(model)) {
+ return handleError(res, { text: 'Invalid model identifier' });
+ }
+
+ req.body.model = model;
+
+ const endpointsConfig = await getEndpointsConfig(req);
+ const endpointConfig = endpointsConfig?.[endpoint];
+
+ if (endpointConfig?.userProvide) {
+ return next();
+ }
+
const modelsConfig = await getModelsConfig(req);
if (!modelsConfig) {
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__/config.spec.js b/api/server/routes/__tests__/config.spec.js
index 7d7d3ea13a..54315a7798 100644
--- a/api/server/routes/__tests__/config.spec.js
+++ b/api/server/routes/__tests__/config.spec.js
@@ -1,25 +1,73 @@
jest.mock('~/cache/getLogStores');
+
+const mockGetAppConfig = jest.fn();
+jest.mock('~/server/services/Config/app', () => ({
+ getAppConfig: (...args) => mockGetAppConfig(...args),
+}));
+
+jest.mock('~/server/services/Config/ldap', () => ({
+ getLdapConfig: jest.fn(() => null),
+}));
+
+const mockGetTenantId = jest.fn(() => undefined);
+jest.mock('@librechat/data-schemas', () => ({
+ ...jest.requireActual('@librechat/data-schemas'),
+ getTenantId: (...args) => mockGetTenantId(...args),
+}));
+
const request = require('supertest');
const express = require('express');
const configRoute = require('../config');
-// file deepcode ignore UseCsurfForExpress/test: test
-const app = express();
-app.disable('x-powered-by');
-app.use('/api/config', configRoute);
+
+function createApp(user) {
+ const app = express();
+ app.disable('x-powered-by');
+ if (user) {
+ app.use((req, _res, next) => {
+ req.user = user;
+ next();
+ });
+ }
+ app.use('/api/config', configRoute);
+ return app;
+}
+
+const baseAppConfig = {
+ registration: { socialLogins: ['google', 'github'] },
+ interfaceConfig: {
+ privacyPolicy: { externalUrl: 'https://example.com/privacy' },
+ termsOfService: { externalUrl: 'https://example.com/tos' },
+ modelSelect: true,
+ },
+ turnstileConfig: { siteKey: 'test-key' },
+ modelSpecs: { list: [{ name: 'test-spec' }] },
+ webSearch: { searchProvider: 'tavily' },
+};
+
+const mockUser = {
+ id: 'user123',
+ role: 'USER',
+ tenantId: undefined,
+};
afterEach(() => {
+ jest.resetAllMocks();
delete process.env.APP_TITLE;
+ delete process.env.CHECK_BALANCE;
+ delete process.env.START_BALANCE;
+ delete process.env.SANDPACK_BUNDLER_URL;
+ delete process.env.SANDPACK_STATIC_BUNDLER_URL;
+ delete process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES;
+ delete process.env.ALLOW_REGISTRATION;
+ delete process.env.ALLOW_SOCIAL_LOGIN;
+ delete process.env.ALLOW_PASSWORD_RESET;
+ delete process.env.DOMAIN_SERVER;
delete process.env.GOOGLE_CLIENT_ID;
delete process.env.GOOGLE_CLIENT_SECRET;
- delete process.env.FACEBOOK_CLIENT_ID;
- delete process.env.FACEBOOK_CLIENT_SECRET;
delete process.env.OPENID_CLIENT_ID;
delete process.env.OPENID_CLIENT_SECRET;
delete process.env.OPENID_ISSUER;
delete process.env.OPENID_SESSION_SECRET;
- delete process.env.OPENID_BUTTON_LABEL;
- delete process.env.OPENID_AUTO_REDIRECT;
- delete process.env.OPENID_AUTH_URL;
delete process.env.GITHUB_CLIENT_ID;
delete process.env.GITHUB_CLIENT_SECRET;
delete process.env.DISCORD_CLIENT_ID;
@@ -28,78 +76,215 @@ afterEach(() => {
delete process.env.SAML_ISSUER;
delete process.env.SAML_CERT;
delete process.env.SAML_SESSION_SECRET;
- delete process.env.SAML_BUTTON_LABEL;
- delete process.env.SAML_IMAGE_URL;
- delete process.env.DOMAIN_SERVER;
- delete process.env.ALLOW_REGISTRATION;
- delete process.env.ALLOW_SOCIAL_LOGIN;
- delete process.env.ALLOW_PASSWORD_RESET;
- delete process.env.LDAP_URL;
- delete process.env.LDAP_BIND_DN;
- delete process.env.LDAP_BIND_CREDENTIALS;
- delete process.env.LDAP_USER_SEARCH_BASE;
- delete process.env.LDAP_SEARCH_FILTER;
});
-//TODO: This works/passes locally but http request tests fail with 404 in CI. Need to figure out why.
+describe('GET /api/config', () => {
+ describe('unauthenticated (no req.user)', () => {
+ it('should call getAppConfig with baseOnly when no tenant context', async () => {
+ mockGetAppConfig.mockResolvedValue(baseAppConfig);
+ mockGetTenantId.mockReturnValue(undefined);
+ const app = createApp(null);
-describe.skip('GET /', () => {
- it('should return 200 and the correct body', async () => {
- process.env.APP_TITLE = 'Test Title';
- process.env.GOOGLE_CLIENT_ID = 'Test Google Client Id';
- process.env.GOOGLE_CLIENT_SECRET = 'Test Google Client Secret';
- process.env.FACEBOOK_CLIENT_ID = 'Test Facebook Client Id';
- process.env.FACEBOOK_CLIENT_SECRET = 'Test Facebook Client Secret';
- process.env.OPENID_CLIENT_ID = 'Test OpenID Id';
- process.env.OPENID_CLIENT_SECRET = 'Test OpenID Secret';
- process.env.OPENID_ISSUER = 'Test OpenID Issuer';
- process.env.OPENID_SESSION_SECRET = 'Test Secret';
- process.env.OPENID_BUTTON_LABEL = 'Test OpenID';
- process.env.OPENID_AUTH_URL = 'http://test-server.com';
- process.env.GITHUB_CLIENT_ID = 'Test Github client Id';
- process.env.GITHUB_CLIENT_SECRET = 'Test Github client Secret';
- process.env.DISCORD_CLIENT_ID = 'Test Discord client Id';
- process.env.DISCORD_CLIENT_SECRET = 'Test Discord client Secret';
- process.env.SAML_ENTRY_POINT = 'http://test-server.com';
- process.env.SAML_ISSUER = 'Test SAML Issuer';
- process.env.SAML_CERT = 'saml.pem';
- process.env.SAML_SESSION_SECRET = 'Test Secret';
- process.env.SAML_BUTTON_LABEL = 'Test SAML';
- process.env.SAML_IMAGE_URL = 'http://test-server.com';
- process.env.DOMAIN_SERVER = 'http://test-server.com';
- process.env.ALLOW_REGISTRATION = 'true';
- process.env.ALLOW_SOCIAL_LOGIN = 'true';
- process.env.ALLOW_PASSWORD_RESET = 'true';
- process.env.LDAP_URL = 'Test LDAP URL';
- process.env.LDAP_BIND_DN = 'Test LDAP Bind DN';
- process.env.LDAP_BIND_CREDENTIALS = 'Test LDAP Bind Credentials';
- process.env.LDAP_USER_SEARCH_BASE = 'Test LDAP User Search Base';
- process.env.LDAP_SEARCH_FILTER = 'Test LDAP Search Filter';
+ await request(app).get('/api/config');
- const response = await request(app).get('/');
+ expect(mockGetAppConfig).toHaveBeenCalledWith({ baseOnly: true });
+ });
- expect(response.statusCode).toBe(200);
- expect(response.body).toEqual({
- appTitle: 'Test Title',
- socialLogins: ['google', 'facebook', 'openid', 'github', 'discord', 'saml'],
- discordLoginEnabled: true,
- facebookLoginEnabled: true,
- githubLoginEnabled: true,
- googleLoginEnabled: true,
- openidLoginEnabled: true,
- openidLabel: 'Test OpenID',
- openidImageUrl: 'http://test-server.com',
- samlLoginEnabled: true,
- samlLabel: 'Test SAML',
- samlImageUrl: 'http://test-server.com',
- ldap: {
- enabled: true,
- },
- serverDomain: 'http://test-server.com',
- emailLoginEnabled: 'true',
- registrationEnabled: 'true',
- passwordResetEnabled: 'true',
- socialLoginEnabled: 'true',
+ it('should call getAppConfig with tenantId when tenant context is present', async () => {
+ mockGetAppConfig.mockResolvedValue(baseAppConfig);
+ mockGetTenantId.mockReturnValue('tenant-abc');
+ const app = createApp(null);
+
+ await request(app).get('/api/config');
+
+ expect(mockGetAppConfig).toHaveBeenCalledWith({ tenantId: 'tenant-abc' });
+ });
+
+ it('should map tenant-scoped config fields in unauthenticated response', async () => {
+ const tenantConfig = {
+ ...baseAppConfig,
+ registration: { socialLogins: ['saml'] },
+ turnstileConfig: { siteKey: 'tenant-key' },
+ };
+ mockGetAppConfig.mockResolvedValue(tenantConfig);
+ mockGetTenantId.mockReturnValue('tenant-abc');
+ const app = createApp(null);
+
+ const response = await request(app).get('/api/config');
+
+ expect(response.statusCode).toBe(200);
+ expect(response.body.socialLogins).toEqual(['saml']);
+ expect(response.body.turnstile).toEqual({ siteKey: 'tenant-key' });
+ expect(response.body).not.toHaveProperty('modelSpecs');
+ });
+
+ it('should return minimal payload without authenticated-only fields', async () => {
+ mockGetAppConfig.mockResolvedValue(baseAppConfig);
+ const app = createApp(null);
+
+ const response = await request(app).get('/api/config');
+
+ expect(response.statusCode).toBe(200);
+ expect(response.body).not.toHaveProperty('modelSpecs');
+ expect(response.body).not.toHaveProperty('balance');
+ expect(response.body).not.toHaveProperty('webSearch');
+ expect(response.body).not.toHaveProperty('bundlerURL');
+ expect(response.body).not.toHaveProperty('staticBundlerURL');
+ expect(response.body).not.toHaveProperty('sharePointFilePickerEnabled');
+ expect(response.body).not.toHaveProperty('conversationImportMaxFileSize');
+ });
+
+ it('should include socialLogins and turnstile from base config', async () => {
+ mockGetAppConfig.mockResolvedValue(baseAppConfig);
+ const app = createApp(null);
+
+ const response = await request(app).get('/api/config');
+
+ expect(response.body.socialLogins).toEqual(['google', 'github']);
+ expect(response.body.turnstile).toEqual({ siteKey: 'test-key' });
+ });
+
+ it('should include only privacyPolicy and termsOfService from interface config', async () => {
+ mockGetAppConfig.mockResolvedValue(baseAppConfig);
+ const app = createApp(null);
+
+ const response = await request(app).get('/api/config');
+
+ expect(response.body.interface).toEqual({
+ privacyPolicy: { externalUrl: 'https://example.com/privacy' },
+ termsOfService: { externalUrl: 'https://example.com/tos' },
+ });
+ expect(response.body.interface).not.toHaveProperty('modelSelect');
+ });
+
+ it('should not include interface if no privacyPolicy or termsOfService', async () => {
+ mockGetAppConfig.mockResolvedValue({
+ ...baseAppConfig,
+ interfaceConfig: { modelSelect: true },
+ });
+ const app = createApp(null);
+
+ const response = await request(app).get('/api/config');
+
+ expect(response.body).not.toHaveProperty('interface');
+ });
+
+ it('should include shared env var fields', async () => {
+ mockGetAppConfig.mockResolvedValue(baseAppConfig);
+ process.env.APP_TITLE = 'Test App';
+ const app = createApp(null);
+
+ const response = await request(app).get('/api/config');
+
+ expect(response.body.appTitle).toBe('Test App');
+ expect(response.body).toHaveProperty('emailLoginEnabled');
+ expect(response.body).toHaveProperty('serverDomain');
+ });
+
+ it('should return 500 when getAppConfig throws', async () => {
+ mockGetAppConfig.mockRejectedValue(new Error('Config service failure'));
+ const app = createApp(null);
+
+ const response = await request(app).get('/api/config');
+
+ expect(response.statusCode).toBe(500);
+ expect(response.body).toHaveProperty('error');
+ });
+ });
+
+ describe('authenticated (req.user exists)', () => {
+ it('should call getAppConfig with role, userId, and tenantId', async () => {
+ mockGetAppConfig.mockResolvedValue(baseAppConfig);
+ mockGetTenantId.mockReturnValue('fallback-tenant');
+ const app = createApp(mockUser);
+
+ await request(app).get('/api/config');
+
+ expect(mockGetAppConfig).toHaveBeenCalledWith({
+ role: 'USER',
+ userId: 'user123',
+ tenantId: 'fallback-tenant',
+ });
+ });
+
+ it('should prefer user tenantId over getTenantId fallback', async () => {
+ mockGetAppConfig.mockResolvedValue(baseAppConfig);
+ mockGetTenantId.mockReturnValue('fallback-tenant');
+ const app = createApp({ ...mockUser, tenantId: 'user-tenant' });
+
+ await request(app).get('/api/config');
+
+ expect(mockGetAppConfig).toHaveBeenCalledWith({
+ role: 'USER',
+ userId: 'user123',
+ tenantId: 'user-tenant',
+ });
+ });
+
+ it('should include modelSpecs, balance, and webSearch', async () => {
+ mockGetAppConfig.mockResolvedValue(baseAppConfig);
+ process.env.CHECK_BALANCE = 'true';
+ process.env.START_BALANCE = '10000';
+ const app = createApp(mockUser);
+
+ const response = await request(app).get('/api/config');
+
+ expect(response.body.modelSpecs).toEqual({ list: [{ name: 'test-spec' }] });
+ expect(response.body.balance).toEqual({ enabled: true, startBalance: 10000 });
+ expect(response.body.webSearch).toEqual({ searchProvider: 'tavily' });
+ });
+
+ it('should include full interface config', async () => {
+ mockGetAppConfig.mockResolvedValue(baseAppConfig);
+ const app = createApp(mockUser);
+
+ const response = await request(app).get('/api/config');
+
+ expect(response.body.interface).toEqual(baseAppConfig.interfaceConfig);
+ });
+
+ it('should include authenticated-only env var fields', async () => {
+ mockGetAppConfig.mockResolvedValue(baseAppConfig);
+ process.env.SANDPACK_BUNDLER_URL = 'https://bundler.test';
+ process.env.SANDPACK_STATIC_BUNDLER_URL = 'https://static-bundler.test';
+ process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '5000000';
+ const app = createApp(mockUser);
+
+ const response = await request(app).get('/api/config');
+
+ expect(response.body.bundlerURL).toBe('https://bundler.test');
+ expect(response.body.staticBundlerURL).toBe('https://static-bundler.test');
+ expect(response.body.conversationImportMaxFileSize).toBe(5000000);
+ });
+
+ it('should merge per-user balance override into config', async () => {
+ mockGetAppConfig.mockResolvedValue({
+ ...baseAppConfig,
+ balance: {
+ enabled: true,
+ startBalance: 50000,
+ },
+ });
+ const app = createApp(mockUser);
+
+ const response = await request(app).get('/api/config');
+
+ expect(response.body.balance).toEqual(
+ expect.objectContaining({
+ enabled: true,
+ startBalance: 50000,
+ }),
+ );
+ });
+
+ it('should return 500 when getAppConfig throws', async () => {
+ mockGetAppConfig.mockRejectedValue(new Error('Config service failure'));
+ const app = createApp(mockUser);
+
+ const response = await request(app).get('/api/config');
+
+ expect(response.statusCode).toBe(500);
+ expect(response.body).toHaveProperty('error');
});
});
});
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__/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/__tests__/grants.spec.js b/api/server/routes/__tests__/grants.spec.js
new file mode 100644
index 0000000000..c7b5b6bdda
--- /dev/null
+++ b/api/server/routes/__tests__/grants.spec.js
@@ -0,0 +1,185 @@
+const express = require('express');
+const request = require('supertest');
+const mongoose = require('mongoose');
+const { MongoMemoryServer } = require('mongodb-memory-server');
+const { createModels, createMethods } = require('@librechat/data-schemas');
+const { PrincipalType, SystemRoles } = require('librechat-data-provider');
+
+/**
+ * Integration test for the admin grants routes.
+ *
+ * Validates the full Express wiring: route registration → middleware →
+ * handler → real MongoDB. Auth middleware is injected (matching the repo
+ * pattern in keys.spec.js) so we can control the caller identity without
+ * a real JWT, while the handler DI deps use real DB methods.
+ */
+
+jest.mock('~/server/middleware', () => ({
+ requireJwtAuth: (_req, _res, next) => next(),
+}));
+
+jest.mock('~/server/middleware/roles/capabilities', () => ({
+ requireCapability: () => (_req, _res, next) => next(),
+}));
+
+let mongoServer;
+let db;
+
+beforeAll(async () => {
+ mongoServer = await MongoMemoryServer.create();
+ await mongoose.connect(mongoServer.getUri());
+ createModels(mongoose);
+ db = createMethods(mongoose);
+ await db.seedSystemGrants();
+ await db.initializeRoles();
+ await db.seedDefaultRoles();
+});
+
+afterAll(async () => {
+ await mongoose.disconnect();
+ await mongoServer.stop();
+});
+
+afterEach(async () => {
+ const SystemGrant = mongoose.models.SystemGrant;
+ // Clean non-seed grants (keep admin seed)
+ await SystemGrant.deleteMany({
+ $or: [
+ { principalId: { $ne: SystemRoles.ADMIN } },
+ { principalType: { $ne: PrincipalType.ROLE } },
+ ],
+ });
+});
+
+function createApp(user) {
+ const { createAdminGrantsHandlers, getCachedPrincipals } = require('@librechat/api');
+
+ const handlers = createAdminGrantsHandlers({
+ listGrants: db.listGrants,
+ countGrants: db.countGrants,
+ getCapabilitiesForPrincipal: db.getCapabilitiesForPrincipal,
+ getCapabilitiesForPrincipals: db.getCapabilitiesForPrincipals,
+ grantCapability: db.grantCapability,
+ revokeCapability: db.revokeCapability,
+ getUserPrincipals: db.getUserPrincipals,
+ hasCapabilityForPrincipals: db.hasCapabilityForPrincipals,
+ getHeldCapabilities: db.getHeldCapabilities,
+ getCachedPrincipals,
+ checkRoleExists: async (name) => (await db.getRoleByName(name)) != null,
+ });
+
+ const app = express();
+ app.use(express.json());
+ app.use((req, _res, next) => {
+ req.user = user;
+ next();
+ });
+
+ const router = express.Router();
+ router.get('/', handlers.listGrants);
+ router.get('/effective', handlers.getEffectiveCapabilities);
+ router.get('/:principalType/:principalId', handlers.getPrincipalGrants);
+ router.post('/', handlers.assignGrant);
+ router.delete('/:principalType/:principalId/:capability', handlers.revokeGrant);
+ app.use('/api/admin/grants', router);
+
+ return app;
+}
+
+describe('Admin Grants Routes — Integration', () => {
+ const adminUserId = new mongoose.Types.ObjectId();
+ const adminUser = {
+ _id: adminUserId,
+ id: adminUserId.toString(),
+ role: SystemRoles.ADMIN,
+ };
+
+ it('GET / returns seeded admin grants', async () => {
+ const app = createApp(adminUser);
+ const res = await request(app).get('/api/admin/grants').expect(200);
+
+ expect(res.body).toHaveProperty('grants');
+ expect(res.body).toHaveProperty('total');
+ expect(res.body.grants.length).toBeGreaterThan(0);
+ // Seeded grants are for the ADMIN role
+ expect(res.body.grants[0].principalType).toBe(PrincipalType.ROLE);
+ });
+
+ it('GET /effective returns capabilities for admin', async () => {
+ const app = createApp(adminUser);
+ const res = await request(app).get('/api/admin/grants/effective').expect(200);
+
+ expect(res.body).toHaveProperty('capabilities');
+ expect(res.body.capabilities).toContain('access:admin');
+ expect(res.body.capabilities).toContain('manage:roles');
+ });
+
+ it('POST / assigns a grant and DELETE / revokes it', async () => {
+ const app = createApp(adminUser);
+
+ // Assign
+ const assignRes = await request(app)
+ .post('/api/admin/grants')
+ .send({
+ principalType: PrincipalType.ROLE,
+ principalId: SystemRoles.USER,
+ capability: 'read:users',
+ })
+ .expect(201);
+
+ expect(assignRes.body.grant).toMatchObject({
+ principalType: PrincipalType.ROLE,
+ principalId: SystemRoles.USER,
+ capability: 'read:users',
+ });
+
+ // Verify via GET
+ const getRes = await request(app)
+ .get(`/api/admin/grants/${PrincipalType.ROLE}/${SystemRoles.USER}`)
+ .expect(200);
+
+ expect(getRes.body.grants.some((g) => g.capability === 'read:users')).toBe(true);
+
+ // Revoke
+ await request(app)
+ .delete(`/api/admin/grants/${PrincipalType.ROLE}/${SystemRoles.USER}/read:users`)
+ .expect(200);
+
+ // Verify revoked
+ const afterRes = await request(app)
+ .get(`/api/admin/grants/${PrincipalType.ROLE}/${SystemRoles.USER}`)
+ .expect(200);
+
+ expect(afterRes.body.grants.some((g) => g.capability === 'read:users')).toBe(false);
+ });
+
+ it('POST / returns 400 for non-existent role when checkRoleExists is wired', async () => {
+ const app = createApp(adminUser);
+
+ const res = await request(app)
+ .post('/api/admin/grants')
+ .send({
+ principalType: PrincipalType.ROLE,
+ principalId: 'nonexistent-role',
+ capability: 'read:users',
+ })
+ .expect(400);
+
+ expect(res.body.error).toBe('Role not found');
+ });
+
+ it('POST / returns 401 without authenticated user', async () => {
+ const app = createApp(undefined);
+
+ const res = await request(app)
+ .post('/api/admin/grants')
+ .send({
+ principalType: PrincipalType.ROLE,
+ principalId: SystemRoles.USER,
+ capability: 'read:users',
+ })
+ .expect(401);
+
+ expect(res.body).toHaveProperty('error', 'Authentication required');
+ });
+});
diff --git a/api/server/routes/__tests__/mcp.spec.js b/api/server/routes/__tests__/mcp.spec.js
index 1ad8cac087..f194f361d3 100644
--- a/api/server/routes/__tests__/mcp.spec.js
+++ b/api/server/routes/__tests__/mcp.spec.js
@@ -18,6 +18,7 @@ const mockRegistryInstance = {
getServerConfig: jest.fn(),
getOAuthServers: jest.fn(),
getAllServerConfigs: jest.fn(),
+ ensureConfigServers: jest.fn().mockResolvedValue({}),
addServer: jest.fn(),
updateServer: jest.fn(),
removeServer: jest.fn(),
@@ -58,6 +59,7 @@ jest.mock('@librechat/api', () => {
});
jest.mock('@librechat/data-schemas', () => ({
+ getTenantId: jest.fn(),
logger: {
debug: jest.fn(),
info: jest.fn(),
@@ -93,14 +95,18 @@ jest.mock('~/server/services/Config', () => ({
getCachedTools: jest.fn(),
getMCPServerTools: jest.fn(),
loadCustomConfig: jest.fn(),
+ getAppConfig: jest.fn().mockResolvedValue({ mcpConfig: {} }),
}));
jest.mock('~/server/services/Config/mcp', () => ({
updateMCPServerTools: jest.fn(),
}));
+const mockResolveAllMcpConfigs = jest.fn().mockResolvedValue({});
jest.mock('~/server/services/MCP', () => ({
getMCPSetupData: jest.fn(),
+ resolveConfigServers: jest.fn().mockResolvedValue({}),
+ resolveAllMcpConfigs: (...args) => mockResolveAllMcpConfigs(...args),
getServerConnectionStatus: jest.fn(),
}));
@@ -579,6 +585,112 @@ describe('MCP Routes', () => {
);
});
+ it('should use oauthHeaders from flow state when present', async () => {
+ const mockFlowManager = {
+ getFlowState: jest.fn().mockResolvedValue({ status: 'PENDING' }),
+ completeFlow: jest.fn().mockResolvedValue(),
+ deleteFlow: jest.fn().mockResolvedValue(true),
+ };
+ const mockFlowState = {
+ serverName: 'test-server',
+ userId: 'test-user-id',
+ metadata: { toolFlowId: 'tool-flow-123' },
+ clientInfo: {},
+ codeVerifier: 'test-verifier',
+ oauthHeaders: { 'X-Custom-Auth': 'header-value' },
+ };
+ const mockTokens = { access_token: 'tok', refresh_token: 'ref' };
+
+ MCPOAuthHandler.getFlowState.mockResolvedValue(mockFlowState);
+ MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens);
+ MCPTokenStorage.storeTokens.mockResolvedValue();
+ getLogStores.mockReturnValue({});
+ require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
+ require('~/config').getOAuthReconnectionManager.mockReturnValue({
+ clearReconnection: jest.fn(),
+ });
+ require('~/config').getMCPManager.mockReturnValue({
+ getUserConnection: jest.fn().mockResolvedValue({
+ fetchTools: jest.fn().mockResolvedValue([]),
+ }),
+ });
+ const { getCachedTools, setCachedTools } = require('~/server/services/Config');
+ getCachedTools.mockResolvedValue({});
+ setCachedTools.mockResolvedValue();
+
+ const flowId = 'test-user-id:test-server';
+ const csrfToken = generateTestCsrfToken(flowId);
+
+ await request(app)
+ .get('/api/mcp/test-server/oauth/callback')
+ .set('Cookie', [`oauth_csrf=${csrfToken}`])
+ .query({ code: 'auth-code', state: flowId });
+
+ expect(MCPOAuthHandler.completeOAuthFlow).toHaveBeenCalledWith(
+ flowId,
+ 'auth-code',
+ mockFlowManager,
+ { 'X-Custom-Auth': 'header-value' },
+ );
+ expect(mockRegistryInstance.getServerConfig).not.toHaveBeenCalled();
+ });
+
+ it('should fall back to registry oauth_headers when flow state lacks them', async () => {
+ const mockFlowManager = {
+ getFlowState: jest.fn().mockResolvedValue({ status: 'PENDING' }),
+ completeFlow: jest.fn().mockResolvedValue(),
+ deleteFlow: jest.fn().mockResolvedValue(true),
+ };
+ const mockFlowState = {
+ serverName: 'test-server',
+ userId: 'test-user-id',
+ metadata: { toolFlowId: 'tool-flow-123' },
+ clientInfo: {},
+ codeVerifier: 'test-verifier',
+ };
+ const mockTokens = { access_token: 'tok', refresh_token: 'ref' };
+
+ MCPOAuthHandler.getFlowState.mockResolvedValue(mockFlowState);
+ MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens);
+ MCPTokenStorage.storeTokens.mockResolvedValue();
+ mockRegistryInstance.getServerConfig.mockResolvedValue({
+ oauth_headers: { 'X-Registry-Header': 'from-registry' },
+ });
+ getLogStores.mockReturnValue({});
+ require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
+ require('~/config').getOAuthReconnectionManager.mockReturnValue({
+ clearReconnection: jest.fn(),
+ });
+ require('~/config').getMCPManager.mockReturnValue({
+ getUserConnection: jest.fn().mockResolvedValue({
+ fetchTools: jest.fn().mockResolvedValue([]),
+ }),
+ });
+ const { getCachedTools, setCachedTools } = require('~/server/services/Config');
+ getCachedTools.mockResolvedValue({});
+ setCachedTools.mockResolvedValue();
+
+ const flowId = 'test-user-id:test-server';
+ const csrfToken = generateTestCsrfToken(flowId);
+
+ await request(app)
+ .get('/api/mcp/test-server/oauth/callback')
+ .set('Cookie', [`oauth_csrf=${csrfToken}`])
+ .query({ code: 'auth-code', state: flowId });
+
+ expect(MCPOAuthHandler.completeOAuthFlow).toHaveBeenCalledWith(
+ flowId,
+ 'auth-code',
+ mockFlowManager,
+ { 'X-Registry-Header': 'from-registry' },
+ );
+ expect(mockRegistryInstance.getServerConfig).toHaveBeenCalledWith(
+ 'test-server',
+ 'test-user-id',
+ undefined,
+ );
+ });
+
it('should redirect to error page when callback processing fails', async () => {
MCPOAuthHandler.getFlowState.mockRejectedValue(new Error('Callback error'));
const flowId = 'test-user-id:test-server';
@@ -1350,19 +1462,10 @@ describe('MCP Routes', () => {
},
});
- expect(getMCPSetupData).toHaveBeenCalledWith('test-user-id');
+ expect(getMCPSetupData).toHaveBeenCalledWith('test-user-id', expect.any(Object));
expect(getServerConnectionStatus).toHaveBeenCalledTimes(2);
});
- it('should return 404 when MCP config is not found', async () => {
- getMCPSetupData.mockRejectedValue(new Error('MCP config not found'));
-
- const response = await request(app).get('/api/mcp/connection/status');
-
- expect(response.status).toBe(404);
- expect(response.body).toEqual({ error: 'MCP config not found' });
- });
-
it('should return 500 when connection status check fails', async () => {
getMCPSetupData.mockRejectedValue(new Error('Database error'));
@@ -1437,15 +1540,6 @@ describe('MCP Routes', () => {
});
});
- it('should return 404 when MCP config is not found', async () => {
- getMCPSetupData.mockRejectedValue(new Error('MCP config not found'));
-
- const response = await request(app).get('/api/mcp/connection/status/test-server');
-
- expect(response.status).toBe(404);
- expect(response.body).toEqual({ error: 'MCP config not found' });
- });
-
it('should return 500 when connection status check fails', async () => {
getMCPSetupData.mockRejectedValue(new Error('Database connection failed'));
@@ -1704,7 +1798,7 @@ describe('MCP Routes', () => {
},
};
- mockRegistryInstance.getAllServerConfigs.mockResolvedValue(mockServerConfigs);
+ mockResolveAllMcpConfigs.mockResolvedValue(mockServerConfigs);
const response = await request(app).get('/api/mcp/servers');
@@ -1721,11 +1815,14 @@ describe('MCP Routes', () => {
});
expect(response.body['server-1'].headers).toBeUndefined();
expect(response.body['server-2'].headers).toBeUndefined();
- expect(mockRegistryInstance.getAllServerConfigs).toHaveBeenCalledWith('test-user-id');
+ expect(mockResolveAllMcpConfigs).toHaveBeenCalledWith(
+ 'test-user-id',
+ expect.objectContaining({ id: 'test-user-id' }),
+ );
});
it('should return empty object when no servers are configured', async () => {
- mockRegistryInstance.getAllServerConfigs.mockResolvedValue({});
+ mockResolveAllMcpConfigs.mockResolvedValue({});
const response = await request(app).get('/api/mcp/servers');
@@ -1749,7 +1846,7 @@ describe('MCP Routes', () => {
});
it('should return 500 when server config retrieval fails', async () => {
- mockRegistryInstance.getAllServerConfigs.mockRejectedValue(new Error('Database error'));
+ mockResolveAllMcpConfigs.mockRejectedValue(new Error('Database error'));
const response = await request(app).get('/api/mcp/servers');
@@ -1939,11 +2036,12 @@ describe('MCP Routes', () => {
expect(mockRegistryInstance.getServerConfig).toHaveBeenCalledWith(
'test-server',
'test-user-id',
+ {},
);
});
it('should return 404 when server not found', async () => {
- mockRegistryInstance.getServerConfig.mockResolvedValue(null);
+ mockRegistryInstance.getServerConfig.mockResolvedValue(undefined);
const response = await request(app).get('/api/mcp/servers/non-existent-server');
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(),
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..72f23b7d52 100644
--- a/api/server/routes/admin/auth.js
+++ b/api/server/routes/admin/auth.js
@@ -1,41 +1,62 @@
const express = require('express');
const passport = require('passport');
-const { randomState } = require('openid-client');
-const { logger } = require('@librechat/data-schemas');
+const crypto = require('node:crypto');
const { CacheKeys } = require('librechat-data-provider');
-const {
- requireAdmin,
- getAdminPanelUrl,
- exchangeAdminCode,
- createSetBalanceConfig,
-} = require('@librechat/api');
+const { logger, 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 getLogStores = require('~/cache/getLogStores');
const { getOpenIdConfig } = require('~/strategies');
const middleware = require('~/server/middleware');
-const { Balance } = require('~/db/models');
+
+const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);
const setBalanceConfig = createSetBalanceConfig({
getAppConfig,
- Balance,
+ findBalanceByUser,
+ upsertBalanceFields,
});
const router = express.Router();
+function resolveRequestOrigin(req) {
+ const originHeader = req.get('origin');
+ if (originHeader) {
+ try {
+ return new URL(originHeader).origin;
+ } catch {
+ return undefined;
+ }
+ }
+
+ const refererHeader = req.get('referer');
+ if (!refererHeader) {
+ return undefined;
+ }
+
+ try {
+ return new URL(refererHeader).origin;
+ } catch {
+ return undefined;
+ }
+}
+
router.post(
'/login/local',
middleware.logHeaders,
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 });
@@ -52,28 +73,340 @@ router.get('/oauth/openid/check', (req, res) => {
res.status(200).json({ message: 'OpenID check successful' });
});
-router.get('/oauth/openid', (req, res, next) => {
+/** PKCE challenge cache TTL: 5 minutes (enough for user to authenticate with IdP) */
+const PKCE_CHALLENGE_TTL = 5 * 60 * 1000;
+/** Regex pattern for valid PKCE challenges: 64 hex characters (SHA-256 hex digest) */
+const PKCE_CHALLENGE_PATTERN = /^[a-f0-9]{64}$/;
+
+/**
+ * Generates a random hex state string for OAuth flows.
+ * @returns {string} A 32-byte random hex string.
+ */
+function generateState() {
+ return crypto.randomBytes(32).toString('hex');
+}
+
+/**
+ * Stores a PKCE challenge in cache keyed by state.
+ * @param {string} state - The OAuth state value.
+ * @param {string | undefined} codeChallenge - The PKCE code_challenge from query params.
+ * @param {string} provider - Provider name for logging.
+ * @returns {Promise} True if stored successfully or no challenge provided.
+ */
+async function storePkceChallenge(state, codeChallenge, provider) {
+ if (typeof codeChallenge !== 'string' || !PKCE_CHALLENGE_PATTERN.test(codeChallenge)) {
+ return true;
+ }
+ try {
+ const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
+ await cache.set(`pkce:${state}`, codeChallenge, PKCE_CHALLENGE_TTL);
+ return true;
+ } catch (err) {
+ logger.error(`[admin/oauth/${provider}] Failed to store PKCE challenge:`, err);
+ return false;
+ }
+}
+
+/**
+ * Middleware to retrieve PKCE challenge from cache using the OAuth state.
+ * Reads state from req.oauthState (set by a preceding middleware).
+ * @param {string} provider - Provider name for logging.
+ * @returns {Function} Express middleware.
+ */
+function retrievePkceChallenge(provider) {
+ return async (req, res, next) => {
+ if (!req.oauthState) {
+ return next();
+ }
+ try {
+ const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
+ const challenge = await cache.get(`pkce:${req.oauthState}`);
+ if (challenge) {
+ req.pkceChallenge = challenge;
+ await cache.delete(`pkce:${req.oauthState}`);
+ } else {
+ logger.warn(
+ `[admin/oauth/${provider}/callback] State present but no PKCE challenge found; PKCE will not be enforced for this request`,
+ );
+ }
+ } catch (err) {
+ logger.error(
+ `[admin/oauth/${provider}/callback] Failed to retrieve PKCE challenge, aborting:`,
+ err,
+ );
+ return res.redirect(
+ `${getAdminPanelUrl()}/auth/${provider}/callback?error=pkce_retrieval_failed&error_description=Failed+to+retrieve+PKCE+challenge`,
+ );
+ }
+ next();
+ };
+}
+
+/* ──────────────────────────────────────────────
+ * OpenID Admin Routes
+ * ────────────────────────────────────────────── */
+
+router.get('/oauth/openid', async (req, res, next) => {
+ const state = generateState();
+ const stored = await storePkceChallenge(state, req.query.code_challenge, 'openid');
+ if (!stored) {
+ return res.redirect(
+ `${getAdminPanelUrl()}/auth/openid/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
+ );
+ }
+
return passport.authenticate('openidAdmin', {
session: false,
- state: randomState(),
+ state,
})(req, res, next);
});
router.get(
'/oauth/openid/callback',
+ (req, res, next) => {
+ req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
+ next();
+ },
passport.authenticate('openidAdmin', {
failureRedirect: `${getAdminPanelUrl()}/auth/openid/callback?error=auth_failed&error_description=Authentication+failed`,
failureMessage: true,
session: false,
}),
- requireAdmin,
+ retrievePkceChallenge('openid'),
+ requireAdminAccess,
setBalanceConfig,
middleware.checkDomainAllowed,
createOAuthHandler(`${getAdminPanelUrl()}/auth/openid/callback`),
);
+/* ──────────────────────────────────────────────
+ * SAML Admin Routes
+ * ────────────────────────────────────────────── */
+
+router.get('/oauth/saml', async (req, res, next) => {
+ const state = generateState();
+ const stored = await storePkceChallenge(state, req.query.code_challenge, 'saml');
+ if (!stored) {
+ return res.redirect(
+ `${getAdminPanelUrl()}/auth/saml/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
+ );
+ }
+
+ return passport.authenticate('samlAdmin', {
+ session: false,
+ additionalParams: { RelayState: state },
+ })(req, res, next);
+});
+
+router.post(
+ '/oauth/saml/callback',
+ (req, res, next) => {
+ req.oauthState = typeof req.body.RelayState === 'string' ? req.body.RelayState : undefined;
+ next();
+ },
+ passport.authenticate('samlAdmin', {
+ failureRedirect: `${getAdminPanelUrl()}/auth/saml/callback?error=auth_failed&error_description=Authentication+failed`,
+ failureMessage: true,
+ session: false,
+ }),
+ retrievePkceChallenge('saml'),
+ requireAdminAccess,
+ setBalanceConfig,
+ middleware.checkDomainAllowed,
+ createOAuthHandler(`${getAdminPanelUrl()}/auth/saml/callback`),
+);
+
+/* ──────────────────────────────────────────────
+ * Google Admin Routes
+ * ────────────────────────────────────────────── */
+
+router.get('/oauth/google', async (req, res, next) => {
+ const state = generateState();
+ const stored = await storePkceChallenge(state, req.query.code_challenge, 'google');
+ if (!stored) {
+ return res.redirect(
+ `${getAdminPanelUrl()}/auth/google/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
+ );
+ }
+
+ return passport.authenticate('googleAdmin', {
+ scope: ['openid', 'profile', 'email'],
+ session: false,
+ state,
+ })(req, res, next);
+});
+
+router.get(
+ '/oauth/google/callback',
+ (req, res, next) => {
+ req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
+ next();
+ },
+ passport.authenticate('googleAdmin', {
+ failureRedirect: `${getAdminPanelUrl()}/auth/google/callback?error=auth_failed&error_description=Authentication+failed`,
+ failureMessage: true,
+ session: false,
+ }),
+ retrievePkceChallenge('google'),
+ requireAdminAccess,
+ setBalanceConfig,
+ middleware.checkDomainAllowed,
+ createOAuthHandler(`${getAdminPanelUrl()}/auth/google/callback`),
+);
+
+/* ──────────────────────────────────────────────
+ * GitHub Admin Routes
+ * ────────────────────────────────────────────── */
+
+router.get('/oauth/github', async (req, res, next) => {
+ const state = generateState();
+ const stored = await storePkceChallenge(state, req.query.code_challenge, 'github');
+ if (!stored) {
+ return res.redirect(
+ `${getAdminPanelUrl()}/auth/github/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
+ );
+ }
+
+ return passport.authenticate('githubAdmin', {
+ scope: ['user:email', 'read:user'],
+ session: false,
+ state,
+ })(req, res, next);
+});
+
+router.get(
+ '/oauth/github/callback',
+ (req, res, next) => {
+ req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
+ next();
+ },
+ passport.authenticate('githubAdmin', {
+ failureRedirect: `${getAdminPanelUrl()}/auth/github/callback?error=auth_failed&error_description=Authentication+failed`,
+ failureMessage: true,
+ session: false,
+ }),
+ retrievePkceChallenge('github'),
+ requireAdminAccess,
+ setBalanceConfig,
+ middleware.checkDomainAllowed,
+ createOAuthHandler(`${getAdminPanelUrl()}/auth/github/callback`),
+);
+
+/* ──────────────────────────────────────────────
+ * Discord Admin Routes
+ * ────────────────────────────────────────────── */
+
+router.get('/oauth/discord', async (req, res, next) => {
+ const state = generateState();
+ const stored = await storePkceChallenge(state, req.query.code_challenge, 'discord');
+ if (!stored) {
+ return res.redirect(
+ `${getAdminPanelUrl()}/auth/discord/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
+ );
+ }
+
+ return passport.authenticate('discordAdmin', {
+ scope: ['identify', 'email'],
+ session: false,
+ state,
+ })(req, res, next);
+});
+
+router.get(
+ '/oauth/discord/callback',
+ (req, res, next) => {
+ req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
+ next();
+ },
+ passport.authenticate('discordAdmin', {
+ failureRedirect: `${getAdminPanelUrl()}/auth/discord/callback?error=auth_failed&error_description=Authentication+failed`,
+ failureMessage: true,
+ session: false,
+ }),
+ retrievePkceChallenge('discord'),
+ requireAdminAccess,
+ setBalanceConfig,
+ middleware.checkDomainAllowed,
+ createOAuthHandler(`${getAdminPanelUrl()}/auth/discord/callback`),
+);
+
+/* ──────────────────────────────────────────────
+ * Facebook Admin Routes
+ * ────────────────────────────────────────────── */
+
+router.get('/oauth/facebook', async (req, res, next) => {
+ const state = generateState();
+ const stored = await storePkceChallenge(state, req.query.code_challenge, 'facebook');
+ if (!stored) {
+ return res.redirect(
+ `${getAdminPanelUrl()}/auth/facebook/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
+ );
+ }
+
+ return passport.authenticate('facebookAdmin', {
+ scope: ['public_profile'],
+ session: false,
+ state,
+ })(req, res, next);
+});
+
+router.get(
+ '/oauth/facebook/callback',
+ (req, res, next) => {
+ req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
+ next();
+ },
+ passport.authenticate('facebookAdmin', {
+ failureRedirect: `${getAdminPanelUrl()}/auth/facebook/callback?error=auth_failed&error_description=Authentication+failed`,
+ failureMessage: true,
+ session: false,
+ }),
+ retrievePkceChallenge('facebook'),
+ requireAdminAccess,
+ setBalanceConfig,
+ middleware.checkDomainAllowed,
+ createOAuthHandler(`${getAdminPanelUrl()}/auth/facebook/callback`),
+);
+
+/* ──────────────────────────────────────────────
+ * Apple Admin Routes (POST callback)
+ * ────────────────────────────────────────────── */
+
+router.get('/oauth/apple', async (req, res, next) => {
+ const state = generateState();
+ const stored = await storePkceChallenge(state, req.query.code_challenge, 'apple');
+ if (!stored) {
+ return res.redirect(
+ `${getAdminPanelUrl()}/auth/apple/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
+ );
+ }
+
+ return passport.authenticate('appleAdmin', {
+ session: false,
+ state,
+ })(req, res, next);
+});
+
+router.post(
+ '/oauth/apple/callback',
+ (req, res, next) => {
+ req.oauthState = typeof req.body.state === 'string' ? req.body.state : undefined;
+ next();
+ },
+ passport.authenticate('appleAdmin', {
+ failureRedirect: `${getAdminPanelUrl()}/auth/apple/callback?error=auth_failed&error_description=Authentication+failed`,
+ failureMessage: true,
+ session: false,
+ }),
+ retrievePkceChallenge('apple'),
+ requireAdminAccess,
+ setBalanceConfig,
+ middleware.checkDomainAllowed,
+ createOAuthHandler(`${getAdminPanelUrl()}/auth/apple/callback`),
+);
+
/** Regex pattern for valid exchange codes: 64 hex characters */
-const EXCHANGE_CODE_PATTERN = /^[a-f0-9]{64}$/i;
+const EXCHANGE_CODE_PATTERN = /^[a-f0-9]{64}$/;
/**
* Exchange OAuth authorization code for tokens.
@@ -81,12 +414,12 @@ const EXCHANGE_CODE_PATTERN = /^[a-f0-9]{64}$/i;
* The code is one-time-use and expires in 30 seconds.
*
* POST /api/admin/oauth/exchange
- * Body: { code: string }
+ * Body: { code: string, code_verifier?: string }
* Response: { token: string, refreshToken: string, user: object }
*/
router.post('/oauth/exchange', middleware.loginLimiter, async (req, res) => {
try {
- const { code } = req.body;
+ const { code, code_verifier: codeVerifier } = req.body;
if (!code) {
logger.warn('[admin/oauth/exchange] Missing authorization code');
@@ -104,8 +437,20 @@ router.post('/oauth/exchange', middleware.loginLimiter, async (req, res) => {
});
}
+ if (
+ codeVerifier !== undefined &&
+ (typeof codeVerifier !== 'string' || codeVerifier.length < 1 || codeVerifier.length > 512)
+ ) {
+ logger.warn('[admin/oauth/exchange] Invalid code_verifier format');
+ return res.status(400).json({
+ error: 'Invalid code_verifier',
+ error_code: 'INVALID_VERIFIER',
+ });
+ }
+
const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
- const result = await exchangeAdminCode(cache, code);
+ const requestOrigin = resolveRequestOrigin(req);
+ const result = await exchangeAdminCode(cache, code, requestOrigin, codeVerifier);
if (!result) {
return res.status(401).json({
diff --git a/api/server/routes/admin/config.js b/api/server/routes/admin/config.js
new file mode 100644
index 0000000000..0632077ea9
--- /dev/null
+++ b/api/server/routes/admin/config.js
@@ -0,0 +1,40 @@
+const express = require('express');
+const { createAdminConfigHandlers } = require('@librechat/api');
+const { SystemCapabilities } = require('@librechat/data-schemas');
+const {
+ hasConfigCapability,
+ requireCapability,
+} = require('~/server/middleware/roles/capabilities');
+const { getAppConfig, invalidateConfigCaches } = require('~/server/services/Config');
+const { requireJwtAuth } = require('~/server/middleware');
+const db = require('~/models');
+
+const router = express.Router();
+
+const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);
+
+const handlers = createAdminConfigHandlers({
+ listAllConfigs: db.listAllConfigs,
+ findConfigByPrincipal: db.findConfigByPrincipal,
+ upsertConfig: db.upsertConfig,
+ patchConfigFields: db.patchConfigFields,
+ unsetConfigField: db.unsetConfigField,
+ deleteConfig: db.deleteConfig,
+ toggleConfigActive: db.toggleConfigActive,
+ hasConfigCapability,
+ getAppConfig,
+ invalidateConfigCaches,
+});
+
+router.use(requireJwtAuth, requireAdminAccess);
+
+router.get('/', handlers.listConfigs);
+router.get('/base', handlers.getBaseConfig);
+router.get('/:principalType/:principalId', handlers.getConfig);
+router.put('/:principalType/:principalId', handlers.upsertConfigOverrides);
+router.patch('/:principalType/:principalId/fields', handlers.patchConfigField);
+router.delete('/:principalType/:principalId/fields', handlers.deleteConfigField);
+router.delete('/:principalType/:principalId', handlers.deleteConfigOverrides);
+router.patch('/:principalType/:principalId/active', handlers.toggleConfig);
+
+module.exports = router;
diff --git a/api/server/routes/admin/grants.js b/api/server/routes/admin/grants.js
new file mode 100644
index 0000000000..a0fa73dc43
--- /dev/null
+++ b/api/server/routes/admin/grants.js
@@ -0,0 +1,35 @@
+const express = require('express');
+const { createAdminGrantsHandlers, getCachedPrincipals } = require('@librechat/api');
+const { SystemCapabilities } = require('@librechat/data-schemas');
+const { requireCapability } = require('~/server/middleware/roles/capabilities');
+const { requireJwtAuth } = require('~/server/middleware');
+const db = require('~/models');
+
+const router = express.Router();
+
+const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);
+
+const handlers = createAdminGrantsHandlers({
+ listGrants: db.listGrants,
+ countGrants: db.countGrants,
+ getCapabilitiesForPrincipal: db.getCapabilitiesForPrincipal,
+ getCapabilitiesForPrincipals: db.getCapabilitiesForPrincipals,
+ grantCapability: db.grantCapability,
+ revokeCapability: db.revokeCapability,
+ getUserPrincipals: db.getUserPrincipals,
+ hasCapabilityForPrincipals: db.hasCapabilityForPrincipals,
+ getHeldCapabilities: db.getHeldCapabilities,
+ getCachedPrincipals,
+ checkRoleExists: async (name) => (await db.getRoleByName(name)) != null,
+});
+
+router.use(requireJwtAuth, requireAdminAccess);
+
+router.get('/', handlers.listGrants);
+router.get('/effective', handlers.getEffectiveCapabilities);
+router.get('/:principalType/:principalId', handlers.getPrincipalGrants);
+router.post('/', handlers.assignGrant);
+/** Callers should encodeURIComponent the capability for client compatibility (e.g. manage%3Aconfigs%3Aendpoints). */
+router.delete('/:principalType/:principalId/:capability', handlers.revokeGrant);
+
+module.exports = router;
diff --git a/api/server/routes/admin/groups.js b/api/server/routes/admin/groups.js
new file mode 100644
index 0000000000..11ed59737e
--- /dev/null
+++ b/api/server/routes/admin/groups.js
@@ -0,0 +1,40 @@
+const express = require('express');
+const { createAdminGroupsHandlers } = require('@librechat/api');
+const { SystemCapabilities } = require('@librechat/data-schemas');
+const { requireCapability } = require('~/server/middleware/roles/capabilities');
+const { requireJwtAuth } = require('~/server/middleware');
+const db = require('~/models');
+
+const router = express.Router();
+
+const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);
+const requireReadGroups = requireCapability(SystemCapabilities.READ_GROUPS);
+const requireManageGroups = requireCapability(SystemCapabilities.MANAGE_GROUPS);
+
+const handlers = createAdminGroupsHandlers({
+ listGroups: db.listGroups,
+ countGroups: db.countGroups,
+ findGroupById: db.findGroupById,
+ createGroup: db.createGroup,
+ updateGroupById: db.updateGroupById,
+ deleteGroup: db.deleteGroup,
+ addUserToGroup: db.addUserToGroup,
+ removeUserFromGroup: db.removeUserFromGroup,
+ removeMemberById: db.removeMemberById,
+ findUsers: db.findUsers,
+ deleteConfig: db.deleteConfig,
+ deleteAclEntries: db.deleteAclEntries,
+});
+
+router.use(requireJwtAuth, requireAdminAccess);
+
+router.get('/', requireReadGroups, handlers.listGroups);
+router.post('/', requireManageGroups, handlers.createGroup);
+router.get('/:id', requireReadGroups, handlers.getGroup);
+router.patch('/:id', requireManageGroups, handlers.updateGroup);
+router.delete('/:id', requireManageGroups, handlers.deleteGroup);
+router.get('/:id/members', requireReadGroups, handlers.getGroupMembers);
+router.post('/:id/members', requireManageGroups, handlers.addGroupMember);
+router.delete('/:id/members/:userId', requireManageGroups, handlers.removeGroupMember);
+
+module.exports = router;
diff --git a/api/server/routes/admin/roles.js b/api/server/routes/admin/roles.js
new file mode 100644
index 0000000000..f2bbd7f7ea
--- /dev/null
+++ b/api/server/routes/admin/roles.js
@@ -0,0 +1,46 @@
+const express = require('express');
+const { createAdminRolesHandlers } = require('@librechat/api');
+const { SystemCapabilities } = require('@librechat/data-schemas');
+const { requireCapability } = require('~/server/middleware/roles/capabilities');
+const { requireJwtAuth } = require('~/server/middleware');
+const db = require('~/models');
+
+const router = express.Router();
+
+const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);
+const requireReadRoles = requireCapability(SystemCapabilities.READ_ROLES);
+const requireManageRoles = requireCapability(SystemCapabilities.MANAGE_ROLES);
+
+const handlers = createAdminRolesHandlers({
+ listRoles: db.listRoles,
+ countRoles: db.countRoles,
+ getRoleByName: db.getRoleByName,
+ createRoleByName: db.createRoleByName,
+ updateRoleByName: db.updateRoleByName,
+ updateAccessPermissions: db.updateAccessPermissions,
+ deleteRoleByName: db.deleteRoleByName,
+ findUser: db.findUser,
+ updateUser: db.updateUser,
+ updateUsersByRole: db.updateUsersByRole,
+ findUserIdsByRole: db.findUserIdsByRole,
+ updateUsersRoleByIds: db.updateUsersRoleByIds,
+ listUsersByRole: db.listUsersByRole,
+ countUsersByRole: db.countUsersByRole,
+ deleteConfig: db.deleteConfig,
+ deleteAclEntries: db.deleteAclEntries,
+ deleteGrantsForPrincipal: db.deleteGrantsForPrincipal,
+});
+
+router.use(requireJwtAuth, requireAdminAccess);
+
+router.get('/', requireReadRoles, handlers.listRoles);
+router.post('/', requireManageRoles, handlers.createRole);
+router.get('/:name', requireReadRoles, handlers.getRole);
+router.patch('/:name', requireManageRoles, handlers.updateRole);
+router.delete('/:name', requireManageRoles, handlers.deleteRole);
+router.patch('/:name/permissions', requireManageRoles, handlers.updateRolePermissions);
+router.get('/:name/members', requireReadRoles, handlers.getRoleMembers);
+router.post('/:name/members', requireManageRoles, handlers.addRoleMember);
+router.delete('/:name/members/:userId', requireManageRoles, handlers.removeRoleMember);
+
+module.exports = router;
diff --git a/api/server/routes/admin/users.js b/api/server/routes/admin/users.js
new file mode 100644
index 0000000000..20d4eb1797
--- /dev/null
+++ b/api/server/routes/admin/users.js
@@ -0,0 +1,28 @@
+const express = require('express');
+const { createAdminUsersHandlers } = require('@librechat/api');
+const { SystemCapabilities } = require('@librechat/data-schemas');
+const { requireCapability } = require('~/server/middleware/roles/capabilities');
+const { requireJwtAuth } = require('~/server/middleware');
+const db = require('~/models');
+
+const router = express.Router();
+
+const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);
+const requireReadUsers = requireCapability(SystemCapabilities.READ_USERS);
+// const requireManageUsers = requireCapability(SystemCapabilities.MANAGE_USERS);
+
+const handlers = createAdminUsersHandlers({
+ findUsers: db.findUsers,
+ countUsers: db.countUsers,
+ deleteUserById: db.deleteUserById,
+ deleteConfig: db.deleteConfig,
+ deleteAclEntries: db.deleteAclEntries,
+});
+
+router.use(requireJwtAuth, requireAdminAccess);
+
+router.get('/', requireReadUsers, handlers.listUsers);
+router.get('/search', requireReadUsers, handlers.searchUsers);
+// router.delete('/:id', requireManageUsers, handlers.deleteUser);
+
+module.exports = router;
diff --git a/api/server/routes/agents/__tests__/streamTenant.spec.js b/api/server/routes/agents/__tests__/streamTenant.spec.js
new file mode 100644
index 0000000000..1f89953186
--- /dev/null
+++ b/api/server/routes/agents/__tests__/streamTenant.spec.js
@@ -0,0 +1,186 @@
+const express = require('express');
+const request = require('supertest');
+
+const mockGenerationJobManager = {
+ getJob: jest.fn(),
+ subscribe: jest.fn(),
+ getResumeState: jest.fn(),
+ abortJob: jest.fn(),
+ getActiveJobIdsForUser: jest.fn().mockResolvedValue([]),
+};
+
+jest.mock('@librechat/data-schemas', () => ({
+ ...jest.requireActual('@librechat/data-schemas'),
+ logger: {
+ debug: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ info: jest.fn(),
+ },
+}));
+
+jest.mock('@librechat/api', () => ({
+ ...jest.requireActual('@librechat/api'),
+ isEnabled: jest.fn().mockReturnValue(false),
+ GenerationJobManager: mockGenerationJobManager,
+}));
+
+jest.mock('~/models', () => ({
+ saveMessage: jest.fn(),
+}));
+
+let mockUserId = 'user-123';
+let mockTenantId;
+
+jest.mock('~/server/middleware', () => ({
+ uaParser: (req, res, next) => next(),
+ checkBan: (req, res, next) => next(),
+ requireJwtAuth: (req, res, next) => {
+ req.user = { id: mockUserId, tenantId: mockTenantId };
+ next();
+ },
+ messageIpLimiter: (req, res, next) => next(),
+ configMiddleware: (req, res, next) => next(),
+ messageUserLimiter: (req, res, next) => next(),
+}));
+
+jest.mock('~/server/routes/agents/chat', () => require('express').Router());
+jest.mock('~/server/routes/agents/v1', () => ({
+ v1: require('express').Router(),
+}));
+jest.mock('~/server/routes/agents/openai', () => require('express').Router());
+jest.mock('~/server/routes/agents/responses', () => require('express').Router());
+
+const agentsRouter = require('../index');
+const app = express();
+app.use(express.json());
+app.use('/agents', agentsRouter);
+
+function mockSubscribeSuccess() {
+ mockGenerationJobManager.subscribe.mockImplementation((_streamId, _writeEvent, onDone) => {
+ process.nextTick(() => onDone({ done: true }));
+ return { unsubscribe: jest.fn() };
+ });
+}
+
+describe('SSE stream tenant isolation', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUserId = 'user-123';
+ mockTenantId = undefined;
+ });
+
+ describe('GET /chat/stream/:streamId', () => {
+ it('returns 403 when a user from a different tenant accesses a stream', async () => {
+ mockUserId = 'user-456';
+ mockTenantId = 'tenant-b';
+
+ mockGenerationJobManager.getJob.mockResolvedValue({
+ metadata: { userId: 'user-456', tenantId: 'tenant-a' },
+ status: 'running',
+ });
+
+ const res = await request(app).get('/agents/chat/stream/stream-123');
+ expect(res.status).toBe(403);
+ expect(res.body.error).toBe('Unauthorized');
+ });
+
+ it('returns 404 when stream does not exist', async () => {
+ mockGenerationJobManager.getJob.mockResolvedValue(null);
+
+ const res = await request(app).get('/agents/chat/stream/nonexistent');
+ expect(res.status).toBe(404);
+ });
+
+ it('proceeds past tenant guard when tenant matches', async () => {
+ mockUserId = 'user-123';
+ mockTenantId = 'tenant-a';
+ mockSubscribeSuccess();
+
+ mockGenerationJobManager.getJob.mockResolvedValue({
+ metadata: { userId: 'user-123', tenantId: 'tenant-a' },
+ status: 'running',
+ });
+
+ const res = await request(app).get('/agents/chat/stream/stream-123');
+ expect(res.status).toBe(200);
+ expect(mockGenerationJobManager.subscribe).toHaveBeenCalledTimes(1);
+ });
+
+ it('proceeds past tenant guard when job has no tenantId (single-tenant mode)', async () => {
+ mockUserId = 'user-123';
+ mockTenantId = undefined;
+ mockSubscribeSuccess();
+
+ mockGenerationJobManager.getJob.mockResolvedValue({
+ metadata: { userId: 'user-123' },
+ status: 'running',
+ });
+
+ const res = await request(app).get('/agents/chat/stream/stream-123');
+ expect(res.status).toBe(200);
+ expect(mockGenerationJobManager.subscribe).toHaveBeenCalledTimes(1);
+ });
+
+ it('returns 403 when job has tenantId but user has no tenantId', async () => {
+ mockUserId = 'user-123';
+ mockTenantId = undefined;
+
+ mockGenerationJobManager.getJob.mockResolvedValue({
+ metadata: { userId: 'user-123', tenantId: 'some-tenant' },
+ status: 'running',
+ });
+
+ const res = await request(app).get('/agents/chat/stream/stream-123');
+ expect(res.status).toBe(403);
+ });
+ });
+
+ describe('GET /chat/status/:conversationId', () => {
+ it('returns 403 when tenant does not match', async () => {
+ mockUserId = 'user-123';
+ mockTenantId = 'tenant-b';
+
+ mockGenerationJobManager.getJob.mockResolvedValue({
+ metadata: { userId: 'user-123', tenantId: 'tenant-a' },
+ status: 'running',
+ });
+
+ const res = await request(app).get('/agents/chat/status/conv-123');
+ expect(res.status).toBe(403);
+ expect(res.body.error).toBe('Unauthorized');
+ });
+
+ it('returns status when tenant matches', async () => {
+ mockUserId = 'user-123';
+ mockTenantId = 'tenant-a';
+
+ mockGenerationJobManager.getJob.mockResolvedValue({
+ metadata: { userId: 'user-123', tenantId: 'tenant-a' },
+ status: 'running',
+ createdAt: Date.now(),
+ });
+ mockGenerationJobManager.getResumeState.mockResolvedValue(null);
+
+ const res = await request(app).get('/agents/chat/status/conv-123');
+ expect(res.status).toBe(200);
+ expect(res.body.active).toBe(true);
+ });
+ });
+
+ describe('POST /chat/abort', () => {
+ it('returns 403 when tenant does not match', async () => {
+ mockUserId = 'user-123';
+ mockTenantId = 'tenant-b';
+
+ mockGenerationJobManager.getJob.mockResolvedValue({
+ metadata: { userId: 'user-123', tenantId: 'tenant-a' },
+ status: 'running',
+ });
+
+ const res = await request(app).post('/agents/chat/abort').send({ streamId: 'stream-123' });
+ expect(res.status).toBe(403);
+ expect(res.body.error).toBe('Unauthorized');
+ });
+ });
+});
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..eb42046bed 100644
--- a/api/server/routes/agents/index.js
+++ b/api/server/routes/agents/index.js
@@ -10,13 +10,18 @@ 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');
const { LIMIT_MESSAGE_IP, LIMIT_MESSAGE_USER } = process.env ?? {};
+/** Untenanted jobs (pre-multi-tenancy) remain accessible if the userId check passes. */
+function hasTenantMismatch(job, user) {
+ return job.metadata?.tenantId != null && job.metadata.tenantId !== user.tenantId;
+}
+
const router = express.Router();
/**
@@ -67,6 +72,10 @@ router.get('/chat/stream/:streamId', async (req, res) => {
return res.status(403).json({ error: 'Unauthorized' });
}
+ if (hasTenantMismatch(job, req.user)) {
+ return res.status(403).json({ error: 'Unauthorized' });
+ }
+
res.setHeader('Content-Encoding', 'identity');
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache, no-transform');
@@ -150,7 +159,10 @@ router.get('/chat/stream/:streamId', async (req, res) => {
* @returns { activeJobIds: string[] }
*/
router.get('/chat/active', async (req, res) => {
- const activeJobIds = await GenerationJobManager.getActiveJobIdsForUser(req.user.id);
+ const activeJobIds = await GenerationJobManager.getActiveJobIdsForUser(
+ req.user.id,
+ req.user.tenantId,
+ );
res.json({ activeJobIds });
});
@@ -174,6 +186,10 @@ router.get('/chat/status/:conversationId', async (req, res) => {
return res.status(403).json({ error: 'Unauthorized' });
}
+ if (hasTenantMismatch(job, req.user)) {
+ return res.status(403).json({ error: 'Unauthorized' });
+ }
+
// Get resume state which contains aggregatedContent
// Avoid calling both getStreamInfo and getResumeState (both fetch content)
const resumeState = await GenerationJobManager.getResumeState(conversationId);
@@ -213,7 +229,10 @@ router.post('/chat/abort', async (req, res) => {
// This handles the case where frontend sends "new" but job was created with a UUID
if (!job && userId) {
logger.debug(`[AgentStream] Job not found by ID, checking active jobs for user: ${userId}`);
- const activeJobIds = await GenerationJobManager.getActiveJobIdsForUser(userId);
+ const activeJobIds = await GenerationJobManager.getActiveJobIdsForUser(
+ userId,
+ req.user.tenantId,
+ );
if (activeJobIds.length > 0) {
// Abort the most recent active job for this user
jobStreamId = activeJobIds[0];
@@ -230,6 +249,10 @@ router.post('/chat/abort', async (req, res) => {
return res.status(403).json({ error: 'Unauthorized' });
}
+ if (hasTenantMismatch(job, req.user)) {
+ return res.status(403).json({ error: 'Unauthorized' });
+ }
+
logger.debug(`[AgentStream] Job found, aborting: ${jobStreamId}`);
const abortResult = await GenerationJobManager.abortJob(jobStreamId);
logger.debug(`[AgentStream] Job aborted successfully: ${jobStreamId}`, {
@@ -263,9 +286,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 ed989bcf44..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');
@@ -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/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/config.js b/api/server/routes/config.js
index 0adc9272bb..a57e4bd958 100644
--- a/api/server/routes/config.js
+++ b/api/server/routes/config.js
@@ -1,11 +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 { defaultSocialLogins } = require('librechat-data-provider');
+const { logger, getTenantId } = require('@librechat/data-schemas');
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();
const emailLoginEnabled =
@@ -21,131 +19,159 @@ const publicSharedLinksEnabled =
const sharePointFilePickerEnabled = isEnabled(process.env.ENABLE_SHAREPOINT_FILEPICKER);
const openidReuseTokens = isEnabled(process.env.OPENID_REUSE_TOKENS);
-router.get('/', async function (req, res) {
- const cache = getLogStores(CacheKeys.CONFIG_STORE);
+function isBirthday() {
+ const today = new Date();
+ return today.getMonth() === 1 && today.getDate() === 11;
+}
- const cachedStartupConfig = await cache.get(CacheKeys.STARTUP_CONFIG);
- if (cachedStartupConfig) {
- res.send(cachedStartupConfig);
- return;
- }
+function buildSharedPayload() {
+ const isOpenIdEnabled =
+ !!process.env.OPENID_CLIENT_ID &&
+ !!process.env.OPENID_CLIENT_SECRET &&
+ !!process.env.OPENID_ISSUER &&
+ !!process.env.OPENID_SESSION_SECRET;
- const isBirthday = () => {
- const today = new Date();
- return today.getMonth() === 1 && today.getDate() === 11;
- };
-
- const instanceProject = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, '_id');
+ const isSamlEnabled =
+ !!process.env.SAML_ENTRY_POINT &&
+ !!process.env.SAML_ISSUER &&
+ !!process.env.SAML_CERT &&
+ !!process.env.SAML_SESSION_SECRET;
const ldap = getLdapConfig();
+ /** @type {Partial} */
+ const payload = {
+ appTitle: process.env.APP_TITLE || 'LibreChat',
+ discordLoginEnabled: !!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET,
+ facebookLoginEnabled: !!process.env.FACEBOOK_CLIENT_ID && !!process.env.FACEBOOK_CLIENT_SECRET,
+ githubLoginEnabled: !!process.env.GITHUB_CLIENT_ID && !!process.env.GITHUB_CLIENT_SECRET,
+ googleLoginEnabled: !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET,
+ appleLoginEnabled:
+ !!process.env.APPLE_CLIENT_ID &&
+ !!process.env.APPLE_TEAM_ID &&
+ !!process.env.APPLE_KEY_ID &&
+ !!process.env.APPLE_PRIVATE_KEY_PATH,
+ openidLoginEnabled: isOpenIdEnabled,
+ openidLabel: process.env.OPENID_BUTTON_LABEL || 'Continue with OpenID',
+ openidImageUrl: process.env.OPENID_IMAGE_URL,
+ openidAutoRedirect: isEnabled(process.env.OPENID_AUTO_REDIRECT),
+ samlLoginEnabled: !isOpenIdEnabled && isSamlEnabled,
+ samlLabel: process.env.SAML_BUTTON_LABEL,
+ samlImageUrl: process.env.SAML_IMAGE_URL,
+ serverDomain: process.env.DOMAIN_SERVER || 'http://localhost:3080',
+ emailLoginEnabled,
+ registrationEnabled: !ldap?.enabled && isEnabled(process.env.ALLOW_REGISTRATION),
+ socialLoginEnabled: isEnabled(process.env.ALLOW_SOCIAL_LOGIN),
+ emailEnabled:
+ (!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
+ !!process.env.EMAIL_USERNAME &&
+ !!process.env.EMAIL_PASSWORD &&
+ !!process.env.EMAIL_FROM,
+ passwordResetEnabled,
+ showBirthdayIcon:
+ isBirthday() ||
+ isEnabled(process.env.SHOW_BIRTHDAY_ICON) ||
+ process.env.SHOW_BIRTHDAY_ICON === '',
+ helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai',
+ sharedLinksEnabled,
+ publicSharedLinksEnabled,
+ analyticsGtmId: process.env.ANALYTICS_GTM_ID,
+ openidReuseTokens,
+ };
+
+ const minPasswordLength = parseInt(process.env.MIN_PASSWORD_LENGTH, 10);
+ if (minPasswordLength && !isNaN(minPasswordLength)) {
+ payload.minPasswordLength = minPasswordLength;
+ }
+
+ if (ldap) {
+ payload.ldap = ldap;
+ }
+
+ if (typeof process.env.CUSTOM_FOOTER === 'string') {
+ payload.customFooter = process.env.CUSTOM_FOOTER;
+ }
+
+ return payload;
+}
+
+function buildWebSearchConfig(appConfig) {
+ const ws = appConfig?.webSearch;
+ if (!ws) {
+ return undefined;
+ }
+ const { searchProvider, scraperProvider, rerankerType } = ws;
+ if (!searchProvider && !scraperProvider && !rerankerType) {
+ return undefined;
+ }
+ return {
+ ...(searchProvider && { searchProvider }),
+ ...(scraperProvider && { scraperProvider }),
+ ...(rerankerType && { rerankerType }),
+ };
+}
+
+router.get('/', async function (req, res) {
try {
- const appConfig = await getAppConfig({ role: req.user?.role });
+ const sharedPayload = buildSharedPayload();
- const isOpenIdEnabled =
- !!process.env.OPENID_CLIENT_ID &&
- !!process.env.OPENID_CLIENT_SECRET &&
- !!process.env.OPENID_ISSUER &&
- !!process.env.OPENID_SESSION_SECRET;
+ if (!req.user) {
+ const tenantId = getTenantId();
+ const baseConfig = await getAppConfig(tenantId ? { tenantId } : { baseOnly: true });
- const isSamlEnabled =
- !!process.env.SAML_ENTRY_POINT &&
- !!process.env.SAML_ISSUER &&
- !!process.env.SAML_CERT &&
- !!process.env.SAML_SESSION_SECRET;
+ /** @type {Partial} */
+ const payload = {
+ ...sharedPayload,
+ socialLogins: baseConfig?.registration?.socialLogins ?? defaultSocialLogins,
+ turnstile: baseConfig?.turnstileConfig,
+ };
+
+ const interfaceConfig = baseConfig?.interfaceConfig;
+ if (interfaceConfig?.privacyPolicy || interfaceConfig?.termsOfService) {
+ payload.interface = {};
+ if (interfaceConfig.privacyPolicy) {
+ payload.interface.privacyPolicy = interfaceConfig.privacyPolicy;
+ }
+ if (interfaceConfig.termsOfService) {
+ payload.interface.termsOfService = interfaceConfig.termsOfService;
+ }
+ }
+
+ return res.status(200).send(payload);
+ }
+
+ const appConfig = await getAppConfig({
+ role: req.user.role,
+ userId: req.user.id,
+ tenantId: req.user.tenantId || getTenantId(),
+ });
const balanceConfig = getBalanceConfig(appConfig);
/** @type {TStartupConfig} */
const payload = {
- appTitle: process.env.APP_TITLE || 'LibreChat',
+ ...sharedPayload,
socialLogins: appConfig?.registration?.socialLogins ?? defaultSocialLogins,
- discordLoginEnabled: !!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET,
- facebookLoginEnabled:
- !!process.env.FACEBOOK_CLIENT_ID && !!process.env.FACEBOOK_CLIENT_SECRET,
- githubLoginEnabled: !!process.env.GITHUB_CLIENT_ID && !!process.env.GITHUB_CLIENT_SECRET,
- googleLoginEnabled: !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET,
- appleLoginEnabled:
- !!process.env.APPLE_CLIENT_ID &&
- !!process.env.APPLE_TEAM_ID &&
- !!process.env.APPLE_KEY_ID &&
- !!process.env.APPLE_PRIVATE_KEY_PATH,
- openidLoginEnabled: isOpenIdEnabled,
- openidLabel: process.env.OPENID_BUTTON_LABEL || 'Continue with OpenID',
- openidImageUrl: process.env.OPENID_IMAGE_URL,
- openidAutoRedirect: isEnabled(process.env.OPENID_AUTO_REDIRECT),
- samlLoginEnabled: !isOpenIdEnabled && isSamlEnabled,
- samlLabel: process.env.SAML_BUTTON_LABEL,
- samlImageUrl: process.env.SAML_IMAGE_URL,
- serverDomain: process.env.DOMAIN_SERVER || 'http://localhost:3080',
- emailLoginEnabled,
- registrationEnabled: !ldap?.enabled && isEnabled(process.env.ALLOW_REGISTRATION),
- socialLoginEnabled: isEnabled(process.env.ALLOW_SOCIAL_LOGIN),
- emailEnabled:
- (!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
- !!process.env.EMAIL_USERNAME &&
- !!process.env.EMAIL_PASSWORD &&
- !!process.env.EMAIL_FROM,
- passwordResetEnabled,
- showBirthdayIcon:
- isBirthday() ||
- isEnabled(process.env.SHOW_BIRTHDAY_ICON) ||
- process.env.SHOW_BIRTHDAY_ICON === '',
- helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai',
interface: appConfig?.interfaceConfig,
turnstile: appConfig?.turnstileConfig,
modelSpecs: appConfig?.modelSpecs,
balance: balanceConfig,
- 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,
sharePointBaseUrl: process.env.SHAREPOINT_BASE_URL,
sharePointPickerGraphScope: process.env.SHAREPOINT_PICKER_GRAPH_SCOPE,
sharePointPickerSharePointScope: process.env.SHAREPOINT_PICKER_SHAREPOINT_SCOPE,
- openidReuseTokens,
conversationImportMaxFileSize: process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES
? parseInt(process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES, 10)
: 0,
};
- const minPasswordLength = parseInt(process.env.MIN_PASSWORD_LENGTH, 10);
- if (minPasswordLength && !isNaN(minPasswordLength)) {
- payload.minPasswordLength = minPasswordLength;
+ const webSearch = buildWebSearchConfig(appConfig);
+ if (webSearch) {
+ payload.webSearch = webSearch;
}
- const webSearchConfig = appConfig?.webSearch;
- if (
- webSearchConfig != null &&
- (webSearchConfig.searchProvider ||
- webSearchConfig.scraperProvider ||
- webSearchConfig.rerankerType)
- ) {
- payload.webSearch = {};
- }
-
- if (webSearchConfig?.searchProvider) {
- payload.webSearch.searchProvider = webSearchConfig.searchProvider;
- }
- if (webSearchConfig?.scraperProvider) {
- payload.webSearch.scraperProvider = webSearchConfig.scraperProvider;
- }
- if (webSearchConfig?.rerankerType) {
- payload.webSearch.rerankerType = webSearchConfig.rerankerType;
- }
-
- if (ldap) {
- payload.ldap = ldap;
- }
-
- if (typeof process.env.CUSTOM_FOOTER === 'string') {
- payload.customFooter = process.env.CUSTOM_FOOTER;
- }
-
- await cache.set(CacheKeys.STARTUP_CONFIG, payload);
return res.status(200).send(payload);
} catch (err) {
logger.error('Error in startup config', err);
diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js
index 578796170a..ded7d835d7 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}` },
);
@@ -261,7 +267,11 @@ router.post(
async (req, res) => {
try {
/* TODO: optimize to return imported conversations and add manually */
- await importConversations({ filepath: req.file.path, requestUserId: req.user.id });
+ await importConversations({
+ filepath: req.file.path,
+ requestUserId: req.user.id,
+ userRole: req.user.role,
+ });
res.status(201).json({ message: 'Conversation(s) imported successfully' });
} catch (error) {
logger.error('Error processing file', error);
diff --git a/api/server/routes/endpoints.js b/api/server/routes/endpoints.js
index 794abde0c2..e7ff1c7000 100644
--- a/api/server/routes/endpoints.js
+++ b/api/server/routes/endpoints.js
@@ -1,7 +1,9 @@
const express = require('express');
+const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
const endpointController = require('~/server/controllers/EndpointController');
const router = express.Router();
-router.get('/', endpointController);
+/** Auth required for role/tenant-scoped endpoint config resolution. */
+router.get('/', requireJwtAuth, endpointController);
module.exports = router;
diff --git a/api/server/routes/files/files.agents.test.js b/api/server/routes/files/files.agents.test.js
index 7c21e95234..cb0e4ff3d2 100644
--- a/api/server/routes/files/files.agents.test.js
+++ b/api/server/routes/files/files.agents.test.js
@@ -2,16 +2,15 @@ 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 { 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', () => ({
@@ -39,7 +38,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(),
}));
@@ -83,6 +91,7 @@ describe('File Routes - Agent Files Endpoint', () => {
let AclEntry;
// eslint-disable-next-line no-unused-vars
let AccessRole;
+ let SystemGrant;
let modelsToCleanup = [];
beforeAll(async () => {
@@ -109,6 +118,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();
@@ -533,7 +543,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,
@@ -543,6 +553,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 9290d1a7ed..eb13ecdc31 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 } = require('@librechat/api');
+const { logger, SystemCapabilities } = require('@librechat/data-schemas');
+const {
+ refreshS3FileUrls,
+ resolveUploadErrorMessage,
+ verifyAgentUploadPermission,
+} = require('@librechat/api');
const {
Time,
isUUID,
@@ -23,29 +27,27 @@ 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 { 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 +76,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 +108,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 +153,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 +211,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 +225,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,
});
@@ -381,34 +383,31 @@ router.post('/', async (req, res) => {
return await processFileUpload({ req, res, metadata });
}
- const denied = await verifyAgentUploadPermission({
- req,
- res,
- metadata,
- 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 });
} 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/files.test.js b/api/server/routes/files/files.test.js
index 1d548b44be..37cbf68b93 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', () => ({
@@ -33,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/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 185ec7a671..353557dc4f 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,
@@ -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) {
@@ -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/api/server/routes/index.js b/api/server/routes/index.js
index 6a48919db3..1feaf63fdb 100644
--- a/api/server/routes/index.js
+++ b/api/server/routes/index.js
@@ -2,6 +2,11 @@ const accessPermissions = require('./accessPermissions');
const assistants = require('./assistants');
const categories = require('./categories');
const adminAuth = require('./admin/auth');
+const adminConfig = require('./admin/config');
+const adminGrants = require('./admin/grants');
+const adminGroups = require('./admin/groups');
+const adminRoles = require('./admin/roles');
+const adminUsers = require('./admin/users');
const endpoints = require('./endpoints');
const staticRoute = require('./static');
const messages = require('./messages');
@@ -31,6 +36,11 @@ module.exports = {
mcp,
auth,
adminAuth,
+ adminConfig,
+ adminGrants,
+ adminGroups,
+ adminRoles,
+ adminUsers,
keys,
apiKeys,
user,
diff --git a/api/server/routes/mcp.js b/api/server/routes/mcp.js
index 57a99d199a..c6496ad4b4 100644
--- a/api/server/routes/mcp.js
+++ b/api/server/routes/mcp.js
@@ -1,5 +1,5 @@
const { Router } = require('express');
-const { logger } = require('@librechat/data-schemas');
+const { logger, getTenantId } = require('@librechat/data-schemas');
const {
CacheKeys,
Constants,
@@ -36,15 +36,17 @@ const {
getFlowStateManager,
getMCPManager,
} = require('~/config');
-const { getMCPSetupData, getServerConnectionStatus } = require('~/server/services/MCP');
+const {
+ getServerConnectionStatus,
+ resolveConfigServers,
+ getMCPSetupData,
+} = 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 +55,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,
});
/**
@@ -103,7 +105,8 @@ router.get('/:serverName/oauth/initiate', requireJwtAuth, setOAuthSession, async
return res.status(400).json({ error: 'Invalid flow state' });
}
- const oauthHeaders = await getOAuthHeaders(serverName, userId);
+ const configServers = await resolveConfigServers(req);
+ const oauthHeaders = await getOAuthHeaders(serverName, userId, configServers);
const {
authorizationUrl,
flowId: oauthFlowId,
@@ -235,7 +238,14 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
}
logger.debug('[MCP OAuth] Completing OAuth flow');
- const oauthHeaders = await getOAuthHeaders(serverName, flowState.userId);
+ if (!flowState.oauthHeaders) {
+ logger.warn(
+ '[MCP OAuth] oauthHeaders absent from flow state — config-source server oauth_headers will be empty',
+ { serverName, flowId },
+ );
+ }
+ const oauthHeaders =
+ flowState.oauthHeaders ?? (await getOAuthHeaders(serverName, flowState.userId));
const tokens = await MCPOAuthHandler.completeOAuthFlow(flowId, code, flowManager, oauthHeaders);
logger.info('[MCP OAuth] OAuth flow completed, tokens received in callback route');
@@ -246,9 +256,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 +296,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,
},
});
@@ -499,7 +509,12 @@ router.post(
logger.info(`[MCP Reinitialize] Reinitializing server: ${serverName}`);
const mcpManager = getMCPManager();
- const serverConfig = await getMCPServersRegistry().getServerConfig(serverName, user.id);
+ const configServers = await resolveConfigServers(req);
+ const serverConfig = await getMCPServersRegistry().getServerConfig(
+ serverName,
+ user.id,
+ configServers,
+ );
if (!serverConfig) {
return res.status(404).json({
error: `MCP server '${serverName}' not found in configuration`,
@@ -517,13 +532,15 @@ router.post(
userMCPAuthMap = await getUserMCPAuthMap({
userId: user.id,
servers: [serverName],
- findPluginAuthsByKeys,
+ findPluginAuthsByKeys: db.findPluginAuthsByKeys,
});
}
const result = await reinitMCPServer({
user,
serverName,
+ serverConfig,
+ configServers,
userMCPAuthMap,
});
@@ -566,6 +583,7 @@ router.get('/connection/status', requireJwtAuth, async (req, res) => {
const { mcpConfig, appConnections, userConnections, oauthServers } = await getMCPSetupData(
user.id,
+ { role: user.role, tenantId: getTenantId() },
);
const connectionStatus = {};
@@ -595,9 +613,6 @@ router.get('/connection/status', requireJwtAuth, async (req, res) => {
connectionStatus,
});
} catch (error) {
- if (error.message === 'MCP config not found') {
- return res.status(404).json({ error: error.message });
- }
logger.error('[MCP Connection Status] Failed to get connection status', error);
res.status(500).json({ error: 'Failed to get connection status' });
}
@@ -618,6 +633,7 @@ router.get('/connection/status/:serverName', requireJwtAuth, async (req, res) =>
const { mcpConfig, appConnections, userConnections, oauthServers } = await getMCPSetupData(
user.id,
+ { role: user.role, tenantId: getTenantId() },
);
if (!mcpConfig[serverName]) {
@@ -642,9 +658,6 @@ router.get('/connection/status/:serverName', requireJwtAuth, async (req, res) =>
requiresOAuth: serverStatus.requiresOAuth,
});
} catch (error) {
- if (error.message === 'MCP config not found') {
- return res.status(404).json({ error: error.message });
- }
logger.error(
`[MCP Per-Server Status] Failed to get connection status for ${req.params.serverName}`,
error,
@@ -666,7 +679,12 @@ router.get('/:serverName/auth-values', requireJwtAuth, checkMCPUsePermissions, a
return res.status(401).json({ error: 'User not authenticated' });
}
- const serverConfig = await getMCPServersRegistry().getServerConfig(serverName, user.id);
+ const configServers = await resolveConfigServers(req);
+ const serverConfig = await getMCPServersRegistry().getServerConfig(
+ serverName,
+ user.id,
+ configServers,
+ );
if (!serverConfig) {
return res.status(404).json({
error: `MCP server '${serverName}' not found in configuration`,
@@ -705,8 +723,12 @@ router.get('/:serverName/auth-values', requireJwtAuth, checkMCPUsePermissions, a
}
});
-async function getOAuthHeaders(serverName, userId) {
- const serverConfig = await getMCPServersRegistry().getServerConfig(serverName, userId);
+async function getOAuthHeaders(serverName, userId, configServers) {
+ const serverConfig = await getMCPServersRegistry().getServerConfig(
+ serverName,
+ userId,
+ configServers,
+ );
return serverConfig?.oauth_headers ?? {};
}
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 037bf04813..5fcf51ba73 100644
--- a/api/server/routes/prompts.js
+++ b/api/server/routes/prompts.js
@@ -1,5 +1,6 @@
const express = require('express');
-const { logger } = require('@librechat/data-schemas');
+const { ObjectId } = require('mongodb');
+const { logger, isValidObjectIdString } = require('@librechat/data-schemas');
const {
generateCheckAccess,
markPublicPromptGroups,
@@ -11,28 +12,32 @@ const {
} = require('@librechat/api');
const {
Permissions,
- SystemRoles,
ResourceType,
AccessRoleIds,
PrincipalType,
PermissionBits,
PermissionTypes,
} = require('librechat-data-provider');
+const { SystemCapabilities } = require('@librechat/data-schemas');
const {
getListPromptGroupsByAccess,
+ getOwnedPromptGroupIds,
+ incrementPromptGroupUsage,
makePromptProduction,
updatePromptGroup,
deletePromptGroup,
createPromptGroup,
getPromptGroup,
+ getRoleByName,
deletePrompt,
getPrompts,
savePrompt,
getPrompt,
-} = require('~/models/Prompt');
+} = require('~/models');
const {
canAccessPromptGroupResource,
canAccessPromptViaGroup,
+ promptUsageLimiter,
requireJwtAuth,
} = require('~/server/middleware');
const {
@@ -41,7 +46,7 @@ const {
findAccessibleResources,
grantPermission,
} = require('~/server/services/PermissionService');
-const { getRoleByName } = require('~/models/Role');
+const { hasCapability } = require('~/server/middleware/roles/capabilities');
const router = express.Router();
@@ -56,18 +61,15 @@ const checkPromptCreate = generateCheckAccess({
getRoleByName,
});
+router.use(requireJwtAuth);
+router.use(checkPromptAccess);
+
const checkGlobalPromptShare = generateCheckAccess({
permissionType: PermissionTypes.PROMPTS,
permissions: [Permissions.USE, Permissions.CREATE],
- bodyProps: {
- [Permissions.SHARE]: ['projectIds', 'removeProjectIds'],
- },
getRoleByName,
});
-router.use(requireJwtAuth);
-router.use(checkPromptAccess);
-
/**
* Route to get single prompt group by its ID
* GET /groups/:groupId
@@ -102,11 +104,10 @@ router.get(
router.get('/all', async (req, res) => {
try {
const userId = req.user.id;
- const { name, category, ...otherFilters } = req.query;
+ const { name, category } = req.query;
const { filter, searchShared, searchSharedOnly } = buildPromptGroupFilter({
name,
category,
- ...otherFilters,
});
let accessibleIds = await findAccessibleResources({
@@ -116,16 +117,20 @@ router.get('/all', async (req, res) => {
requiredPermissions: PermissionBits.VIEW,
});
- const publiclyAccessibleIds = await findPubliclyAccessibleResources({
- resourceType: ResourceType.PROMPTGROUP,
- requiredPermissions: PermissionBits.VIEW,
- });
+ const [publiclyAccessibleIds, ownedPromptGroupIds] = await Promise.all([
+ findPubliclyAccessibleResources({
+ resourceType: ResourceType.PROMPTGROUP,
+ requiredPermissions: PermissionBits.VIEW,
+ }),
+ getOwnedPromptGroupIds(userId),
+ ]);
const filteredAccessibleIds = await filterAccessibleIdsBySharedLogic({
accessibleIds,
searchShared,
searchSharedOnly,
publicPromptGroupIds: publiclyAccessibleIds,
+ ownedPromptGroupIds,
});
const result = await getListPromptGroupsByAccess({
@@ -157,12 +162,11 @@ router.get('/all', async (req, res) => {
router.get('/groups', async (req, res) => {
try {
const userId = req.user.id;
- const { pageSize, limit, cursor, name, category, ...otherFilters } = req.query;
+ const { pageSize, limit, cursor, name, category } = req.query;
const { filter, searchShared, searchSharedOnly } = buildPromptGroupFilter({
name,
category,
- ...otherFilters,
});
let actualLimit = limit;
@@ -186,16 +190,20 @@ router.get('/groups', async (req, res) => {
requiredPermissions: PermissionBits.VIEW,
});
- const publiclyAccessibleIds = await findPubliclyAccessibleResources({
- resourceType: ResourceType.PROMPTGROUP,
- requiredPermissions: PermissionBits.VIEW,
- });
+ const [publiclyAccessibleIds, ownedPromptGroupIds] = await Promise.all([
+ findPubliclyAccessibleResources({
+ resourceType: ResourceType.PROMPTGROUP,
+ requiredPermissions: PermissionBits.VIEW,
+ }),
+ getOwnedPromptGroupIds(userId),
+ ]);
const filteredAccessibleIds = await filterAccessibleIdsBySharedLogic({
accessibleIds,
searchShared,
searchSharedOnly,
publicPromptGroupIds: publiclyAccessibleIds,
+ ownedPromptGroupIds,
});
// Cursor-based pagination only
@@ -299,6 +307,16 @@ const addPromptToGroup = async (req, res) => {
return res.status(400).send({ error: 'Prompt is required' });
}
+ if (typeof prompt.prompt !== 'string' || !prompt.prompt.trim()) {
+ return res
+ .status(400)
+ .send({ error: 'Prompt text is required and must be a non-empty string' });
+ }
+
+ if (prompt.type !== 'text' && prompt.type !== 'chat') {
+ return res.status(400).send({ error: 'Prompt type must be "text" or "chat"' });
+ }
+
// Ensure the prompt is associated with the correct group
prompt.groupId = groupId;
@@ -329,6 +347,37 @@ router.post(
addPromptToGroup,
);
+/**
+ * Records a prompt group usage (increments numberOfGenerations)
+ * POST /groups/:groupId/use
+ */
+router.post(
+ '/groups/:groupId/use',
+ promptUsageLimiter,
+ canAccessPromptGroupResource({
+ requiredPermission: PermissionBits.VIEW,
+ }),
+ async (req, res) => {
+ try {
+ const { groupId } = req.params;
+ if (!isValidObjectIdString(groupId)) {
+ return res.status(400).send({ error: 'Invalid groupId' });
+ }
+ const result = await incrementPromptGroupUsage(groupId);
+ res.status(200).send(result);
+ } catch (error) {
+ logger.error('[recordPromptUsage]', error);
+ if (error.message === 'Invalid groupId') {
+ return res.status(400).send({ error: 'Invalid groupId' });
+ }
+ if (error.message === 'Prompt group not found') {
+ return res.status(404).send({ error: 'Prompt group not found' });
+ }
+ res.status(500).send({ error: 'Error recording prompt usage' });
+ }
+ },
+);
+
/**
* Updates a prompt group
* @param {object} req
@@ -340,11 +389,8 @@ router.post(
const patchPromptGroup = async (req, res) => {
try {
const { groupId } = req.params;
- const author = req.user.id;
- const filter = { _id: groupId, author };
- if (req.user.role === SystemRoles.ADMIN) {
- delete filter.author;
- }
+ // Don't pass author - permissions are now checked by middleware
+ const filter = { _id: groupId };
const validationResult = safeValidatePromptGroupUpdate(req.body);
if (!validationResult.success) {
@@ -410,6 +456,10 @@ router.get('/', async (req, res) => {
// If requesting prompts for a specific group, check permissions
if (groupId) {
+ if (!isValidObjectIdString(groupId)) {
+ return res.status(400).send({ error: 'Invalid groupId' });
+ }
+
const permissions = await getEffectivePermissions({
userId: req.user.id,
role: req.user.role,
@@ -424,13 +474,20 @@ router.get('/', async (req, res) => {
}
// If user has access, fetch all prompts in the group (not just their own)
- const prompts = await getPrompts({ groupId });
+ const prompts = await getPrompts({ groupId: new ObjectId(groupId) });
return res.status(200).send(prompts);
}
// 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);
@@ -454,8 +511,10 @@ 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 };
+ if (!groupId || !isValidObjectIdString(groupId)) {
+ return res.status(400).send({ error: 'Invalid or missing groupId' });
+ }
+ const query = { promptId, groupId };
const result = await deletePrompt(query);
res.status(200).send(result);
} catch (error) {
@@ -473,8 +532,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 caeb90ddfb..c979023ffc 100644
--- a/api/server/routes/prompts.test.js
+++ b/api/server/routes/prompts.test.js
@@ -10,18 +10,33 @@ const {
PrincipalType,
PermissionBits,
} = require('librechat-data-provider');
+const { SystemCapabilities } = require('@librechat/data-schemas');
// Mock modules before importing
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(),
+ promptUsageLimiter: (req, res, next) => next(),
canAccessPromptViaGroup: jest.requireActual('~/server/middleware').canAccessPromptViaGroup,
canAccessPromptGroupResource:
jest.requireActual('~/server/middleware').canAccessPromptGroupResource,
@@ -30,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
@@ -52,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');
@@ -152,8 +168,24 @@ 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/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..25ee47854d 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,13 @@ const {
peoplePickerPermissionsSchema,
remoteAgentsPermissionsSchema,
} = require('librechat-data-provider');
-const { checkAdmin, requireJwtAuth } = require('~/server/middleware');
-const { updateRoleByName, getRoleByName } = require('~/models/Role');
+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);
+const manageRoles = requireCapability(SystemCapabilities.MANAGE_ROLES);
/**
* Permission configuration mapping
@@ -111,14 +114,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 +132,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 +141,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/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/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/AuthService.js b/api/server/services/AuthService.js
index ef50a365b9..816a0eac5b 100644
--- a/api/server/services/AuthService.js
+++ b/api/server/services/AuthService.js
@@ -13,6 +13,7 @@ const {
checkEmailConfig,
isEmailDomainAllowed,
shouldUseSecureCookie,
+ resolveAppConfigForUser,
} = require('@librechat/api');
const {
findUser,
@@ -189,7 +190,7 @@ const registerUser = async (user, additionalData = {}) => {
let newUserId;
try {
- const appConfig = await getAppConfig();
+ const appConfig = await getAppConfig({ baseOnly: true });
if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
const errorMessage =
'The email address provided cannot be used. Please use a different email address.';
@@ -255,19 +256,52 @@ const registerUser = async (user, additionalData = {}) => {
};
/**
- * Request password reset
+ * Request password reset.
+ *
+ * Uses a two-phase domain check: fast-fail with the memory-cached base config
+ * (zero DB queries) to block globally denied domains before user lookup, then
+ * re-check with tenant-scoped config after user lookup so tenant-specific
+ * restrictions are enforced.
+ *
+ * Phase 1 (base check) returns an Error (HTTP 400) — this intentionally reveals
+ * that the domain is globally blocked, but fires before any DB lookup so it
+ * cannot confirm user existence. Phase 2 (tenant check) returns the generic
+ * success message (HTTP 200) to prevent user-enumeration via status codes.
+ *
* @param {ServerRequest} req
*/
const requestPasswordReset = async (req) => {
const { email } = req.body;
- const appConfig = await getAppConfig();
- if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
+
+ const baseConfig = await getAppConfig({ baseOnly: true });
+ if (!isEmailDomainAllowed(email, baseConfig?.registration?.allowedDomains)) {
+ logger.warn(
+ `[requestPasswordReset] Blocked - email domain not allowed [Email: ${email}] [IP: ${req.ip}]`,
+ );
const error = new Error(ErrorTypes.AUTH_FAILED);
error.code = ErrorTypes.AUTH_FAILED;
error.message = 'Email domain not allowed';
return error;
}
- const user = await findUser({ email }, 'email _id');
+
+ const user = await findUser({ email }, 'email _id role tenantId');
+ let appConfig = baseConfig;
+ if (user?.tenantId) {
+ try {
+ appConfig = await resolveAppConfigForUser(getAppConfig, user);
+ } catch (err) {
+ logger.error('[requestPasswordReset] Failed to resolve tenant config, using base:', err);
+ }
+ }
+
+ if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
+ logger.warn(
+ `[requestPasswordReset] Tenant config blocked domain [Email: ${email}] [IP: ${req.ip}]`,
+ );
+ return {
+ message: 'If an account with that email exists, a password reset link has been sent to it.',
+ };
+ }
const emailEnabled = checkEmailConfig();
logger.warn(`[requestPasswordReset] [Password reset request initiated] [Email: ${email}]`);
diff --git a/api/server/services/AuthService.spec.js b/api/server/services/AuthService.spec.js
index da78f8d775..c8abafdbe5 100644
--- a/api/server/services/AuthService.spec.js
+++ b/api/server/services/AuthService.spec.js
@@ -14,6 +14,7 @@ jest.mock('@librechat/api', () => ({
isEmailDomainAllowed: jest.fn(),
math: jest.fn((val, fallback) => (val ? Number(val) : fallback)),
shouldUseSecureCookie: jest.fn(() => false),
+ resolveAppConfigForUser: jest.fn(async (_getAppConfig, _user) => ({})),
}));
jest.mock('~/models', () => ({
findUser: jest.fn(),
@@ -35,8 +36,14 @@ jest.mock('~/strategies/validators', () => ({ registerSchema: { parse: jest.fn()
jest.mock('~/server/services/Config', () => ({ getAppConfig: jest.fn() }));
jest.mock('~/server/utils', () => ({ sendEmail: jest.fn() }));
-const { shouldUseSecureCookie } = require('@librechat/api');
-const { setOpenIDAuthTokens } = require('./AuthService');
+const {
+ shouldUseSecureCookie,
+ isEmailDomainAllowed,
+ resolveAppConfigForUser,
+} = require('@librechat/api');
+const { findUser } = require('~/models');
+const { getAppConfig } = require('~/server/services/Config');
+const { setOpenIDAuthTokens, requestPasswordReset } = require('./AuthService');
/** Helper to build a mock Express response */
function mockResponse() {
@@ -267,3 +274,68 @@ describe('setOpenIDAuthTokens', () => {
});
});
});
+
+describe('requestPasswordReset', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ isEmailDomainAllowed.mockReturnValue(true);
+ getAppConfig.mockResolvedValue({
+ registration: { allowedDomains: ['example.com'] },
+ });
+ resolveAppConfigForUser.mockResolvedValue({
+ registration: { allowedDomains: ['example.com'] },
+ });
+ });
+
+ it('should fast-fail with base config before DB lookup for blocked domains', async () => {
+ isEmailDomainAllowed.mockReturnValue(false);
+
+ const req = { body: { email: 'blocked@evil.com' }, ip: '127.0.0.1' };
+ const result = await requestPasswordReset(req);
+
+ expect(getAppConfig).toHaveBeenCalledWith({ baseOnly: true });
+ expect(findUser).not.toHaveBeenCalled();
+ expect(result).toBeInstanceOf(Error);
+ });
+
+ it('should call resolveAppConfigForUser for tenant user', async () => {
+ const user = {
+ _id: 'user-tenant',
+ email: 'user@example.com',
+ tenantId: 'tenant-x',
+ role: 'USER',
+ };
+ findUser.mockResolvedValue(user);
+
+ const req = { body: { email: 'user@example.com' }, ip: '127.0.0.1' };
+ await requestPasswordReset(req);
+
+ expect(resolveAppConfigForUser).toHaveBeenCalledWith(getAppConfig, user);
+ });
+
+ it('should reuse baseConfig for non-tenant user without calling resolveAppConfigForUser', async () => {
+ findUser.mockResolvedValue({ _id: 'user-no-tenant', email: 'user@example.com' });
+
+ const req = { body: { email: 'user@example.com' }, ip: '127.0.0.1' };
+ await requestPasswordReset(req);
+
+ expect(resolveAppConfigForUser).not.toHaveBeenCalled();
+ });
+
+ it('should return generic response when tenant config blocks the domain (non-enumerable)', async () => {
+ const user = {
+ _id: 'user-tenant',
+ email: 'user@example.com',
+ tenantId: 'tenant-x',
+ role: 'USER',
+ };
+ findUser.mockResolvedValue(user);
+ isEmailDomainAllowed.mockReturnValueOnce(true).mockReturnValueOnce(false);
+
+ const req = { body: { email: 'user@example.com' }, ip: '127.0.0.1' };
+ const result = await requestPasswordReset(req);
+
+ expect(result).not.toBeInstanceOf(Error);
+ expect(result.message).toContain('If an account with that email exists');
+ });
+});
diff --git a/api/server/services/Config/__tests__/invalidateConfigCaches.spec.js b/api/server/services/Config/__tests__/invalidateConfigCaches.spec.js
new file mode 100644
index 0000000000..ddc97042b9
--- /dev/null
+++ b/api/server/services/Config/__tests__/invalidateConfigCaches.spec.js
@@ -0,0 +1,122 @@
+// ── Mocks ──────────────────────────────────────────────────────────────
+
+const mockClearAppConfigCache = jest.fn().mockResolvedValue(undefined);
+const mockClearOverrideCache = jest.fn().mockResolvedValue(undefined);
+
+jest.mock('~/cache/getLogStores', () => {
+ return jest.fn(() => ({}));
+});
+
+jest.mock('~/server/services/start/tools', () => ({
+ loadAndFormatTools: jest.fn(() => ({})),
+}));
+
+jest.mock('../loadCustomConfig', () => jest.fn().mockResolvedValue({}));
+
+jest.mock('@librechat/data-schemas', () => {
+ const actual = jest.requireActual('@librechat/data-schemas');
+ return { ...actual, AppService: jest.fn(() => ({ availableTools: {} })) };
+});
+
+jest.mock('~/models', () => ({
+ getApplicableConfigs: jest.fn().mockResolvedValue([]),
+ getUserPrincipals: jest.fn().mockResolvedValue([]),
+}));
+
+const mockInvalidateCachedTools = jest.fn().mockResolvedValue(undefined);
+jest.mock('../getCachedTools', () => ({
+ setCachedTools: jest.fn().mockResolvedValue(undefined),
+ invalidateCachedTools: mockInvalidateCachedTools,
+}));
+
+const mockClearMcpConfigCache = jest.fn().mockResolvedValue(undefined);
+jest.mock('@librechat/api', () => ({
+ createAppConfigService: jest.fn(() => ({
+ getAppConfig: jest.fn().mockResolvedValue({ availableTools: {} }),
+ clearAppConfigCache: mockClearAppConfigCache,
+ clearOverrideCache: mockClearOverrideCache,
+ })),
+ clearMcpConfigCache: mockClearMcpConfigCache,
+}));
+
+// ── Tests ──────────────────────────────────────────────────────────────
+
+const { invalidateConfigCaches } = require('../app');
+
+describe('invalidateConfigCaches', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('clears all caches', async () => {
+ await invalidateConfigCaches();
+
+ expect(mockClearAppConfigCache).toHaveBeenCalledTimes(1);
+ expect(mockClearOverrideCache).toHaveBeenCalledTimes(1);
+ expect(mockInvalidateCachedTools).toHaveBeenCalledWith({ invalidateGlobal: true });
+ expect(mockClearMcpConfigCache).toHaveBeenCalledTimes(1);
+ });
+
+ it('passes tenantId through to clearOverrideCache', async () => {
+ await invalidateConfigCaches('tenant-a');
+
+ expect(mockClearOverrideCache).toHaveBeenCalledWith('tenant-a');
+ expect(mockClearAppConfigCache).toHaveBeenCalledTimes(1);
+ expect(mockInvalidateCachedTools).toHaveBeenCalledWith({ invalidateGlobal: true });
+ });
+
+ it('all operations run in parallel (not sequentially)', async () => {
+ const order = [];
+
+ mockClearAppConfigCache.mockImplementation(
+ () =>
+ new Promise((r) =>
+ setTimeout(() => {
+ order.push('base');
+ r();
+ }, 10),
+ ),
+ );
+ mockClearOverrideCache.mockImplementation(
+ () =>
+ new Promise((r) =>
+ setTimeout(() => {
+ order.push('override');
+ r();
+ }, 10),
+ ),
+ );
+ mockInvalidateCachedTools.mockImplementation(
+ () =>
+ new Promise((r) =>
+ setTimeout(() => {
+ order.push('tools');
+ r();
+ }, 10),
+ ),
+ );
+ mockClearMcpConfigCache.mockImplementation(
+ () =>
+ new Promise((r) =>
+ setTimeout(() => {
+ order.push('mcp');
+ r();
+ }, 10),
+ ),
+ );
+
+ await invalidateConfigCaches();
+
+ expect(order).toHaveLength(4);
+ expect(new Set(order)).toEqual(new Set(['base', 'override', 'tools', 'mcp']));
+ });
+
+ it('resolves even when clearAppConfigCache throws (partial failure)', async () => {
+ mockClearAppConfigCache.mockRejectedValueOnce(new Error('cache connection lost'));
+
+ await expect(invalidateConfigCaches()).resolves.not.toThrow();
+
+ expect(mockClearOverrideCache).toHaveBeenCalledTimes(1);
+ expect(mockInvalidateCachedTools).toHaveBeenCalledWith({ invalidateGlobal: true });
+ });
+});
diff --git a/api/server/services/Config/app.js b/api/server/services/Config/app.js
index 75a5cbe56d..7aa913e636 100644
--- a/api/server/services/Config/app.js
+++ b/api/server/services/Config/app.js
@@ -1,12 +1,12 @@
const { CacheKeys } = require('librechat-data-provider');
-const { logger, AppService } = require('@librechat/data-schemas');
+const { AppService, logger } = require('@librechat/data-schemas');
+const { createAppConfigService, clearMcpConfigCache } = require('@librechat/api');
+const { setCachedTools, invalidateCachedTools } = require('./getCachedTools');
const { loadAndFormatTools } = require('~/server/services/start/tools');
const loadCustomConfig = require('./loadCustomConfig');
-const { setCachedTools } = require('./getCachedTools');
const getLogStores = require('~/cache/getLogStores');
const paths = require('~/config/paths');
-
-const BASE_CONFIG_KEY = '_BASE_';
+const db = require('~/models');
const loadBaseConfig = async () => {
/** @type {TCustomConfig} */
@@ -20,65 +20,43 @@ const loadBaseConfig = async () => {
return AppService({ config, paths, systemTools });
};
-/**
- * Get the app configuration based on user context
- * @param {Object} [options]
- * @param {string} [options.role] - User role for role-based config
- * @param {boolean} [options.refresh] - Force refresh the cache
- * @returns {Promise}
- */
-async function getAppConfig(options = {}) {
- const { role, refresh } = options;
-
- const cache = getLogStores(CacheKeys.APP_CONFIG);
- const cacheKey = role ? role : BASE_CONFIG_KEY;
-
- if (!refresh) {
- const cached = await cache.get(cacheKey);
- if (cached) {
- return cached;
- }
- }
-
- let baseConfig = await cache.get(BASE_CONFIG_KEY);
- if (!baseConfig) {
- logger.info('[getAppConfig] App configuration not initialized. Initializing AppService...');
- baseConfig = await loadBaseConfig();
-
- if (!baseConfig) {
- throw new Error('Failed to initialize app configuration through AppService.');
- }
-
- if (baseConfig.availableTools) {
- await setCachedTools(baseConfig.availableTools);
- }
-
- await cache.set(BASE_CONFIG_KEY, baseConfig);
- }
-
- // For now, return the base config
- // In the future, this is where we'll apply role-based modifications
- if (role) {
- // TODO: Apply role-based config modifications
- // const roleConfig = await applyRoleBasedConfig(baseConfig, role);
- // await cache.set(cacheKey, roleConfig);
- // return roleConfig;
- }
-
- return baseConfig;
-}
+const { getAppConfig, clearAppConfigCache, clearOverrideCache } = createAppConfigService({
+ loadBaseConfig,
+ setCachedTools,
+ getCache: getLogStores,
+ cacheKeys: CacheKeys,
+ getApplicableConfigs: db.getApplicableConfigs,
+ getUserPrincipals: db.getUserPrincipals,
+});
/**
- * Clear the app configuration cache
- * @returns {Promise}
+ * Invalidate all config-related caches after an admin config mutation.
+ * Clears the base config, per-principal override caches, tool caches,
+ * and the MCP config-source server cache.
+ * @param {string} [tenantId] - Optional tenant ID to scope override cache clearing.
*/
-async function clearAppConfigCache() {
- const cache = getLogStores(CacheKeys.CONFIG_STORE);
- const cacheKey = CacheKeys.APP_CONFIG;
- return await cache.delete(cacheKey);
+async function invalidateConfigCaches(tenantId) {
+ const results = await Promise.allSettled([
+ clearAppConfigCache(),
+ clearOverrideCache(tenantId),
+ invalidateCachedTools({ invalidateGlobal: true }),
+ clearMcpConfigCache(),
+ ]);
+ const labels = [
+ 'clearAppConfigCache',
+ 'clearOverrideCache',
+ 'invalidateCachedTools',
+ 'clearMcpConfigCache',
+ ];
+ for (let i = 0; i < results.length; i++) {
+ if (results[i].status === 'rejected') {
+ logger.error(`[invalidateConfigCaches] ${labels[i]} failed:`, results[i].reason);
+ }
+ }
}
module.exports = {
getAppConfig,
clearAppConfigCache,
+ invalidateConfigCaches,
};
diff --git a/api/server/services/Config/getEndpointsConfig.js b/api/server/services/Config/getEndpointsConfig.js
index bb22584851..d09b45626c 100644
--- a/api/server/services/Config/getEndpointsConfig.js
+++ b/api/server/services/Config/getEndpointsConfig.js
@@ -1,133 +1,10 @@
-const { loadCustomEndpointsConfig } = require('@librechat/api');
-const {
- CacheKeys,
- EModelEndpoint,
- isAgentsEndpoint,
- orderEndpointsConfig,
- defaultAgentCapabilities,
-} = require('librechat-data-provider');
+const { createEndpointsConfigService } = require('@librechat/api');
const loadDefaultEndpointsConfig = require('./loadDefaultEConfig');
-const getLogStores = require('~/cache/getLogStores');
const { getAppConfig } = require('./app');
-/**
- *
- * @param {ServerRequest} req
- * @returns {Promise}
- */
-async function getEndpointsConfig(req) {
- const cache = getLogStores(CacheKeys.CONFIG_STORE);
- const cachedEndpointsConfig = await cache.get(CacheKeys.ENDPOINT_CONFIG);
- if (cachedEndpointsConfig) {
- if (cachedEndpointsConfig.gptPlugins) {
- await cache.delete(CacheKeys.ENDPOINT_CONFIG);
- } else {
- return cachedEndpointsConfig;
- }
- }
-
- const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role }));
- const defaultEndpointsConfig = await loadDefaultEndpointsConfig(appConfig);
- const customEndpointsConfig = loadCustomEndpointsConfig(appConfig?.endpoints?.custom);
-
- /** @type {TEndpointsConfig} */
- const mergedConfig = {
- ...defaultEndpointsConfig,
- ...customEndpointsConfig,
- };
-
- if (appConfig.endpoints?.[EModelEndpoint.azureOpenAI]) {
- /** @type {Omit} */
- mergedConfig[EModelEndpoint.azureOpenAI] = {
- userProvide: false,
- };
- }
-
- // Enable Anthropic endpoint when Vertex AI is configured in YAML
- if (appConfig.endpoints?.[EModelEndpoint.anthropic]?.vertexConfig?.enabled) {
- /** @type {Omit} */
- mergedConfig[EModelEndpoint.anthropic] = {
- userProvide: false,
- };
- }
-
- if (appConfig.endpoints?.[EModelEndpoint.azureOpenAI]?.assistants) {
- /** @type {Omit} */
- mergedConfig[EModelEndpoint.azureAssistants] = {
- userProvide: false,
- };
- }
-
- if (
- mergedConfig[EModelEndpoint.assistants] &&
- appConfig?.endpoints?.[EModelEndpoint.assistants]
- ) {
- const { disableBuilder, retrievalModels, capabilities, version, ..._rest } =
- appConfig.endpoints[EModelEndpoint.assistants];
-
- mergedConfig[EModelEndpoint.assistants] = {
- ...mergedConfig[EModelEndpoint.assistants],
- version,
- retrievalModels,
- disableBuilder,
- capabilities,
- };
- }
- if (mergedConfig[EModelEndpoint.agents] && appConfig?.endpoints?.[EModelEndpoint.agents]) {
- const { disableBuilder, capabilities, allowedProviders, ..._rest } =
- appConfig.endpoints[EModelEndpoint.agents];
-
- mergedConfig[EModelEndpoint.agents] = {
- ...mergedConfig[EModelEndpoint.agents],
- allowedProviders,
- disableBuilder,
- capabilities,
- };
- }
-
- if (
- mergedConfig[EModelEndpoint.azureAssistants] &&
- appConfig?.endpoints?.[EModelEndpoint.azureAssistants]
- ) {
- const { disableBuilder, retrievalModels, capabilities, version, ..._rest } =
- appConfig.endpoints[EModelEndpoint.azureAssistants];
-
- mergedConfig[EModelEndpoint.azureAssistants] = {
- ...mergedConfig[EModelEndpoint.azureAssistants],
- version,
- retrievalModels,
- disableBuilder,
- capabilities,
- };
- }
-
- if (mergedConfig[EModelEndpoint.bedrock] && appConfig?.endpoints?.[EModelEndpoint.bedrock]) {
- const { availableRegions } = appConfig.endpoints[EModelEndpoint.bedrock];
- mergedConfig[EModelEndpoint.bedrock] = {
- ...mergedConfig[EModelEndpoint.bedrock],
- availableRegions,
- };
- }
-
- const endpointsConfig = orderEndpointsConfig(mergedConfig);
-
- await cache.set(CacheKeys.ENDPOINT_CONFIG, endpointsConfig);
- return endpointsConfig;
-}
-
-/**
- * @param {ServerRequest} req
- * @param {import('librechat-data-provider').AgentCapabilities} capability
- * @returns {Promise}
- */
-const checkCapability = async (req, capability) => {
- const isAgents = isAgentsEndpoint(req.body?.endpointType || req.body?.endpoint);
- const endpointsConfig = await getEndpointsConfig(req);
- const capabilities =
- isAgents || endpointsConfig?.[EModelEndpoint.agents]?.capabilities != null
- ? (endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? [])
- : defaultAgentCapabilities;
- return capabilities.includes(capability);
-};
+const { getEndpointsConfig, checkCapability } = createEndpointsConfigService({
+ getAppConfig,
+ loadDefaultEndpointsConfig,
+});
module.exports = { getEndpointsConfig, checkCapability };
diff --git a/api/server/services/Config/loadConfigModels.js b/api/server/services/Config/loadConfigModels.js
index 2bc83ecc3a..93212cd030 100644
--- a/api/server/services/Config/loadConfigModels.js
+++ b/api/server/services/Config/loadConfigModels.js
@@ -1,117 +1,11 @@
-const { isUserProvided, fetchModels } = require('@librechat/api');
-const {
- EModelEndpoint,
- extractEnvVariable,
- normalizeEndpointName,
-} = require('librechat-data-provider');
+const { createLoadConfigModels, fetchModels } = require('@librechat/api');
const { getAppConfig } = require('./app');
+const db = require('~/models');
-/**
- * Load config endpoints from the cached configuration object
- * @function loadConfigModels
- * @param {ServerRequest} req - The Express request object.
- */
-async function loadConfigModels(req) {
- const appConfig = await getAppConfig({ role: req.user?.role });
- if (!appConfig) {
- return {};
- }
- const modelsConfig = {};
- const azureConfig = appConfig.endpoints?.[EModelEndpoint.azureOpenAI];
- const { modelNames } = azureConfig ?? {};
-
- if (modelNames && azureConfig) {
- modelsConfig[EModelEndpoint.azureOpenAI] = modelNames;
- }
-
- if (azureConfig?.assistants && azureConfig.assistantModels) {
- modelsConfig[EModelEndpoint.azureAssistants] = azureConfig.assistantModels;
- }
-
- const bedrockConfig = appConfig.endpoints?.[EModelEndpoint.bedrock];
- if (bedrockConfig?.models && Array.isArray(bedrockConfig.models)) {
- modelsConfig[EModelEndpoint.bedrock] = bedrockConfig.models;
- }
-
- if (!Array.isArray(appConfig.endpoints?.[EModelEndpoint.custom])) {
- return modelsConfig;
- }
-
- const customEndpoints = appConfig.endpoints[EModelEndpoint.custom].filter(
- (endpoint) =>
- endpoint.baseURL &&
- endpoint.apiKey &&
- endpoint.name &&
- endpoint.models &&
- (endpoint.models.fetch || endpoint.models.default),
- );
-
- /**
- * @type {Record>}
- * Map for promises keyed by unique combination of baseURL and apiKey */
- const fetchPromisesMap = {};
- /**
- * @type {Record}
- * Map to associate unique keys with endpoint names; note: one key may can correspond to multiple endpoints */
- const uniqueKeyToEndpointsMap = {};
- /**
- * @type {Record>}
- * Map to associate endpoint names to their configurations */
- const endpointsMap = {};
-
- for (let i = 0; i < customEndpoints.length; i++) {
- const endpoint = customEndpoints[i];
- const { models, name: configName, baseURL, apiKey, headers: endpointHeaders } = endpoint;
- const name = normalizeEndpointName(configName);
- endpointsMap[name] = endpoint;
-
- const API_KEY = extractEnvVariable(apiKey);
- const BASE_URL = extractEnvVariable(baseURL);
-
- const uniqueKey = `${BASE_URL}__${API_KEY}`;
-
- modelsConfig[name] = [];
-
- if (models.fetch && !isUserProvided(API_KEY) && !isUserProvided(BASE_URL)) {
- fetchPromisesMap[uniqueKey] =
- fetchPromisesMap[uniqueKey] ||
- fetchModels({
- name,
- apiKey: API_KEY,
- baseURL: BASE_URL,
- user: req.user.id,
- userObject: req.user,
- headers: endpointHeaders,
- direct: endpoint.directEndpoint,
- userIdQuery: models.userIdQuery,
- });
- uniqueKeyToEndpointsMap[uniqueKey] = uniqueKeyToEndpointsMap[uniqueKey] || [];
- uniqueKeyToEndpointsMap[uniqueKey].push(name);
- continue;
- }
-
- if (Array.isArray(models.default)) {
- modelsConfig[name] = models.default.map((model) =>
- typeof model === 'string' ? model : model.name,
- );
- }
- }
-
- const fetchedData = await Promise.all(Object.values(fetchPromisesMap));
- const uniqueKeys = Object.keys(fetchPromisesMap);
-
- for (let i = 0; i < fetchedData.length; i++) {
- const currentKey = uniqueKeys[i];
- const modelData = fetchedData[i];
- const associatedNames = uniqueKeyToEndpointsMap[currentKey];
-
- for (const name of associatedNames) {
- const endpoint = endpointsMap[name];
- modelsConfig[name] = !modelData?.length ? (endpoint.models.default ?? []) : modelData;
- }
- }
-
- return modelsConfig;
-}
+const loadConfigModels = createLoadConfigModels({
+ getAppConfig,
+ getUserKeyValues: db.getUserKeyValues,
+ fetchModels,
+});
module.exports = loadConfigModels;
diff --git a/api/server/services/Config/loadConfigModels.spec.js b/api/server/services/Config/loadConfigModels.spec.js
index 6ffb8ba522..d3ec0309ae 100644
--- a/api/server/services/Config/loadConfigModels.spec.js
+++ b/api/server/services/Config/loadConfigModels.spec.js
@@ -7,6 +7,13 @@ jest.mock('@librechat/api', () => ({
fetchModels: jest.fn(),
}));
jest.mock('./app');
+jest.mock('@librechat/data-schemas', () => ({
+ ...jest.requireActual('@librechat/data-schemas'),
+ logger: { debug: jest.fn(), error: jest.fn(), warn: jest.fn() },
+}));
+jest.mock('~/models', () => ({
+ getUserKeyValues: jest.fn(),
+}));
const exampleConfig = {
endpoints: {
@@ -68,11 +75,11 @@ describe('loadConfigModels', () => {
const originalEnv = process.env;
beforeEach(() => {
- jest.resetAllMocks();
- jest.resetModules();
+ jest.clearAllMocks();
+ fetchModels.mockReset();
+ require('~/models').getUserKeyValues.mockReset();
process.env = { ...originalEnv };
- // Default mock for getAppConfig
getAppConfig.mockResolvedValue({});
});
@@ -337,6 +344,168 @@ describe('loadConfigModels', () => {
expect(result.FalsyFetchModel).toEqual(['defaultModel1', 'defaultModel2']);
});
+ describe('user-provided API key model fetching', () => {
+ it('fetches models using user-provided API key when key is stored', async () => {
+ const { getUserKeyValues } = require('~/models');
+ getUserKeyValues.mockResolvedValueOnce({
+ apiKey: 'sk-user-key',
+ baseURL: 'https://api.x.com/v1',
+ });
+ getAppConfig.mockResolvedValue({
+ endpoints: {
+ custom: [
+ {
+ name: 'UserEndpoint',
+ apiKey: 'user_provided',
+ baseURL: 'user_provided',
+ models: { fetch: true, default: ['fallback-model'] },
+ },
+ ],
+ },
+ });
+ fetchModels.mockResolvedValue(['fetched-model-a', 'fetched-model-b']);
+
+ const result = await loadConfigModels(mockRequest);
+
+ expect(getUserKeyValues).toHaveBeenCalledWith({ userId: 'testUserId', name: 'UserEndpoint' });
+ expect(fetchModels).toHaveBeenCalledWith(
+ expect.objectContaining({
+ apiKey: 'sk-user-key',
+ baseURL: 'https://api.x.com/v1',
+ skipCache: true,
+ }),
+ );
+ expect(result.UserEndpoint).toEqual(['fetched-model-a', 'fetched-model-b']);
+ });
+
+ it('falls back to defaults when getUserKeyValues returns no apiKey', async () => {
+ const { getUserKeyValues } = require('~/models');
+ getUserKeyValues.mockResolvedValueOnce({ baseURL: 'https://api.x.com/v1' });
+ getAppConfig.mockResolvedValue({
+ endpoints: {
+ custom: [
+ {
+ name: 'NoKeyEndpoint',
+ apiKey: 'user_provided',
+ baseURL: 'https://api.x.com/v1',
+ models: { fetch: true, default: ['default-model'] },
+ },
+ ],
+ },
+ });
+
+ const result = await loadConfigModels(mockRequest);
+
+ expect(fetchModels).not.toHaveBeenCalled();
+ expect(result.NoKeyEndpoint).toEqual(['default-model']);
+ });
+
+ it('falls back to defaults and logs warn when getUserKeyValues throws infra error', async () => {
+ const { getUserKeyValues } = require('~/models');
+ const { logger } = require('@librechat/data-schemas');
+ getUserKeyValues.mockRejectedValueOnce(new Error('DB connection timeout'));
+ getAppConfig.mockResolvedValue({
+ endpoints: {
+ custom: [
+ {
+ name: 'ErrorEndpoint',
+ apiKey: 'user_provided',
+ baseURL: 'https://api.example.com/v1',
+ models: { fetch: true, default: ['fallback'] },
+ },
+ ],
+ },
+ });
+
+ const result = await loadConfigModels(mockRequest);
+
+ expect(fetchModels).not.toHaveBeenCalled();
+ expect(result.ErrorEndpoint).toEqual(['fallback']);
+ expect(logger.warn).toHaveBeenCalledWith(
+ expect.stringContaining(
+ 'Failed to retrieve user key for "ErrorEndpoint": DB connection timeout',
+ ),
+ );
+ expect(logger.debug).not.toHaveBeenCalledWith(expect.stringContaining('No user key stored'));
+ });
+
+ it('logs debug (not warn) for NO_USER_KEY errors', async () => {
+ const { getUserKeyValues } = require('~/models');
+ const { logger } = require('@librechat/data-schemas');
+ getUserKeyValues.mockRejectedValueOnce(new Error(JSON.stringify({ type: 'no_user_key' })));
+ getAppConfig.mockResolvedValue({
+ endpoints: {
+ custom: [
+ {
+ name: 'MissingKeyEndpoint',
+ apiKey: 'user_provided',
+ baseURL: 'https://api.example.com/v1',
+ models: { fetch: true, default: ['default-model'] },
+ },
+ ],
+ },
+ });
+
+ const result = await loadConfigModels(mockRequest);
+
+ expect(result.MissingKeyEndpoint).toEqual(['default-model']);
+ expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('No user key stored'));
+ expect(logger.warn).not.toHaveBeenCalledWith(
+ expect.stringContaining('Failed to retrieve user key'),
+ );
+ });
+
+ it('skips user key lookup when req.user.id is undefined', async () => {
+ const { getUserKeyValues } = require('~/models');
+ getAppConfig.mockResolvedValue({
+ endpoints: {
+ custom: [
+ {
+ name: 'NoUserEndpoint',
+ apiKey: 'user_provided',
+ baseURL: 'https://api.x.com/v1',
+ models: { fetch: true, default: ['anon-model'] },
+ },
+ ],
+ },
+ });
+
+ const result = await loadConfigModels({ user: {} });
+
+ expect(getUserKeyValues).not.toHaveBeenCalled();
+ expect(result.NoUserEndpoint).toEqual(['anon-model']);
+ });
+
+ it('uses stored baseURL only when baseURL is user_provided', async () => {
+ const { getUserKeyValues } = require('~/models');
+ getUserKeyValues.mockResolvedValueOnce({ apiKey: 'sk-key' });
+ getAppConfig.mockResolvedValue({
+ endpoints: {
+ custom: [
+ {
+ name: 'KeyOnly',
+ apiKey: 'user_provided',
+ baseURL: 'https://fixed-base.com/v1',
+ models: { fetch: true, default: ['default'] },
+ },
+ ],
+ },
+ });
+ fetchModels.mockResolvedValue(['model-from-fixed-base']);
+
+ const result = await loadConfigModels(mockRequest);
+
+ expect(fetchModels).toHaveBeenCalledWith(
+ expect.objectContaining({
+ apiKey: 'sk-key',
+ baseURL: 'https://fixed-base.com/v1',
+ skipCache: true,
+ }),
+ );
+ expect(result.KeyOnly).toEqual(['model-from-fixed-base']);
+ });
+ });
+
it('normalizes Ollama endpoint name to lowercase', async () => {
const testCases = [
{
diff --git a/api/server/services/Config/loadDefaultModels.js b/api/server/services/Config/loadDefaultModels.js
index 31aa831a70..85f2c42a33 100644
--- a/api/server/services/Config/loadDefaultModels.js
+++ b/api/server/services/Config/loadDefaultModels.js
@@ -16,7 +16,8 @@ const { getAppConfig } = require('./app');
*/
async function loadDefaultModels(req) {
try {
- const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role }));
+ const appConfig =
+ req.config ?? (await getAppConfig({ role: req.user?.role, tenantId: req.user?.tenantId }));
const vertexConfig = appConfig?.endpoints?.[EModelEndpoint.anthropic]?.vertexConfig;
const [openAI, anthropic, azureOpenAI, assistants, azureAssistants, google, bedrock] =
diff --git a/api/server/services/Config/mcp.js b/api/server/services/Config/mcp.js
index cc4e98b59e..fa37e223f5 100644
--- a/api/server/services/Config/mcp.js
+++ b/api/server/services/Config/mcp.js
@@ -1,97 +1,10 @@
-const { logger } = require('@librechat/data-schemas');
-const { CacheKeys, Constants } = require('librechat-data-provider');
+const { createMCPToolCacheService } = require('@librechat/api');
const { getCachedTools, setCachedTools } = require('./getCachedTools');
-const { getLogStores } = require('~/cache');
-/**
- * Updates MCP tools in the cache for a specific server
- * @param {Object} params - Parameters for updating MCP tools
- * @param {string} params.userId - User ID for user-specific caching
- * @param {string} params.serverName - MCP server name
- * @param {Array} params.tools - Array of tool objects from MCP server
- * @returns {Promise}
- */
-async function updateMCPServerTools({ userId, serverName, tools }) {
- try {
- const serverTools = {};
- const mcpDelimiter = Constants.mcp_delimiter;
-
- if (tools == null || tools.length === 0) {
- logger.debug(`[MCP Cache] No tools to update for server ${serverName} (user: ${userId})`);
- return serverTools;
- }
-
- for (const tool of tools) {
- const name = `${tool.name}${mcpDelimiter}${serverName}`;
- serverTools[name] = {
- type: 'function',
- ['function']: {
- name,
- description: tool.description,
- parameters: tool.inputSchema,
- },
- };
- }
-
- await setCachedTools(serverTools, { userId, serverName });
-
- const cache = getLogStores(CacheKeys.TOOL_CACHE);
- await cache.delete(CacheKeys.TOOLS);
- logger.debug(
- `[MCP Cache] Updated ${tools.length} tools for server ${serverName} (user: ${userId})`,
- );
- return serverTools;
- } catch (error) {
- logger.error(`[MCP Cache] Failed to update tools for ${serverName} (user: ${userId}):`, error);
- throw error;
- }
-}
-
-/**
- * Merges app-level tools with global tools
- * @param {import('@librechat/api').LCAvailableTools} appTools
- * @returns {Promise}
- */
-async function mergeAppTools(appTools) {
- try {
- const count = Object.keys(appTools).length;
- if (!count) {
- return;
- }
- const cachedTools = await getCachedTools();
- const mergedTools = { ...cachedTools, ...appTools };
- await setCachedTools(mergedTools);
- const cache = getLogStores(CacheKeys.TOOL_CACHE);
- await cache.delete(CacheKeys.TOOLS);
- logger.debug(`Merged ${count} app-level tools`);
- } catch (error) {
- logger.error('Failed to merge app-level tools:', error);
- throw error;
- }
-}
-
-/**
- * Caches MCP server tools (no longer merges with global)
- * @param {object} params
- * @param {string} params.userId - User ID for user-specific caching
- * @param {string} params.serverName
- * @param {import('@librechat/api').LCAvailableTools} params.serverTools
- * @returns {Promise}
- */
-async function cacheMCPServerTools({ userId, serverName, serverTools }) {
- try {
- const count = Object.keys(serverTools).length;
- if (!count) {
- return;
- }
- // Only cache server-specific tools, no merging with global
- await setCachedTools(serverTools, { userId, serverName });
- logger.debug(`Cached ${count} MCP server tools for ${serverName} (user: ${userId})`);
- } catch (error) {
- logger.error(`Failed to cache MCP server tools for ${serverName} (user: ${userId}):`, error);
- throw error;
- }
-}
+const { mergeAppTools, cacheMCPServerTools, updateMCPServerTools } = createMCPToolCacheService({
+ getCachedTools,
+ setCachedTools,
+});
module.exports = {
mergeAppTools,
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..69767e191c 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');
@@ -84,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');
@@ -138,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,
@@ -196,10 +206,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 +237,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 +287,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/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/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/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/api/server/services/Files/Audio/STTService.js b/api/server/services/Files/Audio/STTService.js
index 4ba62a7eeb..c9a35c35ea 100644
--- a/api/server/services/Files/Audio/STTService.js
+++ b/api/server/services/Files/Audio/STTService.js
@@ -142,6 +142,7 @@ class STTService {
req.config ??
(await getAppConfig({
role: req?.user?.role,
+ tenantId: req?.user?.tenantId,
}));
const sttSchema = appConfig?.speech?.stt;
if (!sttSchema) {
diff --git a/api/server/services/Files/Audio/TTSService.js b/api/server/services/Files/Audio/TTSService.js
index 2c932968c6..1125dd74ed 100644
--- a/api/server/services/Files/Audio/TTSService.js
+++ b/api/server/services/Files/Audio/TTSService.js
@@ -297,6 +297,7 @@ class TTSService {
req.config ??
(await getAppConfig({
role: req.user?.role,
+ tenantId: req.user?.tenantId,
}));
try {
res.setHeader('Content-Type', 'audio/mpeg');
@@ -365,6 +366,7 @@ class TTSService {
req.config ??
(await getAppConfig({
role: req.user?.role,
+ tenantId: req.user?.tenantId,
}));
const provider = this.getProvider(appConfig);
const ttsSchema = appConfig?.speech?.tts?.[provider];
diff --git a/api/server/services/Files/Audio/getCustomConfigSpeech.js b/api/server/services/Files/Audio/getCustomConfigSpeech.js
index d0d0b51ac2..b438771ec1 100644
--- a/api/server/services/Files/Audio/getCustomConfigSpeech.js
+++ b/api/server/services/Files/Audio/getCustomConfigSpeech.js
@@ -17,6 +17,7 @@ async function getCustomConfigSpeech(req, res) {
try {
const appConfig = await getAppConfig({
role: req.user?.role,
+ tenantId: req.user?.tenantId,
});
if (!appConfig) {
diff --git a/api/server/services/Files/Audio/getVoices.js b/api/server/services/Files/Audio/getVoices.js
index f2f8e100c3..22bd7cea6e 100644
--- a/api/server/services/Files/Audio/getVoices.js
+++ b/api/server/services/Files/Audio/getVoices.js
@@ -18,6 +18,7 @@ async function getVoices(req, res) {
req.config ??
(await getAppConfig({
role: req.user?.role,
+ tenantId: req.user?.tenantId,
}));
const ttsSchema = appConfig?.speech?.tts;
diff --git a/api/server/services/Files/Audio/streamAudio.js b/api/server/services/Files/Audio/streamAudio.js
index a1d7c7a649..7120399b5e 100644
--- a/api/server/services/Files/Audio/streamAudio.js
+++ b/api/server/services/Files/Audio/streamAudio.js
@@ -1,3 +1,4 @@
+const { scopedCacheKey } = require('@librechat/data-schemas');
const {
Time,
CacheKeys,
@@ -5,8 +6,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
@@ -67,6 +68,8 @@ function createChunkProcessor(user, messageId) {
}
const messageCache = getLogStores(CacheKeys.MESSAGES);
+ // Captured at creation time — must be called within an active request ALS scope
+ const cacheKey = scopedCacheKey(messageId);
/**
* @returns {Promise<{ text: string, isFinished: boolean }[] | string>}
@@ -81,7 +84,7 @@ function createChunkProcessor(user, messageId) {
}
/** @type { string | { text: string; complete: boolean } } */
- let message = await messageCache.get(messageId);
+ let message = await messageCache.get(cacheKey);
if (!message) {
message = await getMessage({ user, messageId });
}
@@ -92,7 +95,7 @@ function createChunkProcessor(user, messageId) {
} else {
const text = message.content?.length > 0 ? parseTextParts(message.content) : message.text;
messageCache.set(
- messageId,
+ cacheKey,
{
text,
complete: true,
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..a1d9322467 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
@@ -48,7 +47,10 @@ async function processFileCitations({ user, appConfig, toolArtifact, toolCallId,
logger.error(
`[processFileCitations] Permission check failed for FILE_CITATIONS: ${error.message}`,
);
- logger.debug(`[processFileCitations] Proceeding with citations due to permission error`);
+ logger.warn(
+ '[processFileCitations] Returning null citations due to permission check error — citations will not be shown for this message',
+ );
+ return null;
}
}
@@ -127,7 +129,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;
@@ -146,6 +148,8 @@ async function enhanceSourcesWithMetadata(sources, appConfig) {
metadata: {
...source.metadata,
storageType: configuredStorageType,
+ fileType: fileRecord.type || undefined,
+ fileBytes: fileRecord.bytes || undefined,
},
};
});
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/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/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/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';
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/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/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/server/services/MCP.js b/api/server/services/MCP.js
index c66eb0b6ef..dbb44740a9 100644
--- a/api/server/services/MCP.js
+++ b/api/server/services/MCP.js
@@ -1,5 +1,5 @@
const { tool } = require('@langchain/core/tools');
-const { logger } = require('@librechat/data-schemas');
+const { logger, getTenantId } = require('@librechat/data-schemas');
const {
Providers,
StepTypes,
@@ -14,14 +14,9 @@ const {
normalizeJsonSchema,
GenerationJobManager,
resolveJsonSchemaRefs,
+ buildOAuthToolCallName,
} = 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,
@@ -59,6 +54,53 @@ function evictStale(map, ttl) {
const unavailableMsg =
"This tool's MCP server is temporarily unavailable. Please try again shortly.";
+/**
+ * Resolves config-source MCP servers from admin Config overrides for the current
+ * request context. Returns the parsed configs keyed by server name.
+ * @param {import('express').Request} req - Express request with user context
+ * @returns {Promise>}
+ */
+async function resolveConfigServers(req) {
+ try {
+ const registry = getMCPServersRegistry();
+ const user = req?.user;
+ const appConfig = await getAppConfig({
+ role: user?.role,
+ tenantId: getTenantId(),
+ userId: user?.id,
+ });
+ return await registry.ensureConfigServers(appConfig?.mcpConfig || {});
+ } catch (error) {
+ logger.warn(
+ '[resolveConfigServers] Failed to resolve config servers, degrading to empty:',
+ error,
+ );
+ return {};
+ }
+}
+
+/**
+ * Resolves config-source servers and merges all server configs (YAML + config + user DB)
+ * for the given user context. Shared helper for controllers needing the full merged config.
+ * @param {string} userId
+ * @param {{ id?: string, role?: string }} [user]
+ * @returns {Promise>}
+ */
+async function resolveAllMcpConfigs(userId, user) {
+ const registry = getMCPServersRegistry();
+ const appConfig = await getAppConfig({ role: user?.role, tenantId: getTenantId(), userId });
+ let configServers = {};
+ try {
+ configServers = await registry.ensureConfigServers(appConfig?.mcpConfig || {});
+ } catch (error) {
+ logger.warn(
+ '[resolveAllMcpConfigs] Config server resolution failed, continuing without:',
+ error,
+ );
+ }
+ return await registry.getAllServerConfigs(userId, configServers);
+}
+
/**
* @param {string} toolName
* @param {string} serverName
@@ -254,6 +296,7 @@ async function reconnectServer({
index,
signal,
serverName,
+ configServers,
userMCPAuthMap,
streamId = null,
}) {
@@ -277,7 +320,7 @@ async function reconnectServer({
const stepId = 'step_oauth_login_' + serverName;
const toolCall = {
id: flowId,
- name: serverName,
+ name: buildOAuthToolCallName(serverName),
type: 'tool_call_chunk',
};
@@ -322,6 +365,7 @@ async function reconnectServer({
user,
signal,
serverName,
+ configServers,
oauthStart,
flowManager,
userMCPAuthMap,
@@ -364,15 +408,14 @@ async function createMCPTools({
config,
provider,
serverName,
+ configServers,
userMCPAuthMap,
streamId = null,
}) {
- // Early domain validation before reconnecting server (avoid wasted work on disallowed domains)
- // Use getAppConfig() to support per-user/role domain restrictions
const serverConfig =
- config ?? (await getMCPServersRegistry().getServerConfig(serverName, user?.id));
+ config ?? (await getMCPServersRegistry().getServerConfig(serverName, user?.id, configServers));
if (serverConfig?.url) {
- const appConfig = await getAppConfig({ role: user?.role });
+ const appConfig = await getAppConfig({ role: user?.role, tenantId: user?.tenantId });
const allowedDomains = appConfig?.mcpSettings?.allowedDomains;
const isDomainAllowed = await isMCPDomainAllowed(serverConfig, allowedDomains);
if (!isDomainAllowed) {
@@ -387,6 +430,7 @@ async function createMCPTools({
index,
signal,
serverName,
+ configServers,
userMCPAuthMap,
streamId,
});
@@ -406,6 +450,7 @@ async function createMCPTools({
user,
provider,
userMCPAuthMap,
+ configServers,
streamId,
availableTools: result.availableTools,
toolKey: `${tool.name}${Constants.mcp_delimiter}${serverName}`,
@@ -445,16 +490,15 @@ async function createMCPTool({
userMCPAuthMap,
availableTools,
config,
+ configServers,
streamId = null,
}) {
const [toolName, serverName] = toolKey.split(Constants.mcp_delimiter);
- // Runtime domain validation: check if the server's domain is still allowed
- // Use getAppConfig() to support per-user/role domain restrictions
const serverConfig =
- config ?? (await getMCPServersRegistry().getServerConfig(serverName, user?.id));
+ config ?? (await getMCPServersRegistry().getServerConfig(serverName, user?.id, configServers));
if (serverConfig?.url) {
- const appConfig = await getAppConfig({ role: user?.role });
+ const appConfig = await getAppConfig({ role: user?.role, tenantId: user?.tenantId });
const allowedDomains = appConfig?.mcpSettings?.allowedDomains;
const isDomainAllowed = await isMCPDomainAllowed(serverConfig, allowedDomains);
if (!isDomainAllowed) {
@@ -483,6 +527,7 @@ async function createMCPTool({
index,
signal,
serverName,
+ configServers,
userMCPAuthMap,
streamId,
});
@@ -506,6 +551,7 @@ async function createMCPTool({
provider,
toolName,
serverName,
+ serverConfig,
toolDefinition,
streamId,
});
@@ -515,13 +561,14 @@ function createToolInstance({
res,
toolName,
serverName,
+ serverConfig: capturedServerConfig,
toolDefinition,
- provider: _provider,
+ provider: capturedProvider,
streamId = null,
}) {
/** @type {LCTool} */
const { description, parameters } = toolDefinition;
- const isGoogle = _provider === Providers.VERTEXAI || _provider === Providers.GOOGLE;
+ const isGoogle = capturedProvider === Providers.VERTEXAI || capturedProvider === Providers.GOOGLE;
let schema = parameters ? normalizeJsonSchema(resolveJsonSchemaRefs(parameters)) : null;
@@ -550,7 +597,7 @@ function createToolInstance({
const flowManager = getFlowStateManager(flowsCache);
derivedSignal = config?.signal ? AbortSignal.any([config.signal]) : undefined;
const mcpManager = getMCPManager(userId);
- const provider = (config?.metadata?.provider || _provider)?.toLowerCase();
+ const provider = (config?.metadata?.provider || capturedProvider)?.toLowerCase();
const { args: _args, stepId, ...toolCall } = config.toolCall ?? {};
const flowId = `${serverName}:oauth_login:${config.metadata.thread_id}:${config.metadata.run_id}`;
@@ -582,6 +629,7 @@ function createToolInstance({
const result = await mcpManager.callTool({
serverName,
+ serverConfig: capturedServerConfig,
toolName,
provider,
toolArguments,
@@ -605,9 +653,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(
@@ -652,30 +697,36 @@ function createToolInstance({
}
/**
- * Get MCP setup data including config, connections, and OAuth servers
+ * Get MCP setup data including config, connections, and OAuth servers.
+ * Resolves config-source servers from admin Config overrides when tenant context is available.
* @param {string} userId - The user ID
+ * @param {{ role?: string, tenantId?: string }} [options] - Optional role/tenant context
* @returns {Object} Object containing mcpConfig, appConnections, userConnections, and oauthServers
*/
-async function getMCPSetupData(userId) {
- const mcpConfig = await getMCPServersRegistry().getAllServerConfigs(userId);
-
- if (!mcpConfig) {
- throw new Error('MCP config not found');
- }
+async function getMCPSetupData(userId, options = {}) {
+ const registry = getMCPServersRegistry();
+ const { role, tenantId } = options;
+ const appConfig = await getAppConfig({ role, tenantId, userId });
+ const configServers = await registry.ensureConfigServers(appConfig?.mcpConfig || {});
+ const mcpConfig = await registry.getAllServerConfigs(userId, configServers);
const mcpManager = getMCPManager(userId);
/** @type {Map} */
let appConnections = new Map();
try {
- // Use getLoaded() instead of getAll() to avoid forcing connection creation
+ // Use getLoaded() instead of getAll() to avoid forcing connection creation.
// getAll() creates connections for all servers, which is problematic for servers
- // that require user context (e.g., those with {{LIBRECHAT_USER_ID}} placeholders)
+ // that require user context (e.g., those with {{LIBRECHAT_USER_ID}} placeholders).
appConnections = (await mcpManager.appConnections?.getLoaded()) || new Map();
} catch (error) {
logger.error(`[MCP][User: ${userId}] Error getting app connections:`, error);
}
const userConnections = mcpManager.getUserConnections(userId) || new Map();
- const oauthServers = await getMCPServersRegistry().getOAuthServers(userId);
+ const oauthServers = new Set(
+ Object.entries(mcpConfig)
+ .filter(([, config]) => config.requiresOAuth)
+ .map(([name]) => name),
+ );
return {
mcpConfig,
@@ -797,6 +848,8 @@ module.exports = {
createMCPTool,
createMCPTools,
getMCPSetupData,
+ resolveConfigServers,
+ resolveAllMcpConfigs,
checkOAuthFlowStatus,
getServerConnectionStatus,
createUnavailableToolStub,
diff --git a/api/server/services/MCP.spec.js b/api/server/services/MCP.spec.js
index 14a9ef90ed..c9925827f8 100644
--- a/api/server/services/MCP.spec.js
+++ b/api/server/services/MCP.spec.js
@@ -14,6 +14,7 @@ const mockRegistryInstance = {
getOAuthServers: jest.fn(() => Promise.resolve(new Set())),
getAllServerConfigs: jest.fn(() => Promise.resolve({})),
getServerConfig: jest.fn(() => Promise.resolve(null)),
+ ensureConfigServers: jest.fn(() => Promise.resolve({})),
};
// Create isMCPDomainAllowed mock that can be configured per-test
@@ -113,38 +114,43 @@ describe('tests for the new helper functions used by the MCP connection status e
});
it('should successfully return MCP setup data', async () => {
- mockRegistryInstance.getAllServerConfigs.mockResolvedValue(mockConfig);
+ const mockConfigWithOAuth = {
+ server1: { type: 'stdio' },
+ server2: { type: 'http', requiresOAuth: true },
+ };
+ mockRegistryInstance.getAllServerConfigs.mockResolvedValue(mockConfigWithOAuth);
const mockAppConnections = new Map([['server1', { status: 'connected' }]]);
const mockUserConnections = new Map([['server2', { status: 'disconnected' }]]);
- const mockOAuthServers = new Set(['server2']);
const mockMCPManager = {
appConnections: { getLoaded: jest.fn(() => Promise.resolve(mockAppConnections)) },
getUserConnections: jest.fn(() => mockUserConnections),
};
mockGetMCPManager.mockReturnValue(mockMCPManager);
- mockRegistryInstance.getOAuthServers.mockResolvedValue(mockOAuthServers);
const result = await getMCPSetupData(mockUserId);
- expect(mockRegistryInstance.getAllServerConfigs).toHaveBeenCalledWith(mockUserId);
+ expect(mockRegistryInstance.ensureConfigServers).toHaveBeenCalled();
+ expect(mockRegistryInstance.getAllServerConfigs).toHaveBeenCalledWith(
+ mockUserId,
+ expect.any(Object),
+ );
expect(mockGetMCPManager).toHaveBeenCalledWith(mockUserId);
expect(mockMCPManager.appConnections.getLoaded).toHaveBeenCalled();
expect(mockMCPManager.getUserConnections).toHaveBeenCalledWith(mockUserId);
- expect(mockRegistryInstance.getOAuthServers).toHaveBeenCalledWith(mockUserId);
- expect(result).toEqual({
- mcpConfig: mockConfig,
- appConnections: mockAppConnections,
- userConnections: mockUserConnections,
- oauthServers: mockOAuthServers,
- });
+ expect(result.mcpConfig).toEqual(mockConfigWithOAuth);
+ expect(result.appConnections).toEqual(mockAppConnections);
+ expect(result.userConnections).toEqual(mockUserConnections);
+ expect(result.oauthServers).toEqual(new Set(['server2']));
});
- it('should throw error when MCP config not found', async () => {
- mockRegistryInstance.getAllServerConfigs.mockResolvedValue(null);
- await expect(getMCPSetupData(mockUserId)).rejects.toThrow('MCP config not found');
+ it('should return empty data when no servers are configured', async () => {
+ mockRegistryInstance.getAllServerConfigs.mockResolvedValue({});
+ const result = await getMCPSetupData(mockUserId);
+ expect(result.mcpConfig).toEqual({});
+ expect(result.oauthServers).toEqual(new Set());
});
it('should handle null values from MCP manager gracefully', async () => {
diff --git a/api/server/services/PermissionService.js b/api/server/services/PermissionService.js
index a843f48f6f..fc67b0bc49 100644
--- a/api/server/services/PermissionService.js
+++ b/api/server/services/PermissionService.js
@@ -9,22 +9,7 @@ const {
getGroupMembers,
getGroupOwners,
} = require('~/server/services/GraphApiService');
-const {
- findAccessibleResources: findAccessibleResourcesACL,
- getEffectivePermissions: getEffectivePermissionsACL,
- getEffectivePermissionsForResources: getEffectivePermissionsForResourcesACL,
- grantPermission: grantPermissionACL,
- findEntriesByPrincipalsAndResource,
- findGroupByExternalId,
- findRoleByIdentifier,
- getUserPrincipals,
- hasPermission,
- createGroup,
- createUser,
- updateUser,
- findUser,
-} = require('~/models');
-const { AclEntry, AccessRole, Group } = require('~/db/models');
+const db = require('~/models');
/** @type {boolean|null} */
let transactionSupportCache = null;
@@ -96,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`);
}
@@ -107,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,
@@ -141,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')) {
@@ -170,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;
@@ -206,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,
@@ -244,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
@@ -275,17 +260,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 db.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;
}
@@ -302,7 +279,7 @@ const findPubliclyAccessibleResources = async ({ resourceType, requiredPermissio
const getAvailableRoles = async ({ resourceType }) => {
validateResourceType(resourceType);
- return await AccessRole.find({ resourceType }).lean();
+ return await db.findRolesByResourceType(resourceType);
};
/**
@@ -331,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',
});
@@ -355,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();
}
@@ -420,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 Group.findOne({ email: principal.email.toLowerCase() }).lean();
+ existingGroup = await db.findGroupByQuery({ email: principal.email.toLowerCase() });
}
if (existingGroup) {
@@ -452,7 +429,7 @@ const ensureGroupPrincipalExists = async function (principal, authContext = null
}
if (needsUpdate) {
- await Group.findByIdAndUpdate(existingGroup._id, { $set: updateData }, { new: true });
+ await db.updateGroupById(existingGroup._id, updateData);
}
return existingGroup._id.toString();
@@ -473,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) {
@@ -520,7 +497,7 @@ const syncUserEntraGroupMemberships = async (user, accessToken, session = null)
const sessionOptions = session ? { session } : {};
- await Group.updateMany(
+ await db.bulkUpdateGroups(
{
idOnTheSource: { $in: allGroupIds },
source: 'entra',
@@ -530,13 +507,13 @@ const syncUserEntraGroupMemberships = async (user, accessToken, session = null)
sessionOptions,
);
- await Group.updateMany(
+ await db.bulkUpdateGroups(
{
source: 'entra',
memberIds: user.idOnTheSource,
idOnTheSource: { $nin: allGroupIds },
},
- { $pull: { memberIds: user.idOnTheSource } },
+ { $pullAll: { memberIds: [user.idOnTheSource] } },
sessionOptions,
);
} catch (error) {
@@ -563,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,
@@ -628,7 +605,7 @@ const bulkUpdateResourcePermissions = async ({
const sessionOptions = localSession ? { session: localSession } : {};
- const roles = await AccessRole.find({ resourceType }).lean();
+ const roles = await db.findRolesByResourceType(resourceType);
const rolesMap = new Map();
roles.forEach((role) => {
rolesMap.set(role.accessRoleId, role);
@@ -732,7 +709,7 @@ const bulkUpdateResourcePermissions = async ({
}
if (bulkWrites.length > 0) {
- await AclEntry.bulkWrite(bulkWrites, sessionOptions);
+ await db.bulkWriteAclEntries(bulkWrites, sessionOptions);
}
const deleteQueries = [];
@@ -773,12 +750,7 @@ const bulkUpdateResourcePermissions = async ({
}
if (deleteQueries.length > 0) {
- await AclEntry.deleteMany(
- {
- $or: deleteQueries,
- },
- sessionOptions,
- );
+ await db.deleteAclEntries({ $or: deleteQueries }, sessionOptions);
}
if (shouldEndSession && supportsTransactions) {
@@ -788,7 +760,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;
@@ -814,7 +794,7 @@ const removeAllPermissions = async ({ resourceType, resourceId }) => {
throw new Error(`Invalid resource ID: ${resourceId}`);
}
- const result = await AclEntry.deleteMany({
+ const result = await db.deleteAclEntries({
resourceType,
resourceId,
});
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/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/ToolService.js b/api/server/services/ToolService.js
index ca75e7eb4f..b4d948eda4 100644
--- a/api/server/services/ToolService.js
+++ b/api/server/services/ToolService.js
@@ -19,6 +19,7 @@ const {
buildWebSearchContext,
buildImageToolContext,
buildToolClassification,
+ buildOAuthToolCallName,
} = require('@librechat/api');
const {
Time,
@@ -30,6 +31,7 @@ const {
imageGenTools,
EModelEndpoint,
EToolResources,
+ isActionTool,
actionDelimiter,
ImageVisionTool,
openapiToFunction,
@@ -59,6 +61,7 @@ const { manifestToolMap, toolkits } = require('~/app/clients/tools/manifest');
const { createOnSearchResults } = require('~/server/services/Tools/search');
const { loadAuthValues } = require('~/server/services/Tools/credentials');
const { reinitMCPServer } = require('~/server/services/Tools/mcp');
+const { resolveConfigServers } = require('~/server/services/MCP');
const { recordUsage } = require('~/server/services/Threads');
const { loadTools } = require('~/app/clients/tools/util');
const { redactMessage } = require('~/config/parsers');
@@ -488,7 +491,7 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
if (tool === Tools.web_search) {
return checkCapability(AgentCapabilities.web_search);
}
- if (tool.includes(actionDelimiter)) {
+ if (isActionTool(tool)) {
return actionsEnabled;
}
if (!areToolsEnabled) {
@@ -513,6 +516,7 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
const flowsCache = getLogStores(CacheKeys.FLOWS);
const flowManager = getFlowStateManager(flowsCache);
+ const configServers = await resolveConfigServers(req);
const pendingOAuthServers = new Set();
const createOAuthEmitter = (serverName) => {
@@ -521,7 +525,7 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
const stepId = 'step_oauth_login_' + serverName;
const toolCall = {
id: flowId,
- name: serverName,
+ name: buildOAuthToolCallName(serverName),
type: 'tool_call_chunk',
};
@@ -578,6 +582,7 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
oauthStart,
flowManager,
serverName,
+ configServers,
userMCPAuthMap,
});
@@ -665,6 +670,7 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
const result = await reinitMCPServer({
user: req.user,
serverName,
+ configServers,
userMCPAuthMap,
flowManager,
returnOnOAuth: false,
@@ -866,7 +872,7 @@ async function loadAgentTools({
} else if (tool === Tools.web_search) {
includesWebSearch = checkCapability(AgentCapabilities.web_search);
return includesWebSearch;
- } else if (tool.includes(actionDelimiter)) {
+ } else if (isActionTool(tool)) {
return actionsEnabled;
} else if (!areToolsEnabled) {
return false;
@@ -973,7 +979,7 @@ async function loadAgentTools({
agentTools.push(...additionalTools);
- const hasActionTools = _agentTools.some((t) => t.includes(actionDelimiter));
+ const hasActionTools = _agentTools.some((t) => isActionTool(t));
if (!hasActionTools) {
return {
toolRegistry,
@@ -1232,8 +1238,11 @@ async function loadToolsForExecution({
? [...new Set([...requestedNonSpecialToolNames, ...ptcOrchestratedToolNames])]
: requestedNonSpecialToolNames;
- const actionToolNames = allToolNamesToLoad.filter((name) => name.includes(actionDelimiter));
- const regularToolNames = allToolNamesToLoad.filter((name) => !name.includes(actionDelimiter));
+ const actionToolNames = [];
+ const regularToolNames = [];
+ for (const name of allToolNamesToLoad) {
+ (isActionTool(name) ? actionToolNames : regularToolNames).push(name);
+ }
if (regularToolNames.length > 0) {
const includesWebSearch = regularToolNames.includes(Tools.web_search);
diff --git a/api/server/services/Tools/mcp.js b/api/server/services/Tools/mcp.js
index 7589043e10..f1ebcf9796 100644
--- a/api/server/services/Tools/mcp.js
+++ b/api/server/services/Tools/mcp.js
@@ -25,11 +25,13 @@ async function reinitMCPServer({
signal,
forceNew,
serverName,
+ configServers,
userMCPAuthMap,
connectionTimeout,
returnOnOAuth = true,
oauthStart: _oauthStart,
flowManager: _flowManager,
+ serverConfig: providedConfig,
}) {
/** @type {MCPConnection | null} */
let connection = null;
@@ -42,13 +44,28 @@ async function reinitMCPServer({
try {
const registry = getMCPServersRegistry();
- const serverConfig = await registry.getServerConfig(serverName, user?.id);
+ const serverConfig =
+ providedConfig ?? (await registry.getServerConfig(serverName, user?.id, configServers));
if (serverConfig?.inspectionFailed) {
+ if (serverConfig.source === 'config') {
+ logger.info(
+ `[MCP Reinitialize] Config-source server ${serverName} has inspectionFailed — retry handled by config cache`,
+ );
+ return {
+ availableTools: null,
+ success: false,
+ message: `MCP server '${serverName}' is still unreachable`,
+ oauthRequired: false,
+ serverName,
+ oauthUrl: null,
+ tools: null,
+ };
+ }
logger.info(
`[MCP Reinitialize] Server ${serverName} had failed inspection, attempting reinspection`,
);
try {
- const storageLocation = serverConfig.dbId ? 'DB' : 'CACHE';
+ const storageLocation = serverConfig.source === 'user' ? 'DB' : 'CACHE';
await registry.reinspectServer(serverName, storageLocation, user?.id);
logger.info(`[MCP Reinitialize] Reinspection succeeded for server: ${serverName}`);
} catch (reinspectError) {
@@ -93,6 +110,7 @@ async function reinitMCPServer({
returnOnOAuth,
customUserVars,
connectionTimeout,
+ serverConfig,
});
logger.info(`[MCP Reinitialize] Successfully established connection for ${serverName}`);
@@ -125,6 +143,7 @@ async function reinitMCPServer({
oauthStart,
customUserVars,
connectionTimeout,
+ configServers,
});
if (discoveryResult.tools && discoveryResult.tools.length > 0) {
diff --git a/api/server/services/__tests__/MCP.spec.js b/api/server/services/__tests__/MCP.spec.js
new file mode 100644
index 0000000000..39e99d54ac
--- /dev/null
+++ b/api/server/services/__tests__/MCP.spec.js
@@ -0,0 +1,131 @@
+const mockRegistry = {
+ ensureConfigServers: jest.fn(),
+ getAllServerConfigs: jest.fn(),
+};
+
+jest.mock('~/config', () => ({
+ getMCPServersRegistry: jest.fn(() => mockRegistry),
+ getMCPManager: jest.fn(),
+ getFlowStateManager: jest.fn(),
+ getOAuthReconnectionManager: jest.fn(),
+}));
+
+jest.mock('@librechat/data-schemas', () => ({
+ getTenantId: jest.fn(() => 'tenant-1'),
+ logger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() },
+}));
+
+jest.mock('~/server/services/Config', () => ({
+ getAppConfig: jest.fn(),
+ setCachedTools: jest.fn(),
+ getCachedTools: jest.fn(),
+ getMCPServerTools: jest.fn(),
+ loadCustomConfig: jest.fn(),
+}));
+
+jest.mock('~/cache', () => ({ getLogStores: jest.fn() }));
+jest.mock('~/models', () => ({
+ findToken: jest.fn(),
+ createToken: jest.fn(),
+ updateToken: jest.fn(),
+}));
+jest.mock('~/server/services/GraphTokenService', () => ({
+ getGraphApiToken: jest.fn(),
+}));
+jest.mock('~/server/services/Tools/mcp', () => ({
+ reinitMCPServer: jest.fn(),
+}));
+
+const { getAppConfig } = require('~/server/services/Config');
+const { resolveConfigServers, resolveAllMcpConfigs } = require('../MCP');
+
+describe('resolveConfigServers', () => {
+ beforeEach(() => jest.clearAllMocks());
+
+ it('resolves config servers for the current request context', async () => {
+ getAppConfig.mockResolvedValue({ mcpConfig: { srv: { url: 'http://a' } } });
+ mockRegistry.ensureConfigServers.mockResolvedValue({ srv: { name: 'srv' } });
+
+ const result = await resolveConfigServers({ user: { id: 'u1', role: 'admin' } });
+
+ expect(result).toEqual({ srv: { name: 'srv' } });
+ expect(getAppConfig).toHaveBeenCalledWith(
+ expect.objectContaining({ role: 'admin', userId: 'u1' }),
+ );
+ expect(mockRegistry.ensureConfigServers).toHaveBeenCalledWith({ srv: { url: 'http://a' } });
+ });
+
+ it('returns {} when ensureConfigServers throws', async () => {
+ getAppConfig.mockResolvedValue({ mcpConfig: { srv: {} } });
+ mockRegistry.ensureConfigServers.mockRejectedValue(new Error('inspect failed'));
+
+ const result = await resolveConfigServers({ user: { id: 'u1' } });
+
+ expect(result).toEqual({});
+ });
+
+ it('returns {} when getAppConfig throws', async () => {
+ getAppConfig.mockRejectedValue(new Error('db timeout'));
+
+ const result = await resolveConfigServers({ user: { id: 'u1' } });
+
+ expect(result).toEqual({});
+ });
+
+ it('passes empty mcpConfig when appConfig has none', async () => {
+ getAppConfig.mockResolvedValue({});
+ mockRegistry.ensureConfigServers.mockResolvedValue({});
+
+ await resolveConfigServers({ user: { id: 'u1' } });
+
+ expect(mockRegistry.ensureConfigServers).toHaveBeenCalledWith({});
+ });
+});
+
+describe('resolveAllMcpConfigs', () => {
+ beforeEach(() => jest.clearAllMocks());
+
+ it('merges config servers with base servers', async () => {
+ getAppConfig.mockResolvedValue({ mcpConfig: { cfg_srv: {} } });
+ mockRegistry.ensureConfigServers.mockResolvedValue({ cfg_srv: { name: 'cfg_srv' } });
+ mockRegistry.getAllServerConfigs.mockResolvedValue({
+ cfg_srv: { name: 'cfg_srv' },
+ yaml_srv: { name: 'yaml_srv' },
+ });
+
+ const result = await resolveAllMcpConfigs('u1', { id: 'u1', role: 'user' });
+
+ expect(result).toEqual({
+ cfg_srv: { name: 'cfg_srv' },
+ yaml_srv: { name: 'yaml_srv' },
+ });
+ expect(mockRegistry.getAllServerConfigs).toHaveBeenCalledWith('u1', {
+ cfg_srv: { name: 'cfg_srv' },
+ });
+ });
+
+ it('continues with empty configServers when ensureConfigServers fails', async () => {
+ getAppConfig.mockResolvedValue({ mcpConfig: { srv: {} } });
+ mockRegistry.ensureConfigServers.mockRejectedValue(new Error('inspect failed'));
+ mockRegistry.getAllServerConfigs.mockResolvedValue({ yaml_srv: { name: 'yaml_srv' } });
+
+ const result = await resolveAllMcpConfigs('u1', { id: 'u1' });
+
+ expect(result).toEqual({ yaml_srv: { name: 'yaml_srv' } });
+ expect(mockRegistry.getAllServerConfigs).toHaveBeenCalledWith('u1', {});
+ });
+
+ it('propagates getAllServerConfigs failures', async () => {
+ getAppConfig.mockResolvedValue({ mcpConfig: {} });
+ mockRegistry.ensureConfigServers.mockResolvedValue({});
+ mockRegistry.getAllServerConfigs.mockRejectedValue(new Error('redis down'));
+
+ await expect(resolveAllMcpConfigs('u1', { id: 'u1' })).rejects.toThrow('redis down');
+ });
+
+ it('propagates getAppConfig failures', async () => {
+ getAppConfig.mockRejectedValue(new Error('mongo down'));
+
+ await expect(resolveAllMcpConfigs('u1', { id: 'u1' })).rejects.toThrow('mongo down');
+ });
+});
diff --git a/api/server/services/__tests__/ToolService.spec.js b/api/server/services/__tests__/ToolService.spec.js
index a468a88eb3..740bb06e5a 100644
--- a/api/server/services/__tests__/ToolService.spec.js
+++ b/api/server/services/__tests__/ToolService.spec.js
@@ -2,6 +2,7 @@ const {
Tools,
Constants,
EModelEndpoint,
+ isActionTool,
actionDelimiter,
AgentCapabilities,
defaultAgentCapabilities,
@@ -64,6 +65,9 @@ jest.mock('~/models', () => ({
jest.mock('~/config', () => ({
getFlowStateManager: jest.fn(() => ({})),
}));
+jest.mock('~/server/services/MCP', () => ({
+ resolveConfigServers: jest.fn().mockResolvedValue({}),
+}));
jest.mock('~/cache', () => ({
getLogStores: jest.fn(() => ({})),
}));
@@ -140,6 +144,42 @@ describe('ToolService - Action Capability Gating', () => {
});
});
+ describe('isActionTool — cross-delimiter collision guard', () => {
+ it('should identify real action tools', () => {
+ expect(isActionTool(`get_weather${actionDelimiter}api_example_com`)).toBe(true);
+ expect(isActionTool(`fetch_data${actionDelimiter}my---domain---com`)).toBe(true);
+ });
+
+ it('should identify action tools whose operationId contains _mcp_', () => {
+ expect(isActionTool(`sync_mcp_state${actionDelimiter}api---example---com`)).toBe(true);
+ expect(isActionTool(`get_mcp_config${actionDelimiter}internal---api---com`)).toBe(true);
+ });
+
+ it('should reject MCP tools whose name ends with _action', () => {
+ expect(isActionTool(`get_action${Constants.mcp_delimiter}myserver`)).toBe(false);
+ expect(isActionTool(`fetch_action${Constants.mcp_delimiter}server_name`)).toBe(false);
+ expect(isActionTool(`retrieve_action${Constants.mcp_delimiter}srv`)).toBe(false);
+ });
+
+ it('should reject MCP tools with _action_ in the middle of their name', () => {
+ expect(isActionTool(`get_action_data${Constants.mcp_delimiter}myserver`)).toBe(false);
+ expect(isActionTool(`create_action_item${Constants.mcp_delimiter}server`)).toBe(false);
+ });
+
+ it('should reject tools without the action delimiter', () => {
+ expect(isActionTool('calculator')).toBe(false);
+ expect(isActionTool(`web_search${Constants.mcp_delimiter}myserver`)).toBe(false);
+ });
+
+ it('known limitation: non-RFC domain with _mcp_ substring yields false negative', () => {
+ // RFC 952/1123 prohibit underscores in hostnames, so this is not expected in practice.
+ // Encoded domain `api_mcp_internal_com` places `_mcp_` after `_action_`, which
+ // the guard interprets as the MCP suffix.
+ const edgeCaseTool = `getData${actionDelimiter}api_mcp_internal_com`;
+ expect(isActionTool(edgeCaseTool)).toBe(false);
+ });
+ });
+
describe('loadAgentTools (definitionsOnly=true) — action tool filtering', () => {
const actionToolName = `get_weather${actionDelimiter}api_example_com`;
const regularTool = 'calculator';
@@ -180,6 +220,25 @@ describe('ToolService - Action Capability Gating', () => {
expect(callArgs.tools).toContain(actionToolName);
});
+ it('should not filter MCP tools whose name contains _action (cross-delimiter collision)', async () => {
+ const mcpToolWithAction = `get_action${Constants.mcp_delimiter}myserver`;
+ const capabilities = [AgentCapabilities.tools];
+ const req = createMockReq(capabilities);
+ mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
+
+ await loadAgentTools({
+ req,
+ res: {},
+ agent: { id: 'agent_123', tools: [regularTool, mcpToolWithAction] },
+ definitionsOnly: true,
+ });
+
+ expect(mockLoadToolDefinitions).toHaveBeenCalledTimes(1);
+ const [callArgs] = mockLoadToolDefinitions.mock.calls[0];
+ expect(callArgs.tools).toContain(mcpToolWithAction);
+ expect(callArgs.tools).toContain(regularTool);
+ });
+
it('should return actionsEnabled in the result', async () => {
const capabilities = [AgentCapabilities.tools];
const req = createMockReq(capabilities);
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/initializeMCPs.js b/api/server/services/initializeMCPs.js
index c7f27acd0e..5728730131 100644
--- a/api/server/services/initializeMCPs.js
+++ b/api/server/services/initializeMCPs.js
@@ -7,7 +7,7 @@ const { createMCPServersRegistry, createMCPManager } = require('~/config');
* Initialize MCP servers
*/
async function initializeMCPs() {
- const appConfig = await getAppConfig();
+ const appConfig = await getAppConfig({ baseOnly: true });
const mcpServers = appConfig.mcpConfig;
try {
diff --git a/api/server/services/start/migration.js b/api/server/services/start/migration.js
index 83b9c83e39..70f8300e08 100644
--- a/api/server/services/start/migration.js
+++ b/api/server/services/start/migration.js
@@ -6,8 +6,6 @@ const {
checkAgentPermissionsMigration,
checkPromptPermissionsMigration,
} = require('@librechat/api');
-const { getProjectByName } = require('~/models/Project');
-const { Agent, PromptGroup } = require('~/db/models');
const { findRoleByIdentifier } = require('~/models');
/**
@@ -20,9 +18,8 @@ async function checkMigrations() {
mongoose,
methods: {
findRoleByIdentifier,
- getProjectByName,
},
- AgentModel: Agent,
+ AgentModel: mongoose.models.Agent,
});
logAgentMigrationWarning(agentMigrationResult);
} catch (error) {
@@ -33,9 +30,8 @@ async function checkMigrations() {
mongoose,
methods: {
findRoleByIdentifier,
- getProjectByName,
},
- PromptGroupModel: PromptGroup,
+ PromptGroupModel: mongoose.models.PromptGroup,
});
logPromptMigrationWarning(promptMigrationResult);
} catch (error) {
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/api/server/socialLogins.js b/api/server/socialLogins.js
index a84c33bd52..dfb03b4d37 100644
--- a/api/server/socialLogins.js
+++ b/api/server/socialLogins.js
@@ -6,11 +6,16 @@ const { logger, DEFAULT_SESSION_EXPIRY } = require('@librechat/data-schemas');
const {
openIdJwtLogin,
facebookLogin,
+ facebookAdminLogin,
discordLogin,
+ discordAdminLogin,
setupOpenId,
googleLogin,
+ googleAdminLogin,
githubLogin,
+ githubAdminLogin,
appleLogin,
+ appleAdminLogin,
setupSaml,
} = require('~/strategies');
const { getLogStores } = require('~/cache');
@@ -58,18 +63,23 @@ const configureSocialLogins = async (app) => {
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
passport.use(googleLogin());
+ passport.use('googleAdmin', googleAdminLogin());
}
if (process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET) {
passport.use(facebookLogin());
+ passport.use('facebookAdmin', facebookAdminLogin());
}
if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) {
passport.use(githubLogin());
+ passport.use('githubAdmin', githubAdminLogin());
}
if (process.env.DISCORD_CLIENT_ID && process.env.DISCORD_CLIENT_SECRET) {
passport.use(discordLogin());
+ passport.use('discordAdmin', discordAdminLogin());
}
if (process.env.APPLE_CLIENT_ID && process.env.APPLE_PRIVATE_KEY_PATH) {
passport.use(appleLogin());
+ passport.use('appleAdmin', appleAdminLogin());
}
if (
process.env.OPENID_CLIENT_ID &&
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/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/importConversations.js b/api/server/utils/import/importConversations.js
index e56176c609..ad2d743f01 100644
--- a/api/server/utils/import/importConversations.js
+++ b/api/server/utils/import/importConversations.js
@@ -7,10 +7,10 @@ const maxFileSize = resolveImportMaxFileSize();
/**
* Job definition for importing a conversation.
- * @param {{ filepath, requestUserId }} job - The job object.
+ * @param {{ filepath: string, requestUserId: string, userRole?: string }} job
*/
const importConversations = async (job) => {
- const { filepath, requestUserId } = job;
+ const { filepath, requestUserId, userRole } = job;
try {
logger.debug(`user: ${requestUserId} | Importing conversation(s) from file...`);
@@ -24,7 +24,7 @@ const importConversations = async (job) => {
const fileData = await fs.readFile(filepath, 'utf8');
const jsonData = JSON.parse(fileData);
const importer = getImporter(jsonData);
- await importer(jsonData, requestUserId);
+ await importer(jsonData, requestUserId, undefined, userRole);
logger.debug(`user: ${requestUserId} | Finished importing conversations`);
} catch (error) {
logger.error(`user: ${requestUserId} | Failed to import conversation: `, error);
diff --git a/api/server/utils/import/importers-timestamp.spec.js b/api/server/utils/import/importers-timestamp.spec.js
index c7665dfe25..e12c099abb 100644
--- a/api/server/utils/import/importers-timestamp.spec.js
+++ b/api/server/utils/import/importers-timestamp.spec.js
@@ -1,25 +1,23 @@
+const { logger } = require('@librechat/data-schemas');
const { Constants } = require('librechat-data-provider');
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');
-const getLogStores = require('~/cache/getLogStores');
-const mockedCacheGet = jest.fn();
-getLogStores.mockImplementation(() => ({
- get: mockedCacheGet,
+
+const mockGetEndpointsConfig = jest.fn().mockResolvedValue(null);
+jest.mock('~/server/services/Config', () => ({
+ getEndpointsConfig: (...args) => mockGetEndpointsConfig(...args),
}));
describe('Import Timestamp Ordering', () => {
beforeEach(() => {
jest.clearAllMocks();
- mockedCacheGet.mockResolvedValue(null);
+ mockGetEndpointsConfig.mockResolvedValue(null);
});
describe('LibreChat Import - Timestamp Issues', () => {
@@ -368,6 +366,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..7bcca41e04 100644
--- a/api/server/utils/import/importers.js
+++ b/api/server/utils/import/importers.js
@@ -1,9 +1,9 @@
const { v4: uuidv4 } = require('uuid');
-const { logger } = require('@librechat/data-schemas');
-const { EModelEndpoint, Constants, openAISettings, CacheKeys } = require('librechat-data-provider');
+const { logger, getTenantId } = require('@librechat/data-schemas');
+const { EModelEndpoint, Constants, openAISettings } = require('librechat-data-provider');
+const { getEndpointsConfig } = require('~/server/services/Config');
const { createImportBatchBuilder } = require('./importBatchBuilder');
const { cloneMessagesWithTimestamps } = require('./fork');
-const getLogStores = require('~/cache/getLogStores');
/**
* Returns the appropriate importer function based on the provided JSON data.
@@ -194,6 +194,7 @@ async function importLibreChatConvo(
jsonData,
requestUserId,
builderFactory = createImportBatchBuilder,
+ userRole,
) {
try {
/** @type {ImportBatchBuilder} */
@@ -202,8 +203,9 @@ async function importLibreChatConvo(
/* Endpoint configuration */
let endpoint = jsonData.endpoint ?? options.endpoint ?? EModelEndpoint.openAI;
- const cache = getLogStores(CacheKeys.CONFIG_STORE);
- const endpointsConfig = await cache.get(CacheKeys.ENDPOINT_CONFIG);
+ const endpointsConfig = await getEndpointsConfig({
+ user: { id: requestUserId, role: userRole, tenantId: getTenantId() },
+ });
const endpointConfig = endpointsConfig?.[endpoint];
if (!endpointConfig && endpointsConfig) {
endpoint = Object.keys(endpointsConfig)[0];
@@ -324,32 +326,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 +478,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 +568,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 +599,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 };
diff --git a/api/server/utils/import/importers.spec.js b/api/server/utils/import/importers.spec.js
index 2ddfa76658..6e712881fc 100644
--- a/api/server/utils/import/importers.spec.js
+++ b/api/server/utils/import/importers.spec.js
@@ -1,23 +1,21 @@
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 getLogStores = require('~/cache/getLogStores');
+const { bulkSaveMessages, bulkSaveConvos: _bulkSaveConvos } = require('~/models');
-jest.mock('~/cache/getLogStores');
-const mockedCacheGet = jest.fn();
-getLogStores.mockImplementation(() => ({
- get: mockedCacheGet,
+const mockGetEndpointsConfig = jest.fn().mockResolvedValue({
+ [EModelEndpoint.openAI]: { userProvide: false },
+});
+
+jest.mock('~/server/services/Config', () => ({
+ getEndpointsConfig: (...args) => mockGetEndpointsConfig(...args),
}));
// Mock the database methods
-jest.mock('~/models/Conversation', () => ({
+jest.mock('~/models', () => ({
bulkSaveConvos: jest.fn(),
-}));
-jest.mock('~/models/Message', () => ({
bulkSaveMessages: jest.fn(),
}));
@@ -761,7 +759,7 @@ describe('importLibreChatConvo', () => {
);
it('should import conversation correctly', async () => {
- mockedCacheGet.mockResolvedValue({
+ mockGetEndpointsConfig.mockResolvedValue({
[EModelEndpoint.openAI]: {},
});
const expectedNumberOfMessages = 6;
@@ -787,7 +785,7 @@ describe('importLibreChatConvo', () => {
});
it('should import linear, non-recursive thread correctly with correct endpoint', async () => {
- mockedCacheGet.mockResolvedValue({
+ mockGetEndpointsConfig.mockResolvedValue({
[EModelEndpoint.azureOpenAI]: {},
});
@@ -927,7 +925,7 @@ describe('importLibreChatConvo', () => {
});
it('should retain properties from the original conversation as well as new settings', async () => {
- mockedCacheGet.mockResolvedValue({
+ mockGetEndpointsConfig.mockResolvedValue({
[EModelEndpoint.azureOpenAI]: {},
});
const requestUserId = 'user-123';
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/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/api/strategies/appleStrategy.js b/api/strategies/appleStrategy.js
index fbba2a1f41..6eace87bae 100644
--- a/api/strategies/appleStrategy.js
+++ b/api/strategies/appleStrategy.js
@@ -34,16 +34,28 @@ const getProfileDetails = ({ idToken, profile }) => {
// Initialize the social login handler for Apple
const appleLogin = socialLogin('apple', getProfileDetails);
+const appleAdminLogin = socialLogin('apple', getProfileDetails, { existingUsersOnly: true });
-module.exports = () =>
+const getAppleConfig = (callbackURL) => ({
+ clientID: process.env.APPLE_CLIENT_ID,
+ teamID: process.env.APPLE_TEAM_ID,
+ callbackURL,
+ keyID: process.env.APPLE_KEY_ID,
+ privateKeyLocation: process.env.APPLE_PRIVATE_KEY_PATH,
+ passReqToCallback: false,
+});
+
+const appleStrategy = () =>
new AppleStrategy(
- {
- clientID: process.env.APPLE_CLIENT_ID,
- teamID: process.env.APPLE_TEAM_ID,
- callbackURL: `${process.env.DOMAIN_SERVER}${process.env.APPLE_CALLBACK_URL}`,
- keyID: process.env.APPLE_KEY_ID,
- privateKeyLocation: process.env.APPLE_PRIVATE_KEY_PATH,
- passReqToCallback: false, // Set to true if you need to access the request in the callback
- },
+ getAppleConfig(`${process.env.DOMAIN_SERVER}${process.env.APPLE_CALLBACK_URL}`),
appleLogin,
);
+
+const appleAdminStrategy = () =>
+ new AppleStrategy(
+ getAppleConfig(`${process.env.DOMAIN_SERVER}/api/admin/oauth/apple/callback`),
+ appleAdminLogin,
+ );
+
+module.exports = appleStrategy;
+module.exports.appleAdminLogin = appleAdminStrategy;
diff --git a/api/strategies/discordStrategy.js b/api/strategies/discordStrategy.js
index dc7cb05ac6..7fb68280d5 100644
--- a/api/strategies/discordStrategy.js
+++ b/api/strategies/discordStrategy.js
@@ -22,15 +22,27 @@ const getProfileDetails = ({ profile }) => {
};
const discordLogin = socialLogin('discord', getProfileDetails);
+const discordAdminLogin = socialLogin('discord', getProfileDetails, { existingUsersOnly: true });
-module.exports = () =>
+const getDiscordConfig = (callbackURL) => ({
+ clientID: process.env.DISCORD_CLIENT_ID,
+ clientSecret: process.env.DISCORD_CLIENT_SECRET,
+ callbackURL,
+ scope: ['identify', 'email'],
+ authorizationURL: 'https://discord.com/api/oauth2/authorize?prompt=none',
+});
+
+const discordStrategy = () =>
new DiscordStrategy(
- {
- clientID: process.env.DISCORD_CLIENT_ID,
- clientSecret: process.env.DISCORD_CLIENT_SECRET,
- callbackURL: `${process.env.DOMAIN_SERVER}${process.env.DISCORD_CALLBACK_URL}`,
- scope: ['identify', 'email'],
- authorizationURL: 'https://discord.com/api/oauth2/authorize?prompt=none',
- },
+ getDiscordConfig(`${process.env.DOMAIN_SERVER}${process.env.DISCORD_CALLBACK_URL}`),
discordLogin,
);
+
+const discordAdminStrategy = () =>
+ new DiscordStrategy(
+ getDiscordConfig(`${process.env.DOMAIN_SERVER}/api/admin/oauth/discord/callback`),
+ discordAdminLogin,
+ );
+
+module.exports = discordStrategy;
+module.exports.discordAdminLogin = discordAdminStrategy;
diff --git a/api/strategies/facebookStrategy.js b/api/strategies/facebookStrategy.js
index e5d1b054db..f638c3bfdb 100644
--- a/api/strategies/facebookStrategy.js
+++ b/api/strategies/facebookStrategy.js
@@ -11,16 +11,28 @@ const getProfileDetails = ({ profile }) => ({
});
const facebookLogin = socialLogin('facebook', getProfileDetails);
+const facebookAdminLogin = socialLogin('facebook', getProfileDetails, { existingUsersOnly: true });
-module.exports = () =>
+const getFacebookConfig = (callbackURL) => ({
+ clientID: process.env.FACEBOOK_CLIENT_ID,
+ clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
+ callbackURL,
+ proxy: true,
+ scope: ['public_profile'],
+ profileFields: ['id', 'email', 'name'],
+});
+
+const facebookStrategy = () =>
new FacebookStrategy(
- {
- clientID: process.env.FACEBOOK_CLIENT_ID,
- clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
- callbackURL: `${process.env.DOMAIN_SERVER}${process.env.FACEBOOK_CALLBACK_URL}`,
- proxy: true,
- scope: ['public_profile'],
- profileFields: ['id', 'email', 'name'],
- },
+ getFacebookConfig(`${process.env.DOMAIN_SERVER}${process.env.FACEBOOK_CALLBACK_URL}`),
facebookLogin,
);
+
+const facebookAdminStrategy = () =>
+ new FacebookStrategy(
+ getFacebookConfig(`${process.env.DOMAIN_SERVER}/api/admin/oauth/facebook/callback`),
+ facebookAdminLogin,
+ );
+
+module.exports = facebookStrategy;
+module.exports.facebookAdminLogin = facebookAdminStrategy;
diff --git a/api/strategies/githubStrategy.js b/api/strategies/githubStrategy.js
index 1c3937381e..363acbfcdb 100644
--- a/api/strategies/githubStrategy.js
+++ b/api/strategies/githubStrategy.js
@@ -11,24 +11,36 @@ const getProfileDetails = ({ profile }) => ({
});
const githubLogin = socialLogin('github', getProfileDetails);
+const githubAdminLogin = socialLogin('github', getProfileDetails, { existingUsersOnly: true });
-module.exports = () =>
+const getGitHubConfig = (callbackURL) => ({
+ clientID: process.env.GITHUB_CLIENT_ID,
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
+ callbackURL,
+ proxy: false,
+ scope: ['user:email'],
+ ...(process.env.GITHUB_ENTERPRISE_BASE_URL && {
+ authorizationURL: `${process.env.GITHUB_ENTERPRISE_BASE_URL}/login/oauth/authorize`,
+ tokenURL: `${process.env.GITHUB_ENTERPRISE_BASE_URL}/login/oauth/access_token`,
+ userProfileURL: `${process.env.GITHUB_ENTERPRISE_BASE_URL}/api/v3/user`,
+ userEmailURL: `${process.env.GITHUB_ENTERPRISE_BASE_URL}/api/v3/user/emails`,
+ ...(process.env.GITHUB_ENTERPRISE_USER_AGENT && {
+ userAgent: process.env.GITHUB_ENTERPRISE_USER_AGENT,
+ }),
+ }),
+});
+
+const githubStrategy = () =>
new GitHubStrategy(
- {
- clientID: process.env.GITHUB_CLIENT_ID,
- clientSecret: process.env.GITHUB_CLIENT_SECRET,
- callbackURL: `${process.env.DOMAIN_SERVER}${process.env.GITHUB_CALLBACK_URL}`,
- proxy: false,
- scope: ['user:email'],
- ...(process.env.GITHUB_ENTERPRISE_BASE_URL && {
- authorizationURL: `${process.env.GITHUB_ENTERPRISE_BASE_URL}/login/oauth/authorize`,
- tokenURL: `${process.env.GITHUB_ENTERPRISE_BASE_URL}/login/oauth/access_token`,
- userProfileURL: `${process.env.GITHUB_ENTERPRISE_BASE_URL}/api/v3/user`,
- userEmailURL: `${process.env.GITHUB_ENTERPRISE_BASE_URL}/api/v3/user/emails`,
- ...(process.env.GITHUB_ENTERPRISE_USER_AGENT && {
- userAgent: process.env.GITHUB_ENTERPRISE_USER_AGENT,
- }),
- }),
- },
+ getGitHubConfig(`${process.env.DOMAIN_SERVER}${process.env.GITHUB_CALLBACK_URL}`),
githubLogin,
);
+
+const githubAdminStrategy = () =>
+ new GitHubStrategy(
+ getGitHubConfig(`${process.env.DOMAIN_SERVER}/api/admin/oauth/github/callback`),
+ githubAdminLogin,
+ );
+
+module.exports = githubStrategy;
+module.exports.githubAdminLogin = githubAdminStrategy;
diff --git a/api/strategies/googleStrategy.js b/api/strategies/googleStrategy.js
index fd65823327..bee9a061a2 100644
--- a/api/strategies/googleStrategy.js
+++ b/api/strategies/googleStrategy.js
@@ -11,14 +11,26 @@ const getProfileDetails = ({ profile }) => ({
});
const googleLogin = socialLogin('google', getProfileDetails);
+const googleAdminLogin = socialLogin('google', getProfileDetails, { existingUsersOnly: true });
-module.exports = () =>
+const getGoogleConfig = (callbackURL) => ({
+ clientID: process.env.GOOGLE_CLIENT_ID,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
+ callbackURL,
+ proxy: true,
+});
+
+const googleStrategy = () =>
new GoogleStrategy(
- {
- clientID: process.env.GOOGLE_CLIENT_ID,
- clientSecret: process.env.GOOGLE_CLIENT_SECRET,
- callbackURL: `${process.env.DOMAIN_SERVER}${process.env.GOOGLE_CALLBACK_URL}`,
- proxy: true,
- },
+ getGoogleConfig(`${process.env.DOMAIN_SERVER}${process.env.GOOGLE_CALLBACK_URL}`),
googleLogin,
);
+
+const googleAdminStrategy = () =>
+ new GoogleStrategy(
+ getGoogleConfig(`${process.env.DOMAIN_SERVER}/api/admin/oauth/google/callback`),
+ googleAdminLogin,
+ );
+
+module.exports = googleStrategy;
+module.exports.googleAdminLogin = googleAdminStrategy;
diff --git a/api/strategies/index.js b/api/strategies/index.js
index 9a1c58ad38..c15bbc4ce5 100644
--- a/api/strategies/index.js
+++ b/api/strategies/index.js
@@ -1,23 +1,33 @@
const { setupOpenId, getOpenIdConfig, getOpenIdEmail } = require('./openidStrategy');
const openIdJwtLogin = require('./openIdJwtStrategy');
const facebookLogin = require('./facebookStrategy');
+const { facebookAdminLogin } = facebookLogin;
const discordLogin = require('./discordStrategy');
+const { discordAdminLogin } = discordLogin;
const passportLogin = require('./localStrategy');
const googleLogin = require('./googleStrategy');
+const { googleAdminLogin } = googleLogin;
const githubLogin = require('./githubStrategy');
+const { githubAdminLogin } = githubLogin;
const { setupSaml } = require('./samlStrategy');
const appleLogin = require('./appleStrategy');
+const { appleAdminLogin } = appleLogin;
const ldapLogin = require('./ldapStrategy');
const jwtLogin = require('./jwtStrategy');
module.exports = {
appleLogin,
+ appleAdminLogin,
passportLogin,
googleLogin,
+ googleAdminLogin,
githubLogin,
+ githubAdminLogin,
discordLogin,
+ discordAdminLogin,
jwtLogin,
facebookLogin,
+ facebookAdminLogin,
setupOpenId,
getOpenIdConfig,
getOpenIdEmail,
diff --git a/api/strategies/ldapStrategy.js b/api/strategies/ldapStrategy.js
index dcadc26a45..9253f54196 100644
--- a/api/strategies/ldapStrategy.js
+++ b/api/strategies/ldapStrategy.js
@@ -2,7 +2,12 @@ const fs = require('fs');
const LdapStrategy = require('passport-ldapauth');
const { logger } = require('@librechat/data-schemas');
const { SystemRoles, ErrorTypes } = require('librechat-data-provider');
-const { isEnabled, getBalanceConfig, isEmailDomainAllowed } = require('@librechat/api');
+const {
+ isEnabled,
+ getBalanceConfig,
+ isEmailDomainAllowed,
+ resolveAppConfigForUser,
+} = require('@librechat/api');
const { createUser, findUser, updateUser, countUsers } = require('~/models');
const { getAppConfig } = require('~/server/services/Config');
@@ -89,16 +94,6 @@ const ldapLogin = new LdapStrategy(ldapOptions, async (userinfo, done) => {
const ldapId =
(LDAP_ID && userinfo[LDAP_ID]) || userinfo.uid || userinfo.sAMAccountName || userinfo.mail;
- let user = await findUser({ ldapId });
- if (user && user.provider !== 'ldap') {
- logger.info(
- `[ldapStrategy] User ${user.email} already exists with provider ${user.provider}`,
- );
- return done(null, false, {
- message: ErrorTypes.AUTH_FAILED,
- });
- }
-
const fullNameAttributes = LDAP_FULL_NAME && LDAP_FULL_NAME.split(',');
const fullName =
fullNameAttributes && fullNameAttributes.length > 0
@@ -122,7 +117,31 @@ const ldapLogin = new LdapStrategy(ldapOptions, async (userinfo, done) => {
);
}
- const appConfig = await getAppConfig();
+ // Domain check before findUser for two-phase fast-fail (consistent with SAML/OpenID/social).
+ // This means cross-provider users from blocked domains get 'Email domain not allowed'
+ // instead of AUTH_FAILED — both deny access.
+ const baseConfig = await getAppConfig({ baseOnly: true });
+ if (!isEmailDomainAllowed(mail, baseConfig?.registration?.allowedDomains)) {
+ logger.error(
+ `[LDAP Strategy] Authentication blocked - email domain not allowed [Email: ${mail}]`,
+ );
+ return done(null, false, { message: 'Email domain not allowed' });
+ }
+
+ let user = await findUser({ ldapId });
+ if (user && user.provider !== 'ldap') {
+ logger.info(
+ `[ldapStrategy] User ${user.email} already exists with provider ${user.provider}`,
+ );
+ return done(null, false, {
+ message: ErrorTypes.AUTH_FAILED,
+ });
+ }
+
+ const appConfig = user?.tenantId
+ ? await resolveAppConfigForUser(getAppConfig, user)
+ : baseConfig;
+
if (!isEmailDomainAllowed(mail, appConfig?.registration?.allowedDomains)) {
logger.error(
`[LDAP Strategy] Authentication blocked - email domain not allowed [Email: ${mail}]`,
diff --git a/api/strategies/ldapStrategy.spec.js b/api/strategies/ldapStrategy.spec.js
index a00e9b14b7..876d70f845 100644
--- a/api/strategies/ldapStrategy.spec.js
+++ b/api/strategies/ldapStrategy.spec.js
@@ -9,10 +9,10 @@ jest.mock('@librechat/data-schemas', () => ({
}));
jest.mock('@librechat/api', () => ({
- // isEnabled used for TLS flags
isEnabled: jest.fn(() => false),
isEmailDomainAllowed: jest.fn(() => true),
getBalanceConfig: jest.fn(() => ({ enabled: false })),
+ resolveAppConfigForUser: jest.fn(async (_getAppConfig, _user) => ({})),
}));
jest.mock('~/models', () => ({
@@ -30,14 +30,15 @@ jest.mock('~/server/services/Config', () => ({
let verifyCallback;
jest.mock('passport-ldapauth', () => {
return jest.fn().mockImplementation((options, verify) => {
- verifyCallback = verify; // capture the strategy verify function
+ verifyCallback = verify;
return { name: 'ldap', options, verify };
});
});
const { ErrorTypes } = require('librechat-data-provider');
-const { isEmailDomainAllowed } = require('@librechat/api');
+const { isEmailDomainAllowed, resolveAppConfigForUser } = require('@librechat/api');
const { findUser, createUser, updateUser, countUsers } = require('~/models');
+const { getAppConfig } = require('~/server/services/Config');
// Helper to call the verify callback and wrap in a Promise for convenience
const callVerify = (userinfo) =>
@@ -117,6 +118,7 @@ describe('ldapStrategy', () => {
expect(user).toBe(false);
expect(info).toEqual({ message: ErrorTypes.AUTH_FAILED });
expect(createUser).not.toHaveBeenCalled();
+ expect(resolveAppConfigForUser).not.toHaveBeenCalled();
});
it('updates an existing ldap user with current LDAP info', async () => {
@@ -158,7 +160,6 @@ describe('ldapStrategy', () => {
uid: 'uid999',
givenName: 'John',
cn: 'John Doe',
- // no mail and no custom LDAP_EMAIL
};
const { user } = await callVerify(userinfo);
@@ -180,4 +181,66 @@ describe('ldapStrategy', () => {
expect(user).toBe(false);
expect(info).toEqual({ message: 'Email domain not allowed' });
});
+
+ it('passes getAppConfig and found user to resolveAppConfigForUser', async () => {
+ const existing = {
+ _id: 'u3',
+ provider: 'ldap',
+ email: 'tenant@example.com',
+ ldapId: 'uid-tenant',
+ username: 'tenantuser',
+ name: 'Tenant User',
+ tenantId: 'tenant-a',
+ role: 'USER',
+ };
+ findUser.mockResolvedValue(existing);
+
+ const userinfo = {
+ uid: 'uid-tenant',
+ mail: 'tenant@example.com',
+ givenName: 'Tenant',
+ cn: 'Tenant User',
+ };
+
+ await callVerify(userinfo);
+
+ expect(resolveAppConfigForUser).toHaveBeenCalledWith(getAppConfig, existing);
+ });
+
+ it('uses baseConfig for new user without calling resolveAppConfigForUser', async () => {
+ findUser.mockResolvedValue(null);
+
+ const userinfo = {
+ uid: 'uid-new',
+ mail: 'newuser@example.com',
+ givenName: 'New',
+ cn: 'New User',
+ };
+
+ await callVerify(userinfo);
+
+ expect(resolveAppConfigForUser).not.toHaveBeenCalled();
+ expect(getAppConfig).toHaveBeenCalledWith({ baseOnly: true });
+ });
+
+ it('should block login when tenant config restricts the domain', async () => {
+ const existing = {
+ _id: 'u-blocked',
+ provider: 'ldap',
+ ldapId: 'uid-tenant',
+ tenantId: 'tenant-strict',
+ role: 'USER',
+ };
+ findUser.mockResolvedValue(existing);
+ resolveAppConfigForUser.mockResolvedValue({
+ registration: { allowedDomains: ['other.com'] },
+ });
+ isEmailDomainAllowed.mockReturnValueOnce(true).mockReturnValueOnce(false);
+
+ const userinfo = { uid: 'uid-tenant', mail: 'user@example.com', givenName: 'Test', cn: 'Test' };
+ const { user, info } = await callVerify(userinfo);
+
+ expect(user).toBe(false);
+ expect(info).toEqual({ message: 'Email domain not allowed' });
+ });
});
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/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.js b/api/strategies/openidStrategy.js
index 7c43358297..7314a84e15 100644
--- a/api/strategies/openidStrategy.js
+++ b/api/strategies/openidStrategy.js
@@ -15,6 +15,7 @@ const {
findOpenIDUser,
getBalanceConfig,
isEmailDomainAllowed,
+ resolveAppConfigForUser,
} = require('@librechat/api');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { findUser, createUser, updateUser } = require('~/models');
@@ -468,9 +469,10 @@ async function processOpenIDAuth(tokenset, existingUsersOnly = false) {
Object.assign(userinfo, providerUserinfo);
}
- const appConfig = await getAppConfig();
const email = getOpenIdEmail(userinfo);
- if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
+
+ const baseConfig = await getAppConfig({ baseOnly: true });
+ if (!isEmailDomainAllowed(email, baseConfig?.registration?.allowedDomains)) {
logger.error(
`[OpenID Strategy] Authentication blocked - email domain not allowed [Identifier: ${email}]`,
);
@@ -491,6 +493,15 @@ async function processOpenIDAuth(tokenset, existingUsersOnly = false) {
throw new Error(ErrorTypes.AUTH_FAILED);
}
+ const appConfig = user?.tenantId ? await resolveAppConfigForUser(getAppConfig, user) : baseConfig;
+
+ if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
+ logger.error(
+ `[OpenID Strategy] Authentication blocked - email domain not allowed [Identifier: ${email}]`,
+ );
+ throw new Error('Email domain not allowed');
+ }
+
const fullName = getFullName(userinfo);
const requiredRole = process.env.OPENID_REQUIRED_ROLE;
diff --git a/api/strategies/openidStrategy.spec.js b/api/strategies/openidStrategy.spec.js
index 16fa548a59..6d824176f7 100644
--- a/api/strategies/openidStrategy.spec.js
+++ b/api/strategies/openidStrategy.spec.js
@@ -1,1795 +1,1873 @@
-const undici = require('undici');
-const fetch = require('node-fetch');
-const jwtDecode = require('jsonwebtoken/decode');
-const { ErrorTypes } = require('librechat-data-provider');
-const { findUser, createUser, updateUser } = require('~/models');
-const { setupOpenId } = require('./openidStrategy');
-
-// --- Mocks ---
-jest.mock('node-fetch');
-jest.mock('jsonwebtoken/decode');
-jest.mock('undici', () => ({
- fetch: jest.fn(),
- ProxyAgent: jest.fn(),
-}));
-jest.mock('~/server/services/Files/strategies', () => ({
- getStrategyFunctions: jest.fn(() => ({
- saveBuffer: jest.fn().mockResolvedValue('/fake/path/to/avatar.png'),
- })),
-}));
-jest.mock('~/server/services/Config', () => ({
- getAppConfig: jest.fn().mockResolvedValue({}),
-}));
-jest.mock('@librechat/api', () => ({
- ...jest.requireActual('@librechat/api'),
- isEnabled: jest.fn(() => false),
- isEmailDomainAllowed: jest.fn(() => true),
- findOpenIDUser: jest.requireActual('@librechat/api').findOpenIDUser,
- getBalanceConfig: jest.fn(() => ({
- enabled: false,
- })),
-}));
-jest.mock('~/models', () => ({
- findUser: jest.fn(),
- createUser: jest.fn(),
- updateUser: jest.fn(),
-}));
-jest.mock('@librechat/data-schemas', () => ({
- ...jest.requireActual('@librechat/api'),
- logger: {
- info: jest.fn(),
- warn: jest.fn(),
- debug: jest.fn(),
- error: jest.fn(),
- },
- hashToken: jest.fn().mockResolvedValue('hashed-token'),
-}));
-jest.mock('~/cache/getLogStores', () =>
- jest.fn(() => ({
- get: jest.fn(),
- set: jest.fn(),
- })),
-);
-
-// Mock the openid-client module and all its dependencies
-jest.mock('openid-client', () => {
- return {
- discovery: jest.fn().mockResolvedValue({
- clientId: 'fake_client_id',
- clientSecret: 'fake_client_secret',
- issuer: 'https://fake-issuer.com',
- // Add any other properties needed by the implementation
- }),
- fetchUserInfo: jest.fn().mockImplementation(() => {
- // 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'),
- };
-});
-
-jest.mock('openid-client/passport', () => {
- /** Store callbacks by strategy name - 'openid' and 'openidAdmin' */
- const verifyCallbacks = {};
- let lastVerifyCallback;
-
- const mockStrategy = jest.fn((options, verify) => {
- lastVerifyCallback = verify;
- return { name: 'openid', options, verify };
- });
-
- return {
- Strategy: mockStrategy,
- /** Get the last registered callback (for backward compatibility) */
- __getVerifyCallback: () => lastVerifyCallback,
- /** Store callback by name when passport.use is called */
- __setVerifyCallback: (name, callback) => {
- verifyCallbacks[name] = callback;
- },
- /** Get callback by strategy name */
- __getVerifyCallbackByName: (name) => verifyCallbacks[name],
- };
-});
-
-// Mock passport - capture strategy name and callback
-jest.mock('passport', () => ({
- use: jest.fn((name, strategy) => {
- const passportMock = require('openid-client/passport');
- if (strategy && strategy.verify) {
- passportMock.__setVerifyCallback(name, strategy.verify);
- }
- }),
-}));
-
-describe('setupOpenId', () => {
- // Store a reference to the verify callback once it's set up
- let verifyCallback;
-
- // Helper to wrap the verify callback in a promise
- const validate = (tokenset) =>
- new Promise((resolve, reject) => {
- verifyCallback(tokenset, (err, user, details) => {
- if (err) {
- reject(err);
- } else {
- resolve({ user, details });
- }
- });
- });
-
- const tokenset = {
- id_token: 'fake_id_token',
- access_token: 'fake_access_token',
- claims: () => ({
- sub: '1234',
- email: 'test@example.com',
- email_verified: true,
- given_name: 'First',
- family_name: 'Last',
- name: 'My Full',
- preferred_username: 'testusername',
- username: 'flast',
- picture: 'https://example.com/avatar.png',
- }),
- };
-
- beforeEach(async () => {
- // Clear previous mock calls and reset implementations
- jest.clearAllMocks();
-
- // Reset environment variables needed by the strategy
- process.env.OPENID_ISSUER = 'https://fake-issuer.com';
- process.env.OPENID_CLIENT_ID = 'fake_client_id';
- process.env.OPENID_CLIENT_SECRET = 'fake_client_secret';
- process.env.DOMAIN_SERVER = 'https://example.com';
- process.env.OPENID_CALLBACK_URL = '/callback';
- process.env.OPENID_SCOPE = 'openid profile email';
- 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 = 'permissions';
- process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'id';
- delete process.env.OPENID_USERNAME_CLAIM;
- delete process.env.OPENID_NAME_CLAIM;
- delete process.env.OPENID_EMAIL_CLAIM;
- delete process.env.PROXY;
- delete process.env.OPENID_USE_PKCE;
-
- // Default jwtDecode mock returns a token that includes the required role.
- jwtDecode.mockReturnValue({
- roles: ['requiredRole'],
- permissions: ['admin'],
- });
-
- // By default, assume that no user is found, so createUser will be called
- findUser.mockResolvedValue(null);
- createUser.mockImplementation(async (userData) => {
- // simulate created user with an _id property
- return { _id: 'newUserId', ...userData };
- });
- updateUser.mockImplementation(async (id, userData) => {
- return { _id: id, ...userData };
- });
-
- // For image download, simulate a successful response
- const fakeBuffer = Buffer.from('fake image');
- const fakeResponse = {
- ok: true,
- buffer: jest.fn().mockResolvedValue(fakeBuffer),
- };
- fetch.mockResolvedValue(fakeResponse);
-
- // Call the setup function and capture the verify callback for the regular 'openid' strategy
- // (not 'openidAdmin' which requires existing users)
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
- });
-
- it('should create a new user with correct username when preferred_username claim exists', async () => {
- // Arrange – our userinfo already has preferred_username 'testusername'
- const userinfo = tokenset.claims();
-
- // Act
- const { user } = await validate(tokenset);
-
- // Assert
- expect(user.username).toBe(userinfo.preferred_username);
- expect(createUser).toHaveBeenCalledWith(
- expect.objectContaining({
- provider: 'openid',
- openidId: userinfo.sub,
- username: userinfo.preferred_username,
- email: userinfo.email,
- name: `${userinfo.given_name} ${userinfo.family_name}`,
- }),
- { enabled: false },
- true,
- true,
- );
- });
-
- it('should use username as username when preferred_username claim is missing', async () => {
- // Arrange – remove preferred_username from userinfo
- const userinfo = { ...tokenset.claims() };
- delete userinfo.preferred_username;
- // Expect the username to be the "username"
- const expectUsername = userinfo.username;
-
- // Act
- const { user } = await validate({ ...tokenset, claims: () => userinfo });
-
- // Assert
- expect(user.username).toBe(expectUsername);
- expect(createUser).toHaveBeenCalledWith(
- expect.objectContaining({ username: expectUsername }),
- { enabled: false },
- true,
- true,
- );
- });
-
- it('should use email as username when username and preferred_username are missing', async () => {
- // Arrange – remove username and preferred_username
- const userinfo = { ...tokenset.claims() };
- delete userinfo.username;
- delete userinfo.preferred_username;
- const expectUsername = userinfo.email;
-
- // Act
- const { user } = await validate({ ...tokenset, claims: () => userinfo });
-
- // Assert
- expect(user.username).toBe(expectUsername);
- expect(createUser).toHaveBeenCalledWith(
- expect.objectContaining({ username: expectUsername }),
- { enabled: false },
- true,
- true,
- );
- });
-
- it('should override username with OPENID_USERNAME_CLAIM when set', async () => {
- // Arrange – set OPENID_USERNAME_CLAIM so that the sub claim is used
- process.env.OPENID_USERNAME_CLAIM = 'sub';
- const userinfo = tokenset.claims();
-
- // Act
- const { user } = await validate(tokenset);
-
- // Assert – username should equal the sub (converted as-is)
- expect(user.username).toBe(userinfo.sub);
- expect(createUser).toHaveBeenCalledWith(
- expect.objectContaining({ username: userinfo.sub }),
- { enabled: false },
- true,
- true,
- );
- });
-
- it('should set the full name correctly when given_name and family_name exist', async () => {
- // Arrange
- const userinfo = tokenset.claims();
- const expectedFullName = `${userinfo.given_name} ${userinfo.family_name}`;
-
- // Act
- const { user } = await validate(tokenset);
-
- // Assert
- expect(user.name).toBe(expectedFullName);
- });
-
- it('should override full name with OPENID_NAME_CLAIM when set', async () => {
- // Arrange – use the name claim as the full name
- process.env.OPENID_NAME_CLAIM = 'name';
- const userinfo = { ...tokenset.claims(), name: 'Custom Name' };
-
- // Act
- const { user } = await validate({ ...tokenset, claims: () => userinfo });
-
- // Assert
- expect(user.name).toBe('Custom Name');
- });
-
- it('should update an existing user on login', async () => {
- // Arrange – simulate that a user already exists with openid provider
- const existingUser = {
- _id: 'existingUserId',
- provider: 'openid',
- email: tokenset.claims().email,
- openidId: '',
- username: '',
- name: '',
- };
- findUser.mockImplementation(async (query) => {
- if (query.openidId === tokenset.claims().sub || query.email === tokenset.claims().email) {
- return existingUser;
- }
- return null;
- });
-
- const userinfo = tokenset.claims();
-
- // Act
- await validate(tokenset);
-
- // Assert – updateUser should be called and the user object updated
- expect(updateUser).toHaveBeenCalledWith(
- existingUser._id,
- expect.objectContaining({
- provider: 'openid',
- openidId: userinfo.sub,
- username: userinfo.preferred_username,
- name: `${userinfo.given_name} ${userinfo.family_name}`,
- }),
- );
- });
-
- it('should block login when email exists with different provider', async () => {
- // Arrange – simulate that a user exists with same email but different provider
- const existingUser = {
- _id: 'existingUserId',
- provider: 'google',
- email: tokenset.claims().email,
- googleId: 'some-google-id',
- username: 'existinguser',
- name: 'Existing User',
- };
- findUser.mockImplementation(async (query) => {
- if (query.email === tokenset.claims().email && !query.provider) {
- return existingUser;
- }
- return null;
- });
-
- // Act
- const result = await validate(tokenset);
-
- // Assert – verify that the strategy rejects login
- 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({
- roles: ['SomeOtherRole'],
- });
-
- // Act
- const { user, details } = await validate(tokenset);
-
- // Assert – verify that the strategy rejects login
- expect(user).toBe(false);
- expect(details.message).toBe('You must have "requiredRole" role to log in.');
- });
-
- it('should not treat substring matches in string roles as satisfying required role', async () => {
- // Arrange – override required role to "read" then re-setup
- process.env.OPENID_REQUIRED_ROLE = 'read';
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- // Token contains "bread" which *contains* "read" as a substring
- jwtDecode.mockReturnValue({
- roles: 'bread',
- });
-
- // Act
- const { user, details } = await validate(tokenset);
-
- // Assert – verify that substring match does not grant access
- expect(user).toBe(false);
- expect(details.message).toBe('You must have "read" role to log in.');
- });
-
- it('should allow login when roles claim is a space-separated string containing the required role', async () => {
- // Arrange – IdP returns roles as a space-delimited string
- jwtDecode.mockReturnValue({
- roles: 'role1 role2 requiredRole',
- });
-
- // Act
- const { user } = await validate(tokenset);
-
- // Assert – login succeeds when required role is present after splitting
- expect(user).toBeTruthy();
- expect(createUser).toHaveBeenCalled();
- });
-
- it('should allow login when roles claim is a comma-separated string containing the required role', async () => {
- // Arrange – IdP returns roles as a comma-delimited string
- jwtDecode.mockReturnValue({
- roles: 'role1,role2,requiredRole',
- });
-
- // Act
- const { user } = await validate(tokenset);
-
- // Assert – login succeeds when required role is present after splitting
- expect(user).toBeTruthy();
- expect(createUser).toHaveBeenCalled();
- });
-
- it('should allow login when roles claim is a mixed comma-and-space-separated string containing the required role', async () => {
- // Arrange – IdP returns roles with comma-and-space delimiters
- jwtDecode.mockReturnValue({
- roles: 'role1, role2, requiredRole',
- });
-
- // Act
- const { user } = await validate(tokenset);
-
- // Assert – login succeeds when required role is present after splitting
- expect(user).toBeTruthy();
- expect(createUser).toHaveBeenCalled();
- });
-
- it('should reject login when roles claim is a space-separated string that does not contain the required role', async () => {
- // Arrange – IdP returns a delimited string but required role is absent
- jwtDecode.mockReturnValue({
- roles: 'role1 role2 otherRole',
- });
-
- // Act
- const { user, details } = await validate(tokenset);
-
- // Assert – login is rejected with the correct error message
- expect(user).toBe(false);
- expect(details.message).toBe('You must have "requiredRole" role to log in.');
- });
-
- it('should allow login when single required role is present (backward compatibility)', async () => {
- // Arrange – ensure single role configuration (as set in beforeEach)
- // OPENID_REQUIRED_ROLE = 'requiredRole'
- // Default jwtDecode mock in beforeEach already returns this role
- jwtDecode.mockReturnValue({
- roles: ['requiredRole', 'anotherRole'],
- });
-
- // Act
- const { user } = await validate(tokenset);
-
- // Assert – verify that login succeeds with single role configuration
- expect(user).toBeTruthy();
- expect(user.email).toBe(tokenset.claims().email);
- expect(user.username).toBe(tokenset.claims().preferred_username);
- expect(createUser).toHaveBeenCalled();
- });
-
- describe('group overage and groups handling', () => {
- it.each([
- ['groups array contains required group', ['group-required', 'other-group'], true, undefined],
- [
- 'groups array missing required group',
- ['other-group'],
- false,
- 'You must have "group-required" role to log in.',
- ],
- ['groups string equals required group', 'group-required', true, undefined],
- [
- 'groups string is other group',
- 'other-group',
- false,
- 'You must have "group-required" role to log in.',
- ],
- ])(
- 'uses groups claim directly when %s (no overage)',
- async (_label, groupsClaim, expectedAllowed, expectedMessage) => {
- 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({
- groups: groupsClaim,
- permissions: ['admin'],
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user, details } = await validate(tokenset);
-
- expect(undici.fetch).not.toHaveBeenCalled();
- expect(Boolean(user)).toBe(expectedAllowed);
- expect(details?.message).toBe(expectedMessage);
- },
- );
-
- it.each([
- ['token kind is not id', { kind: 'access', path: 'groups', decoded: { hasgroups: true } }],
- ['parameter path is not groups', { kind: 'id', path: 'roles', decoded: { hasgroups: true } }],
- ['decoded token is falsy', { kind: 'id', path: 'groups', decoded: null }],
- [
- 'no overage indicators in decoded token',
- {
- kind: 'id',
- path: 'groups',
- decoded: {
- permissions: ['admin'],
- },
- },
- ],
- [
- 'only _claim_names present (no _claim_sources)',
- {
- kind: 'id',
- path: 'groups',
- decoded: {
- _claim_names: { groups: 'src1' },
- permissions: ['admin'],
- },
- },
- ],
- [
- 'only _claim_sources present (no _claim_names)',
- {
- kind: 'id',
- path: 'groups',
- decoded: {
- _claim_sources: { src1: { endpoint: 'https://graph.windows.net/ignored' } },
- permissions: ['admin'],
- },
- },
- ],
- ])('does not attempt overage resolution when %s', async (_label, cfg) => {
- process.env.OPENID_REQUIRED_ROLE = 'group-required';
- process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = cfg.path;
- process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = cfg.kind;
-
- jwtDecode.mockReturnValue(cfg.decoded);
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user, details } = await validate(tokenset);
-
- expect(undici.fetch).not.toHaveBeenCalled();
- expect(user).toBe(false);
- expect(details.message).toBe('You must have "group-required" role to log in.');
- const { logger } = require('@librechat/data-schemas');
- const expectedTokenKind = cfg.kind === 'access' ? 'access token' : 'id token';
- expect(logger.error).toHaveBeenCalledWith(
- expect.stringContaining(`Key '${cfg.path}' not found in ${expectedTokenKind}!`),
- );
- });
- });
-
- describe('resolving groups via Microsoft Graph', () => {
- it('denies login and does not call Graph when access token is missing', async () => {
- process.env.OPENID_REQUIRED_ROLE = 'group-required';
- process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'groups';
- process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id';
-
- const { logger } = require('@librechat/data-schemas');
-
- jwtDecode.mockReturnValue({
- hasgroups: true,
- permissions: ['admin'],
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const tokensetWithoutAccess = {
- ...tokenset,
- access_token: undefined,
- };
-
- const { user, details } = await validate(tokensetWithoutAccess);
-
- expect(user).toBe(false);
- expect(details.message).toBe('You must have "group-required" role to log in.');
-
- expect(undici.fetch).not.toHaveBeenCalled();
- expect(logger.error).toHaveBeenCalledWith(
- expect.stringContaining('Access token missing; cannot resolve group overage'),
- );
- });
-
- it.each([
- [
- 'Graph returns HTTP error',
- async () => ({
- ok: false,
- status: 403,
- statusText: 'Forbidden',
- json: async () => ({}),
- }),
- [
- '[openidStrategy] Failed to resolve groups via Microsoft Graph getMemberObjects: HTTP 403 Forbidden',
- ],
- ],
- [
- 'Graph network error',
- async () => {
- throw new Error('network error');
- },
- [
- '[openidStrategy] Error resolving groups via Microsoft Graph getMemberObjects:',
- expect.any(Error),
- ],
- ],
- [
- 'Graph returns unexpected shape (no value)',
- async () => ({
- ok: true,
- status: 200,
- statusText: 'OK',
- json: async () => ({}),
- }),
- [
- '[openidStrategy] Unexpected response format when resolving groups via Microsoft Graph getMemberObjects',
- ],
- ],
- [
- 'Graph returns invalid value type',
- async () => ({
- ok: true,
- status: 200,
- statusText: 'OK',
- json: async () => ({ value: 'not-an-array' }),
- }),
- [
- '[openidStrategy] Unexpected response format when resolving groups via Microsoft Graph getMemberObjects',
- ],
- ],
- ])(
- 'denies login when overage resolution fails because %s',
- async (_label, setupFetch, expectedErrorArgs) => {
- process.env.OPENID_REQUIRED_ROLE = 'group-required';
- process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'groups';
- process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id';
-
- const { logger } = require('@librechat/data-schemas');
-
- jwtDecode.mockReturnValue({
- hasgroups: true,
- permissions: ['admin'],
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- undici.fetch.mockImplementation(setupFetch);
-
- const { user, details } = await validate(tokenset);
-
- expect(undici.fetch).toHaveBeenCalled();
- expect(user).toBe(false);
- expect(details.message).toBe('You must have "group-required" role to log in.');
-
- expect(logger.error).toHaveBeenCalledWith(...expectedErrorArgs);
- },
- );
-
- it.each([
- [
- 'hasgroups overage and Graph contains required group',
- {
- hasgroups: true,
- },
- ['group-required', 'some-other-group'],
- true,
- ],
- [
- '_claim_* overage and Graph contains required group',
- {
- _claim_names: { groups: 'src1' },
- _claim_sources: { src1: { endpoint: 'https://graph.windows.net/ignored' } },
- },
- ['group-required', 'some-other-group'],
- true,
- ],
- [
- 'hasgroups overage and Graph does NOT contain required group',
- {
- hasgroups: true,
- },
- ['some-other-group'],
- false,
- ],
- [
- '_claim_* overage and Graph does NOT contain required group',
- {
- _claim_names: { groups: 'src1' },
- _claim_sources: { src1: { endpoint: 'https://graph.windows.net/ignored' } },
- },
- ['some-other-group'],
- false,
- ],
- ])(
- 'resolves groups via Microsoft Graph when %s',
- async (_label, decodedTokenValue, graphGroups, expectedAllowed) => {
- process.env.OPENID_REQUIRED_ROLE = 'group-required';
- process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'groups';
- process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id';
-
- const { logger } = require('@librechat/data-schemas');
-
- jwtDecode.mockReturnValue(decodedTokenValue);
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- undici.fetch.mockResolvedValue({
- ok: true,
- status: 200,
- statusText: 'OK',
- json: async () => ({
- value: graphGroups,
- }),
- });
-
- const { user } = await validate(tokenset);
-
- expect(undici.fetch).toHaveBeenCalledWith(
- 'https://graph.microsoft.com/v1.0/me/getMemberObjects',
- expect.objectContaining({
- method: 'POST',
- headers: expect.objectContaining({
- Authorization: 'Bearer exchanged_graph_token',
- }),
- }),
- );
- expect(Boolean(user)).toBe(expectedAllowed);
-
- expect(logger.debug).toHaveBeenCalledWith(
- expect.stringContaining(
- `Successfully resolved ${graphGroups.length} groups via Microsoft Graph getMemberObjects`,
- ),
- );
- },
- );
- });
-
- 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);
-
- // Assert – verify that download was attempted and the avatar field was set via updateUser
- expect(fetch).toHaveBeenCalled();
- // Our mock getStrategyFunctions.saveBuffer returns '/fake/path/to/avatar.png'
- expect(user.avatar).toBe('/fake/path/to/avatar.png');
- });
-
- it('should not attempt to download avatar if picture is not provided', async () => {
- // Arrange – remove picture
- const userinfo = { ...tokenset.claims() };
- delete userinfo.picture;
-
- // Act
- await validate({ ...tokenset, claims: () => userinfo });
-
- // Assert – fetch should not be called and avatar should remain undefined or empty
- expect(fetch).not.toHaveBeenCalled();
- // Depending on your implementation, user.avatar may be undefined or an empty string.
- });
-
- it('should support comma-separated multiple roles', async () => {
- // Arrange
- process.env.OPENID_REQUIRED_ROLE = 'someRole,anotherRole,admin';
- await setupOpenId(); // Re-initialize the strategy
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
- jwtDecode.mockReturnValue({
- roles: ['anotherRole', 'aThirdRole'],
- });
-
- // Act
- const { user } = await validate(tokenset);
-
- // Assert
- expect(user).toBeTruthy();
- expect(user.email).toBe(tokenset.claims().email);
- });
-
- it('should reject login when user has none of the required multiple roles', async () => {
- // Arrange
- process.env.OPENID_REQUIRED_ROLE = 'someRole,anotherRole,admin';
- await setupOpenId(); // Re-initialize the strategy
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
- jwtDecode.mockReturnValue({
- roles: ['aThirdRole', 'aFourthRole'],
- });
-
- // Act
- const { user, details } = await validate(tokenset);
-
- // Assert
- expect(user).toBe(false);
- expect(details.message).toBe(
- 'You must have one of: "someRole", "anotherRole", "admin" role to log in.',
- );
- });
-
- it('should handle spaces in comma-separated roles', async () => {
- // Arrange
- process.env.OPENID_REQUIRED_ROLE = ' someRole , anotherRole , admin ';
- await setupOpenId(); // Re-initialize the strategy
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
- jwtDecode.mockReturnValue({
- roles: ['someRole'],
- });
-
- // Act
- const { user } = await validate(tokenset);
-
- // Assert
- expect(user).toBeTruthy();
- });
-
- it('should default to usePKCE false when OPENID_USE_PKCE is not defined', async () => {
- const OpenIDStrategy = require('openid-client/passport').Strategy;
-
- delete process.env.OPENID_USE_PKCE;
- await setupOpenId();
-
- const callOptions = OpenIDStrategy.mock.calls[OpenIDStrategy.mock.calls.length - 1][0];
- expect(callOptions.usePKCE).toBe(false);
- expect(callOptions.params?.code_challenge_method).toBeUndefined();
- });
-
- it('should attach federatedTokens to user object for token propagation', async () => {
- // Arrange - setup tokenset with access token, id token, refresh token, and expiration
- const tokensetWithTokens = {
- ...tokenset,
- access_token: 'mock_access_token_abc123',
- id_token: 'mock_id_token_def456',
- refresh_token: 'mock_refresh_token_xyz789',
- expires_at: 1234567890,
- };
-
- // Act - validate with the tokenset containing tokens
- const { user } = await validate(tokensetWithTokens);
-
- // Assert - verify federatedTokens object is attached with correct values
- expect(user.federatedTokens).toBeDefined();
- expect(user.federatedTokens).toEqual({
- access_token: 'mock_access_token_abc123',
- id_token: 'mock_id_token_def456',
- refresh_token: 'mock_refresh_token_xyz789',
- expires_at: 1234567890,
- });
- });
-
- it('should include id_token in federatedTokens distinct from access_token', async () => {
- // Arrange - use different values for access_token and id_token
- const tokensetWithTokens = {
- ...tokenset,
- access_token: 'the_access_token',
- id_token: 'the_id_token',
- refresh_token: 'the_refresh_token',
- expires_at: 9999999999,
- };
-
- // Act
- const { user } = await validate(tokensetWithTokens);
-
- // Assert - id_token and access_token must be different values
- expect(user.federatedTokens.access_token).toBe('the_access_token');
- expect(user.federatedTokens.id_token).toBe('the_id_token');
- expect(user.federatedTokens.id_token).not.toBe(user.federatedTokens.access_token);
- });
-
- it('should include tokenset along with federatedTokens', async () => {
- // Arrange
- const tokensetWithTokens = {
- ...tokenset,
- access_token: 'test_access_token',
- id_token: 'test_id_token',
- refresh_token: 'test_refresh_token',
- expires_at: 9999999999,
- };
-
- // Act
- const { user } = await validate(tokensetWithTokens);
-
- // Assert - both tokenset and federatedTokens should be present
- expect(user.tokenset).toBeDefined();
- expect(user.federatedTokens).toBeDefined();
- expect(user.tokenset.access_token).toBe('test_access_token');
- expect(user.tokenset.id_token).toBe('test_id_token');
- expect(user.federatedTokens.access_token).toBe('test_access_token');
- expect(user.federatedTokens.id_token).toBe('test_id_token');
- });
-
- it('should set role to "ADMIN" if OPENID_ADMIN_ROLE is set and user has that role', async () => {
- // Act
- const { user } = await validate(tokenset);
-
- // Assert – verify that the user role is set to "ADMIN"
- expect(user.role).toBe('ADMIN');
- });
-
- it('should not set user role if OPENID_ADMIN_ROLE is set but the user does not have that role', async () => {
- // Arrange – simulate a token without the admin permission
- jwtDecode.mockReturnValue({
- roles: ['requiredRole'],
- permissions: ['not-admin'],
- });
-
- // Act
- const { user } = await validate(tokenset);
-
- // Assert – verify that the user role is not defined
- expect(user.role).toBeUndefined();
- });
-
- it('should demote existing admin user when admin role is removed from token', async () => {
- // Arrange – simulate an existing user who is currently an admin
- 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;
- });
-
- // Token without admin permission
- jwtDecode.mockReturnValue({
- roles: ['requiredRole'],
- permissions: ['not-admin'],
- });
-
- const { logger } = require('@librechat/data-schemas');
-
- // Act
- const { user } = await validate(tokenset);
-
- // Assert – verify that the user was demoted
- expect(user.role).toBe('USER');
- expect(updateUser).toHaveBeenCalledWith(
- existingAdminUser._id,
- expect.objectContaining({
- role: 'USER',
- }),
- );
- expect(logger.info).toHaveBeenCalledWith(
- expect.stringContaining('demoted from admin - role no longer present in token'),
- );
- });
-
- it('should NOT demote admin user when admin role env vars are not configured', async () => {
- // Arrange – remove admin role env vars
- delete process.env.OPENID_ADMIN_ROLE;
- delete process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH;
- delete process.env.OPENID_ADMIN_ROLE_TOKEN_KIND;
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- // Simulate an existing admin user
- 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({
- roles: ['requiredRole'],
- });
-
- // Act
- const { user } = await validate(tokenset);
-
- // Assert – verify that the admin user was NOT demoted
- expect(user.role).toBe('ADMIN');
- expect(updateUser).toHaveBeenCalledWith(
- existingAdminUser._id,
- expect.objectContaining({
- role: 'ADMIN',
- }),
- );
- });
-
- describe('lodash get - nested path extraction', () => {
- it('should extract roles from deeply nested token path', async () => {
- process.env.OPENID_REQUIRED_ROLE = 'app-user';
- process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'resource_access.my-client.roles';
-
- jwtDecode.mockReturnValue({
- resource_access: {
- 'my-client': {
- roles: ['app-user', 'viewer'],
- },
- },
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user } = await validate(tokenset);
-
- expect(user).toBeTruthy();
- expect(user.email).toBe(tokenset.claims().email);
- });
-
- it('should extract roles from three-level nested path', async () => {
- process.env.OPENID_REQUIRED_ROLE = 'editor';
- process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'data.access.permissions.roles';
-
- jwtDecode.mockReturnValue({
- data: {
- access: {
- permissions: {
- roles: ['editor', 'reader'],
- },
- },
- },
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user } = await validate(tokenset);
-
- expect(user).toBeTruthy();
- });
-
- it('should log error and reject login when required role path does not exist in token', async () => {
- const { logger } = require('@librechat/data-schemas');
- process.env.OPENID_REQUIRED_ROLE = 'app-user';
- process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'resource_access.nonexistent.roles';
-
- jwtDecode.mockReturnValue({
- resource_access: {
- 'my-client': {
- roles: ['app-user'],
- },
- },
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user, details } = await validate(tokenset);
-
- expect(logger.error).toHaveBeenCalledWith(
- expect.stringContaining("Key 'resource_access.nonexistent.roles' not found in id token!"),
- );
- expect(user).toBe(false);
- expect(details.message).toContain('role to log in');
- });
-
- it('should handle missing intermediate nested path gracefully', async () => {
- const { logger } = require('@librechat/data-schemas');
- process.env.OPENID_REQUIRED_ROLE = 'user';
- process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'org.team.roles';
-
- jwtDecode.mockReturnValue({
- org: {
- other: 'value',
- },
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user } = await validate(tokenset);
-
- expect(logger.error).toHaveBeenCalledWith(
- expect.stringContaining("Key 'org.team.roles' not found in id token!"),
- );
- expect(user).toBe(false);
- });
-
- it('should extract admin role from nested path in access token', async () => {
- process.env.OPENID_ADMIN_ROLE = 'admin';
- process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'realm_access.roles';
- process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'access';
-
- jwtDecode.mockImplementation((token) => {
- if (token === 'fake_access_token') {
- return {
- realm_access: {
- roles: ['admin', 'user'],
- },
- };
- }
- return {
- roles: ['requiredRole'],
- };
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user } = await validate(tokenset);
-
- expect(user.role).toBe('ADMIN');
- });
-
- it('should extract admin role from nested path in userinfo', async () => {
- process.env.OPENID_ADMIN_ROLE = 'admin';
- process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'organization.permissions';
- process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'userinfo';
-
- const userinfoWithNestedGroups = {
- ...tokenset.claims(),
- organization: {
- permissions: ['admin', 'write'],
- },
- };
-
- require('openid-client').fetchUserInfo.mockResolvedValue({
- organization: {
- permissions: ['admin', 'write'],
- },
- });
-
- jwtDecode.mockReturnValue({
- roles: ['requiredRole'],
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user } = await validate({
- ...tokenset,
- claims: () => userinfoWithNestedGroups,
- });
-
- expect(user.role).toBe('ADMIN');
- });
-
- it('should handle boolean admin role value', async () => {
- process.env.OPENID_ADMIN_ROLE = 'admin';
- process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'is_admin';
-
- jwtDecode.mockReturnValue({
- roles: ['requiredRole'],
- is_admin: true,
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user } = await validate(tokenset);
-
- expect(user.role).toBe('ADMIN');
- });
-
- it('should handle string admin role value matching exactly', async () => {
- process.env.OPENID_ADMIN_ROLE = 'super-admin';
- process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'role';
-
- jwtDecode.mockReturnValue({
- roles: ['requiredRole'],
- role: 'super-admin',
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user } = await validate(tokenset);
-
- expect(user.role).toBe('ADMIN');
- });
-
- it('should not set admin role when string value does not match', async () => {
- process.env.OPENID_ADMIN_ROLE = 'super-admin';
- process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'role';
-
- jwtDecode.mockReturnValue({
- roles: ['requiredRole'],
- role: 'regular-user',
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user } = await validate(tokenset);
-
- expect(user.role).toBeUndefined();
- });
-
- it('should handle array admin role value', async () => {
- process.env.OPENID_ADMIN_ROLE = 'site-admin';
- process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'app_roles';
-
- jwtDecode.mockReturnValue({
- roles: ['requiredRole'],
- app_roles: ['user', 'site-admin', 'moderator'],
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user } = await validate(tokenset);
-
- expect(user.role).toBe('ADMIN');
- });
-
- it('should not set admin when role is not in array', async () => {
- process.env.OPENID_ADMIN_ROLE = 'site-admin';
- process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'app_roles';
-
- jwtDecode.mockReturnValue({
- roles: ['requiredRole'],
- app_roles: ['user', 'moderator'],
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user } = await validate(tokenset);
-
- expect(user.role).toBeUndefined();
- });
-
- it('should grant admin when admin role claim is a space-separated string containing the admin role', async () => {
- // Arrange – IdP returns admin roles as a space-delimited string
- process.env.OPENID_ADMIN_ROLE = 'site-admin';
- process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'app_roles';
-
- jwtDecode.mockReturnValue({
- roles: ['requiredRole'],
- app_roles: 'user site-admin moderator',
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- // Act
- const { user } = await validate(tokenset);
-
- // Assert – admin role is granted after splitting the delimited string
- expect(user.role).toBe('ADMIN');
- });
-
- it('should not grant admin when admin role claim is a space-separated string that does not contain the admin role', async () => {
- // Arrange – delimited string present but admin role is absent
- process.env.OPENID_ADMIN_ROLE = 'site-admin';
- process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'app_roles';
-
- jwtDecode.mockReturnValue({
- roles: ['requiredRole'],
- app_roles: 'user moderator',
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- // Act
- const { user } = await validate(tokenset);
-
- // Assert – admin role is not granted
- expect(user.role).toBeUndefined();
- });
-
- it('should handle nested path with special characters in keys', async () => {
- process.env.OPENID_REQUIRED_ROLE = 'app-user';
- process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'resource_access.my-app-123.roles';
-
- jwtDecode.mockReturnValue({
- resource_access: {
- 'my-app-123': {
- roles: ['app-user'],
- },
- },
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user } = await validate(tokenset);
-
- expect(user).toBeTruthy();
- });
-
- it('should handle empty object at nested path', async () => {
- const { logger } = require('@librechat/data-schemas');
- process.env.OPENID_REQUIRED_ROLE = 'user';
- process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'access.roles';
-
- jwtDecode.mockReturnValue({
- access: {},
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user } = await validate(tokenset);
-
- expect(logger.error).toHaveBeenCalledWith(
- expect.stringContaining("Key 'access.roles' not found in id token!"),
- );
- expect(user).toBe(false);
- });
-
- it('should handle null value at intermediate path', async () => {
- const { logger } = require('@librechat/data-schemas');
- process.env.OPENID_REQUIRED_ROLE = 'user';
- process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'data.roles';
-
- jwtDecode.mockReturnValue({
- data: null,
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user } = await validate(tokenset);
-
- expect(logger.error).toHaveBeenCalledWith(
- expect.stringContaining("Key 'data.roles' not found in id token!"),
- );
- expect(user).toBe(false);
- });
-
- it('should reject login with invalid admin role token kind', async () => {
- process.env.OPENID_ADMIN_ROLE = 'admin';
- process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'roles';
- process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'invalid';
-
- const { logger } = require('@librechat/data-schemas');
-
- jwtDecode.mockReturnValue({
- roles: ['requiredRole', 'admin'],
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- await expect(validate(tokenset)).rejects.toThrow('Invalid admin role token kind');
-
- expect(logger.error).toHaveBeenCalledWith(
- expect.stringContaining(
- "Invalid admin role token kind: invalid. Must be one of 'access', 'id', or 'userinfo'",
- ),
- );
- });
-
- it('should reject login when roles path returns invalid type (object)', async () => {
- const { logger } = require('@librechat/data-schemas');
- process.env.OPENID_REQUIRED_ROLE = 'app-user';
- process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'roles';
-
- jwtDecode.mockReturnValue({
- roles: { admin: true, user: false },
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user, details } = await validate(tokenset);
-
- expect(logger.error).toHaveBeenCalledWith(
- expect.stringContaining("Key 'roles' not found in id token!"),
- );
- expect(user).toBe(false);
- expect(details.message).toContain('role to log in');
- });
-
- it('should reject login when roles path returns invalid type (number)', async () => {
- const { logger } = require('@librechat/data-schemas');
- process.env.OPENID_REQUIRED_ROLE = 'user';
- process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'roleCount';
-
- jwtDecode.mockReturnValue({
- roleCount: 5,
- });
-
- await setupOpenId();
- verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
-
- const { user } = await validate(tokenset);
-
- expect(logger.error).toHaveBeenCalledWith(
- expect.stringContaining("Key 'roleCount' not found in id token!"),
- );
- expect(user).toBe(false);
- });
- });
-
- describe('OPENID_EMAIL_CLAIM', () => {
- it('should use the default email when OPENID_EMAIL_CLAIM is not set', async () => {
- const { user } = await validate(tokenset);
- expect(user.email).toBe('test@example.com');
- });
-
- it('should use the configured claim when OPENID_EMAIL_CLAIM is set', async () => {
- process.env.OPENID_EMAIL_CLAIM = 'upn';
- const userinfo = { ...tokenset.claims(), upn: 'user@corp.example.com' };
-
- const { user } = await validate({ ...tokenset, claims: () => userinfo });
-
- expect(user.email).toBe('user@corp.example.com');
- expect(createUser).toHaveBeenCalledWith(
- expect.objectContaining({ email: 'user@corp.example.com' }),
- expect.anything(),
- true,
- true,
- );
- });
-
- it('should fall back to preferred_username when email is missing and OPENID_EMAIL_CLAIM is not set', async () => {
- const userinfo = { ...tokenset.claims() };
- delete userinfo.email;
-
- const { user } = await validate({ ...tokenset, claims: () => userinfo });
-
- expect(user.email).toBe('testusername');
- });
-
- it('should fall back to upn when email and preferred_username are missing and OPENID_EMAIL_CLAIM is not set', async () => {
- const userinfo = { ...tokenset.claims(), upn: 'user@corp.example.com' };
- delete userinfo.email;
- delete userinfo.preferred_username;
-
- const { user } = await validate({ ...tokenset, claims: () => userinfo });
-
- expect(user.email).toBe('user@corp.example.com');
- });
-
- it('should ignore empty string OPENID_EMAIL_CLAIM and use default fallback', async () => {
- process.env.OPENID_EMAIL_CLAIM = '';
-
- const { user } = await validate(tokenset);
-
- expect(user.email).toBe('test@example.com');
- });
-
- it('should trim whitespace from OPENID_EMAIL_CLAIM and resolve correctly', async () => {
- process.env.OPENID_EMAIL_CLAIM = ' upn ';
- const userinfo = { ...tokenset.claims(), upn: 'user@corp.example.com' };
-
- const { user } = await validate({ ...tokenset, claims: () => userinfo });
-
- expect(user.email).toBe('user@corp.example.com');
- });
-
- it('should ignore whitespace-only OPENID_EMAIL_CLAIM and use default fallback', async () => {
- process.env.OPENID_EMAIL_CLAIM = ' ';
-
- const { user } = await validate(tokenset);
-
- expect(user.email).toBe('test@example.com');
- });
-
- it('should fall back to default chain with warning when configured claim is missing from userinfo', async () => {
- const { logger } = require('@librechat/data-schemas');
- process.env.OPENID_EMAIL_CLAIM = 'nonexistent_claim';
-
- const { user } = await validate(tokenset);
-
- expect(user.email).toBe('test@example.com');
- expect(logger.warn).toHaveBeenCalledWith(
- expect.stringContaining('OPENID_EMAIL_CLAIM="nonexistent_claim" not present in userinfo'),
- );
- });
- });
-});
+const undici = require('undici');
+const fetch = require('node-fetch');
+const jwtDecode = require('jsonwebtoken/decode');
+const { ErrorTypes } = require('librechat-data-provider');
+const { findUser, createUser, updateUser } = require('~/models');
+const { resolveAppConfigForUser } = require('@librechat/api');
+const { getAppConfig } = require('~/server/services/Config');
+const { setupOpenId } = require('./openidStrategy');
+
+// --- Mocks ---
+jest.mock('node-fetch');
+jest.mock('jsonwebtoken/decode');
+jest.mock('undici', () => ({
+ fetch: jest.fn(),
+ ProxyAgent: jest.fn(),
+}));
+jest.mock('~/server/services/Files/strategies', () => ({
+ getStrategyFunctions: jest.fn(() => ({
+ saveBuffer: jest.fn().mockResolvedValue('/fake/path/to/avatar.png'),
+ })),
+}));
+jest.mock('~/server/services/Config', () => ({
+ getAppConfig: jest.fn().mockResolvedValue({}),
+}));
+jest.mock('@librechat/api', () => ({
+ ...jest.requireActual('@librechat/api'),
+ isEnabled: jest.fn(() => false),
+ isEmailDomainAllowed: jest.fn(() => true),
+ findOpenIDUser: jest.requireActual('@librechat/api').findOpenIDUser,
+ getBalanceConfig: jest.fn(() => ({
+ enabled: false,
+ })),
+ resolveAppConfigForUser: jest.fn(async (_getAppConfig, _user) => ({})),
+}));
+jest.mock('~/models', () => ({
+ findUser: jest.fn(),
+ createUser: jest.fn(),
+ updateUser: jest.fn(),
+}));
+jest.mock('@librechat/data-schemas', () => ({
+ ...jest.requireActual('@librechat/api'),
+ logger: {
+ info: jest.fn(),
+ warn: jest.fn(),
+ debug: jest.fn(),
+ error: jest.fn(),
+ },
+ hashToken: jest.fn().mockResolvedValue('hashed-token'),
+}));
+jest.mock('~/cache/getLogStores', () =>
+ jest.fn(() => ({
+ get: jest.fn(),
+ set: jest.fn(),
+ })),
+);
+
+// Mock the openid-client module and all its dependencies
+jest.mock('openid-client', () => {
+ return {
+ discovery: jest.fn().mockResolvedValue({
+ clientId: 'fake_client_id',
+ clientSecret: 'fake_client_secret',
+ issuer: 'https://fake-issuer.com',
+ // Add any other properties needed by the implementation
+ }),
+ fetchUserInfo: jest.fn().mockImplementation(() => {
+ // 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'),
+ };
+});
+
+jest.mock('openid-client/passport', () => {
+ /** Store callbacks by strategy name - 'openid' and 'openidAdmin' */
+ const verifyCallbacks = {};
+ let lastVerifyCallback;
+
+ const mockStrategy = jest.fn((options, verify) => {
+ lastVerifyCallback = verify;
+ return { name: 'openid', options, verify };
+ });
+
+ return {
+ Strategy: mockStrategy,
+ /** Get the last registered callback (for backward compatibility) */
+ __getVerifyCallback: () => lastVerifyCallback,
+ /** Store callback by name when passport.use is called */
+ __setVerifyCallback: (name, callback) => {
+ verifyCallbacks[name] = callback;
+ },
+ /** Get callback by strategy name */
+ __getVerifyCallbackByName: (name) => verifyCallbacks[name],
+ };
+});
+
+// Mock passport - capture strategy name and callback
+jest.mock('passport', () => ({
+ use: jest.fn((name, strategy) => {
+ const passportMock = require('openid-client/passport');
+ if (strategy && strategy.verify) {
+ passportMock.__setVerifyCallback(name, strategy.verify);
+ }
+ }),
+}));
+
+describe('setupOpenId', () => {
+ // Store a reference to the verify callback once it's set up
+ let verifyCallback;
+
+ // Helper to wrap the verify callback in a promise
+ const validate = (tokenset) =>
+ new Promise((resolve, reject) => {
+ verifyCallback(tokenset, (err, user, details) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve({ user, details });
+ }
+ });
+ });
+
+ const tokenset = {
+ id_token: 'fake_id_token',
+ access_token: 'fake_access_token',
+ claims: () => ({
+ sub: '1234',
+ email: 'test@example.com',
+ email_verified: true,
+ given_name: 'First',
+ family_name: 'Last',
+ name: 'My Full',
+ preferred_username: 'testusername',
+ username: 'flast',
+ picture: 'https://example.com/avatar.png',
+ }),
+ };
+
+ beforeEach(async () => {
+ // Clear previous mock calls and reset implementations
+ jest.clearAllMocks();
+
+ // Reset environment variables needed by the strategy
+ process.env.OPENID_ISSUER = 'https://fake-issuer.com';
+ process.env.OPENID_CLIENT_ID = 'fake_client_id';
+ process.env.OPENID_CLIENT_SECRET = 'fake_client_secret';
+ process.env.DOMAIN_SERVER = 'https://example.com';
+ process.env.OPENID_CALLBACK_URL = '/callback';
+ process.env.OPENID_SCOPE = 'openid profile email';
+ 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 = 'permissions';
+ process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'id';
+ delete process.env.OPENID_USERNAME_CLAIM;
+ delete process.env.OPENID_NAME_CLAIM;
+ delete process.env.OPENID_EMAIL_CLAIM;
+ delete process.env.PROXY;
+ delete process.env.OPENID_USE_PKCE;
+
+ // Default jwtDecode mock returns a token that includes the required role.
+ jwtDecode.mockReturnValue({
+ roles: ['requiredRole'],
+ permissions: ['admin'],
+ });
+
+ // By default, assume that no user is found, so createUser will be called
+ findUser.mockResolvedValue(null);
+ createUser.mockImplementation(async (userData) => {
+ // simulate created user with an _id property
+ return { _id: 'newUserId', ...userData };
+ });
+ updateUser.mockImplementation(async (id, userData) => {
+ return { _id: id, ...userData };
+ });
+
+ // For image download, simulate a successful response
+ const fakeBuffer = Buffer.from('fake image');
+ const fakeResponse = {
+ ok: true,
+ buffer: jest.fn().mockResolvedValue(fakeBuffer),
+ };
+ fetch.mockResolvedValue(fakeResponse);
+
+ // Call the setup function and capture the verify callback for the regular 'openid' strategy
+ // (not 'openidAdmin' which requires existing users)
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+ });
+
+ it('should create a new user with correct username when preferred_username claim exists', async () => {
+ // Arrange – our userinfo already has preferred_username 'testusername'
+ const userinfo = tokenset.claims();
+
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert
+ expect(user.username).toBe(userinfo.preferred_username);
+ expect(createUser).toHaveBeenCalledWith(
+ expect.objectContaining({
+ provider: 'openid',
+ openidId: userinfo.sub,
+ username: userinfo.preferred_username,
+ email: userinfo.email,
+ name: `${userinfo.given_name} ${userinfo.family_name}`,
+ }),
+ { enabled: false },
+ true,
+ true,
+ );
+ });
+
+ it('should use username as username when preferred_username claim is missing', async () => {
+ // Arrange – remove preferred_username from userinfo
+ const userinfo = { ...tokenset.claims() };
+ delete userinfo.preferred_username;
+ // Expect the username to be the "username"
+ const expectUsername = userinfo.username;
+
+ // Act
+ const { user } = await validate({ ...tokenset, claims: () => userinfo });
+
+ // Assert
+ expect(user.username).toBe(expectUsername);
+ expect(createUser).toHaveBeenCalledWith(
+ expect.objectContaining({ username: expectUsername }),
+ { enabled: false },
+ true,
+ true,
+ );
+ });
+
+ it('should use email as username when username and preferred_username are missing', async () => {
+ // Arrange – remove username and preferred_username
+ const userinfo = { ...tokenset.claims() };
+ delete userinfo.username;
+ delete userinfo.preferred_username;
+ const expectUsername = userinfo.email;
+
+ // Act
+ const { user } = await validate({ ...tokenset, claims: () => userinfo });
+
+ // Assert
+ expect(user.username).toBe(expectUsername);
+ expect(createUser).toHaveBeenCalledWith(
+ expect.objectContaining({ username: expectUsername }),
+ { enabled: false },
+ true,
+ true,
+ );
+ });
+
+ it('should override username with OPENID_USERNAME_CLAIM when set', async () => {
+ // Arrange – set OPENID_USERNAME_CLAIM so that the sub claim is used
+ process.env.OPENID_USERNAME_CLAIM = 'sub';
+ const userinfo = tokenset.claims();
+
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert – username should equal the sub (converted as-is)
+ expect(user.username).toBe(userinfo.sub);
+ expect(createUser).toHaveBeenCalledWith(
+ expect.objectContaining({ username: userinfo.sub }),
+ { enabled: false },
+ true,
+ true,
+ );
+ });
+
+ it('should set the full name correctly when given_name and family_name exist', async () => {
+ // Arrange
+ const userinfo = tokenset.claims();
+ const expectedFullName = `${userinfo.given_name} ${userinfo.family_name}`;
+
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert
+ expect(user.name).toBe(expectedFullName);
+ });
+
+ it('should override full name with OPENID_NAME_CLAIM when set', async () => {
+ // Arrange – use the name claim as the full name
+ process.env.OPENID_NAME_CLAIM = 'name';
+ const userinfo = { ...tokenset.claims(), name: 'Custom Name' };
+
+ // Act
+ const { user } = await validate({ ...tokenset, claims: () => userinfo });
+
+ // Assert
+ expect(user.name).toBe('Custom Name');
+ });
+
+ it('should update an existing user on login', async () => {
+ // Arrange – simulate that a user already exists with openid provider
+ const existingUser = {
+ _id: 'existingUserId',
+ provider: 'openid',
+ email: tokenset.claims().email,
+ openidId: '',
+ username: '',
+ name: '',
+ };
+ findUser.mockImplementation(async (query) => {
+ if (query.openidId === tokenset.claims().sub || query.email === tokenset.claims().email) {
+ return existingUser;
+ }
+ return null;
+ });
+
+ const userinfo = tokenset.claims();
+
+ // Act
+ await validate(tokenset);
+
+ // Assert – updateUser should be called and the user object updated
+ expect(updateUser).toHaveBeenCalledWith(
+ existingUser._id,
+ expect.objectContaining({
+ provider: 'openid',
+ openidId: userinfo.sub,
+ username: userinfo.preferred_username,
+ name: `${userinfo.given_name} ${userinfo.family_name}`,
+ }),
+ );
+ });
+
+ it('should block login when email exists with different provider', async () => {
+ // Arrange – simulate that a user exists with same email but different provider
+ const existingUser = {
+ _id: 'existingUserId',
+ provider: 'google',
+ email: tokenset.claims().email,
+ googleId: 'some-google-id',
+ username: 'existinguser',
+ name: 'Existing User',
+ };
+ findUser.mockImplementation(async (query) => {
+ if (query.email === tokenset.claims().email && !query.provider) {
+ return existingUser;
+ }
+ return null;
+ });
+
+ // Act
+ const result = await validate(tokenset);
+
+ // Assert – verify that the strategy rejects login
+ expect(result.user).toBe(false);
+ expect(result.details.message).toBe(ErrorTypes.AUTH_FAILED);
+ expect(createUser).not.toHaveBeenCalled();
+ 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({
+ roles: ['SomeOtherRole'],
+ });
+
+ // Act
+ const { user, details } = await validate(tokenset);
+
+ // Assert – verify that the strategy rejects login
+ expect(user).toBe(false);
+ expect(details.message).toBe('You must have "requiredRole" role to log in.');
+ });
+
+ it('should not treat substring matches in string roles as satisfying required role', async () => {
+ // Arrange – override required role to "read" then re-setup
+ process.env.OPENID_REQUIRED_ROLE = 'read';
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ // Token contains "bread" which *contains* "read" as a substring
+ jwtDecode.mockReturnValue({
+ roles: 'bread',
+ });
+
+ // Act
+ const { user, details } = await validate(tokenset);
+
+ // Assert – verify that substring match does not grant access
+ expect(user).toBe(false);
+ expect(details.message).toBe('You must have "read" role to log in.');
+ });
+
+ it('should allow login when roles claim is a space-separated string containing the required role', async () => {
+ // Arrange – IdP returns roles as a space-delimited string
+ jwtDecode.mockReturnValue({
+ roles: 'role1 role2 requiredRole',
+ });
+
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert – login succeeds when required role is present after splitting
+ expect(user).toBeTruthy();
+ expect(createUser).toHaveBeenCalled();
+ });
+
+ it('should allow login when roles claim is a comma-separated string containing the required role', async () => {
+ // Arrange – IdP returns roles as a comma-delimited string
+ jwtDecode.mockReturnValue({
+ roles: 'role1,role2,requiredRole',
+ });
+
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert – login succeeds when required role is present after splitting
+ expect(user).toBeTruthy();
+ expect(createUser).toHaveBeenCalled();
+ });
+
+ it('should allow login when roles claim is a mixed comma-and-space-separated string containing the required role', async () => {
+ // Arrange – IdP returns roles with comma-and-space delimiters
+ jwtDecode.mockReturnValue({
+ roles: 'role1, role2, requiredRole',
+ });
+
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert – login succeeds when required role is present after splitting
+ expect(user).toBeTruthy();
+ expect(createUser).toHaveBeenCalled();
+ });
+
+ it('should reject login when roles claim is a space-separated string that does not contain the required role', async () => {
+ // Arrange – IdP returns a delimited string but required role is absent
+ jwtDecode.mockReturnValue({
+ roles: 'role1 role2 otherRole',
+ });
+
+ // Act
+ const { user, details } = await validate(tokenset);
+
+ // Assert – login is rejected with the correct error message
+ expect(user).toBe(false);
+ expect(details.message).toBe('You must have "requiredRole" role to log in.');
+ });
+
+ it('should allow login when single required role is present (backward compatibility)', async () => {
+ // Arrange – ensure single role configuration (as set in beforeEach)
+ // OPENID_REQUIRED_ROLE = 'requiredRole'
+ // Default jwtDecode mock in beforeEach already returns this role
+ jwtDecode.mockReturnValue({
+ roles: ['requiredRole', 'anotherRole'],
+ });
+
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert – verify that login succeeds with single role configuration
+ expect(user).toBeTruthy();
+ expect(user.email).toBe(tokenset.claims().email);
+ expect(user.username).toBe(tokenset.claims().preferred_username);
+ expect(createUser).toHaveBeenCalled();
+ });
+
+ describe('group overage and groups handling', () => {
+ it.each([
+ ['groups array contains required group', ['group-required', 'other-group'], true, undefined],
+ [
+ 'groups array missing required group',
+ ['other-group'],
+ false,
+ 'You must have "group-required" role to log in.',
+ ],
+ ['groups string equals required group', 'group-required', true, undefined],
+ [
+ 'groups string is other group',
+ 'other-group',
+ false,
+ 'You must have "group-required" role to log in.',
+ ],
+ ])(
+ 'uses groups claim directly when %s (no overage)',
+ async (_label, groupsClaim, expectedAllowed, expectedMessage) => {
+ 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({
+ groups: groupsClaim,
+ permissions: ['admin'],
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user, details } = await validate(tokenset);
+
+ expect(undici.fetch).not.toHaveBeenCalled();
+ expect(Boolean(user)).toBe(expectedAllowed);
+ expect(details?.message).toBe(expectedMessage);
+ },
+ );
+
+ it.each([
+ ['token kind is not id', { kind: 'access', path: 'groups', decoded: { hasgroups: true } }],
+ ['parameter path is not groups', { kind: 'id', path: 'roles', decoded: { hasgroups: true } }],
+ ['decoded token is falsy', { kind: 'id', path: 'groups', decoded: null }],
+ [
+ 'no overage indicators in decoded token',
+ {
+ kind: 'id',
+ path: 'groups',
+ decoded: {
+ permissions: ['admin'],
+ },
+ },
+ ],
+ [
+ 'only _claim_names present (no _claim_sources)',
+ {
+ kind: 'id',
+ path: 'groups',
+ decoded: {
+ _claim_names: { groups: 'src1' },
+ permissions: ['admin'],
+ },
+ },
+ ],
+ [
+ 'only _claim_sources present (no _claim_names)',
+ {
+ kind: 'id',
+ path: 'groups',
+ decoded: {
+ _claim_sources: { src1: { endpoint: 'https://graph.windows.net/ignored' } },
+ permissions: ['admin'],
+ },
+ },
+ ],
+ ])('does not attempt overage resolution when %s', async (_label, cfg) => {
+ process.env.OPENID_REQUIRED_ROLE = 'group-required';
+ process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = cfg.path;
+ process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = cfg.kind;
+
+ jwtDecode.mockReturnValue(cfg.decoded);
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user, details } = await validate(tokenset);
+
+ expect(undici.fetch).not.toHaveBeenCalled();
+ expect(user).toBe(false);
+ expect(details.message).toBe('You must have "group-required" role to log in.');
+ const { logger } = require('@librechat/data-schemas');
+ const expectedTokenKind = cfg.kind === 'access' ? 'access token' : 'id token';
+ expect(logger.error).toHaveBeenCalledWith(
+ expect.stringContaining(`Key '${cfg.path}' not found in ${expectedTokenKind}!`),
+ );
+ });
+ });
+
+ describe('resolving groups via Microsoft Graph', () => {
+ it('denies login and does not call Graph when access token is missing', async () => {
+ process.env.OPENID_REQUIRED_ROLE = 'group-required';
+ process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'groups';
+ process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id';
+
+ const { logger } = require('@librechat/data-schemas');
+
+ jwtDecode.mockReturnValue({
+ hasgroups: true,
+ permissions: ['admin'],
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const tokensetWithoutAccess = {
+ ...tokenset,
+ access_token: undefined,
+ };
+
+ const { user, details } = await validate(tokensetWithoutAccess);
+
+ expect(user).toBe(false);
+ expect(details.message).toBe('You must have "group-required" role to log in.');
+
+ expect(undici.fetch).not.toHaveBeenCalled();
+ expect(logger.error).toHaveBeenCalledWith(
+ expect.stringContaining('Access token missing; cannot resolve group overage'),
+ );
+ });
+
+ it.each([
+ [
+ 'Graph returns HTTP error',
+ async () => ({
+ ok: false,
+ status: 403,
+ statusText: 'Forbidden',
+ json: async () => ({}),
+ }),
+ [
+ '[openidStrategy] Failed to resolve groups via Microsoft Graph getMemberObjects: HTTP 403 Forbidden',
+ ],
+ ],
+ [
+ 'Graph network error',
+ async () => {
+ throw new Error('network error');
+ },
+ [
+ '[openidStrategy] Error resolving groups via Microsoft Graph getMemberObjects:',
+ expect.any(Error),
+ ],
+ ],
+ [
+ 'Graph returns unexpected shape (no value)',
+ async () => ({
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ json: async () => ({}),
+ }),
+ [
+ '[openidStrategy] Unexpected response format when resolving groups via Microsoft Graph getMemberObjects',
+ ],
+ ],
+ [
+ 'Graph returns invalid value type',
+ async () => ({
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ json: async () => ({ value: 'not-an-array' }),
+ }),
+ [
+ '[openidStrategy] Unexpected response format when resolving groups via Microsoft Graph getMemberObjects',
+ ],
+ ],
+ ])(
+ 'denies login when overage resolution fails because %s',
+ async (_label, setupFetch, expectedErrorArgs) => {
+ process.env.OPENID_REQUIRED_ROLE = 'group-required';
+ process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'groups';
+ process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id';
+
+ const { logger } = require('@librechat/data-schemas');
+
+ jwtDecode.mockReturnValue({
+ hasgroups: true,
+ permissions: ['admin'],
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ undici.fetch.mockImplementation(setupFetch);
+
+ const { user, details } = await validate(tokenset);
+
+ expect(undici.fetch).toHaveBeenCalled();
+ expect(user).toBe(false);
+ expect(details.message).toBe('You must have "group-required" role to log in.');
+
+ expect(logger.error).toHaveBeenCalledWith(...expectedErrorArgs);
+ },
+ );
+
+ it.each([
+ [
+ 'hasgroups overage and Graph contains required group',
+ {
+ hasgroups: true,
+ },
+ ['group-required', 'some-other-group'],
+ true,
+ ],
+ [
+ '_claim_* overage and Graph contains required group',
+ {
+ _claim_names: { groups: 'src1' },
+ _claim_sources: { src1: { endpoint: 'https://graph.windows.net/ignored' } },
+ },
+ ['group-required', 'some-other-group'],
+ true,
+ ],
+ [
+ 'hasgroups overage and Graph does NOT contain required group',
+ {
+ hasgroups: true,
+ },
+ ['some-other-group'],
+ false,
+ ],
+ [
+ '_claim_* overage and Graph does NOT contain required group',
+ {
+ _claim_names: { groups: 'src1' },
+ _claim_sources: { src1: { endpoint: 'https://graph.windows.net/ignored' } },
+ },
+ ['some-other-group'],
+ false,
+ ],
+ ])(
+ 'resolves groups via Microsoft Graph when %s',
+ async (_label, decodedTokenValue, graphGroups, expectedAllowed) => {
+ process.env.OPENID_REQUIRED_ROLE = 'group-required';
+ process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'groups';
+ process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id';
+
+ const { logger } = require('@librechat/data-schemas');
+
+ jwtDecode.mockReturnValue(decodedTokenValue);
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ undici.fetch.mockResolvedValue({
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ json: async () => ({
+ value: graphGroups,
+ }),
+ });
+
+ const { user } = await validate(tokenset);
+
+ expect(undici.fetch).toHaveBeenCalledWith(
+ 'https://graph.microsoft.com/v1.0/me/getMemberObjects',
+ expect.objectContaining({
+ method: 'POST',
+ headers: expect.objectContaining({
+ Authorization: 'Bearer exchanged_graph_token',
+ }),
+ }),
+ );
+ expect(Boolean(user)).toBe(expectedAllowed);
+
+ expect(logger.debug).toHaveBeenCalledWith(
+ expect.stringContaining(
+ `Successfully resolved ${graphGroups.length} groups via Microsoft Graph getMemberObjects`,
+ ),
+ );
+ },
+ );
+ });
+
+ 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);
+
+ // Assert – verify that download was attempted and the avatar field was set via updateUser
+ expect(fetch).toHaveBeenCalled();
+ // Our mock getStrategyFunctions.saveBuffer returns '/fake/path/to/avatar.png'
+ expect(user.avatar).toBe('/fake/path/to/avatar.png');
+ });
+
+ it('should not attempt to download avatar if picture is not provided', async () => {
+ // Arrange – remove picture
+ const userinfo = { ...tokenset.claims() };
+ delete userinfo.picture;
+
+ // Act
+ await validate({ ...tokenset, claims: () => userinfo });
+
+ // Assert – fetch should not be called and avatar should remain undefined or empty
+ expect(fetch).not.toHaveBeenCalled();
+ // Depending on your implementation, user.avatar may be undefined or an empty string.
+ });
+
+ it('should support comma-separated multiple roles', async () => {
+ // Arrange
+ process.env.OPENID_REQUIRED_ROLE = 'someRole,anotherRole,admin';
+ await setupOpenId(); // Re-initialize the strategy
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+ jwtDecode.mockReturnValue({
+ roles: ['anotherRole', 'aThirdRole'],
+ });
+
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert
+ expect(user).toBeTruthy();
+ expect(user.email).toBe(tokenset.claims().email);
+ });
+
+ it('should reject login when user has none of the required multiple roles', async () => {
+ // Arrange
+ process.env.OPENID_REQUIRED_ROLE = 'someRole,anotherRole,admin';
+ await setupOpenId(); // Re-initialize the strategy
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+ jwtDecode.mockReturnValue({
+ roles: ['aThirdRole', 'aFourthRole'],
+ });
+
+ // Act
+ const { user, details } = await validate(tokenset);
+
+ // Assert
+ expect(user).toBe(false);
+ expect(details.message).toBe(
+ 'You must have one of: "someRole", "anotherRole", "admin" role to log in.',
+ );
+ });
+
+ it('should handle spaces in comma-separated roles', async () => {
+ // Arrange
+ process.env.OPENID_REQUIRED_ROLE = ' someRole , anotherRole , admin ';
+ await setupOpenId(); // Re-initialize the strategy
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+ jwtDecode.mockReturnValue({
+ roles: ['someRole'],
+ });
+
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert
+ expect(user).toBeTruthy();
+ });
+
+ it('should default to usePKCE false when OPENID_USE_PKCE is not defined', async () => {
+ const OpenIDStrategy = require('openid-client/passport').Strategy;
+
+ delete process.env.OPENID_USE_PKCE;
+ await setupOpenId();
+
+ const callOptions = OpenIDStrategy.mock.calls[OpenIDStrategy.mock.calls.length - 1][0];
+ expect(callOptions.usePKCE).toBe(false);
+ expect(callOptions.params?.code_challenge_method).toBeUndefined();
+ });
+
+ it('should attach federatedTokens to user object for token propagation', async () => {
+ // Arrange - setup tokenset with access token, id token, refresh token, and expiration
+ const tokensetWithTokens = {
+ ...tokenset,
+ access_token: 'mock_access_token_abc123',
+ id_token: 'mock_id_token_def456',
+ refresh_token: 'mock_refresh_token_xyz789',
+ expires_at: 1234567890,
+ };
+
+ // Act - validate with the tokenset containing tokens
+ const { user } = await validate(tokensetWithTokens);
+
+ // Assert - verify federatedTokens object is attached with correct values
+ expect(user.federatedTokens).toBeDefined();
+ expect(user.federatedTokens).toEqual({
+ access_token: 'mock_access_token_abc123',
+ id_token: 'mock_id_token_def456',
+ refresh_token: 'mock_refresh_token_xyz789',
+ expires_at: 1234567890,
+ });
+ });
+
+ it('should include id_token in federatedTokens distinct from access_token', async () => {
+ // Arrange - use different values for access_token and id_token
+ const tokensetWithTokens = {
+ ...tokenset,
+ access_token: 'the_access_token',
+ id_token: 'the_id_token',
+ refresh_token: 'the_refresh_token',
+ expires_at: 9999999999,
+ };
+
+ // Act
+ const { user } = await validate(tokensetWithTokens);
+
+ // Assert - id_token and access_token must be different values
+ expect(user.federatedTokens.access_token).toBe('the_access_token');
+ expect(user.federatedTokens.id_token).toBe('the_id_token');
+ expect(user.federatedTokens.id_token).not.toBe(user.federatedTokens.access_token);
+ });
+
+ it('should include tokenset along with federatedTokens', async () => {
+ // Arrange
+ const tokensetWithTokens = {
+ ...tokenset,
+ access_token: 'test_access_token',
+ id_token: 'test_id_token',
+ refresh_token: 'test_refresh_token',
+ expires_at: 9999999999,
+ };
+
+ // Act
+ const { user } = await validate(tokensetWithTokens);
+
+ // Assert - both tokenset and federatedTokens should be present
+ expect(user.tokenset).toBeDefined();
+ expect(user.federatedTokens).toBeDefined();
+ expect(user.tokenset.access_token).toBe('test_access_token');
+ expect(user.tokenset.id_token).toBe('test_id_token');
+ expect(user.federatedTokens.access_token).toBe('test_access_token');
+ expect(user.federatedTokens.id_token).toBe('test_id_token');
+ });
+
+ it('should set role to "ADMIN" if OPENID_ADMIN_ROLE is set and user has that role', async () => {
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert – verify that the user role is set to "ADMIN"
+ expect(user.role).toBe('ADMIN');
+ });
+
+ it('should not set user role if OPENID_ADMIN_ROLE is set but the user does not have that role', async () => {
+ // Arrange – simulate a token without the admin permission
+ jwtDecode.mockReturnValue({
+ roles: ['requiredRole'],
+ permissions: ['not-admin'],
+ });
+
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert – verify that the user role is not defined
+ expect(user.role).toBeUndefined();
+ });
+
+ it('should demote existing admin user when admin role is removed from token', async () => {
+ // Arrange – simulate an existing user who is currently an admin
+ 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;
+ });
+
+ // Token without admin permission
+ jwtDecode.mockReturnValue({
+ roles: ['requiredRole'],
+ permissions: ['not-admin'],
+ });
+
+ const { logger } = require('@librechat/data-schemas');
+
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert – verify that the user was demoted
+ expect(user.role).toBe('USER');
+ expect(updateUser).toHaveBeenCalledWith(
+ existingAdminUser._id,
+ expect.objectContaining({
+ role: 'USER',
+ }),
+ );
+ expect(logger.info).toHaveBeenCalledWith(
+ expect.stringContaining('demoted from admin - role no longer present in token'),
+ );
+ });
+
+ it('should NOT demote admin user when admin role env vars are not configured', async () => {
+ // Arrange – remove admin role env vars
+ delete process.env.OPENID_ADMIN_ROLE;
+ delete process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH;
+ delete process.env.OPENID_ADMIN_ROLE_TOKEN_KIND;
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ // Simulate an existing admin user
+ 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({
+ roles: ['requiredRole'],
+ });
+
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert – verify that the admin user was NOT demoted
+ expect(user.role).toBe('ADMIN');
+ expect(updateUser).toHaveBeenCalledWith(
+ existingAdminUser._id,
+ expect.objectContaining({
+ role: 'ADMIN',
+ }),
+ );
+ });
+
+ describe('lodash get - nested path extraction', () => {
+ it('should extract roles from deeply nested token path', async () => {
+ process.env.OPENID_REQUIRED_ROLE = 'app-user';
+ process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'resource_access.my-client.roles';
+
+ jwtDecode.mockReturnValue({
+ resource_access: {
+ 'my-client': {
+ roles: ['app-user', 'viewer'],
+ },
+ },
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user } = await validate(tokenset);
+
+ expect(user).toBeTruthy();
+ expect(user.email).toBe(tokenset.claims().email);
+ });
+
+ it('should extract roles from three-level nested path', async () => {
+ process.env.OPENID_REQUIRED_ROLE = 'editor';
+ process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'data.access.permissions.roles';
+
+ jwtDecode.mockReturnValue({
+ data: {
+ access: {
+ permissions: {
+ roles: ['editor', 'reader'],
+ },
+ },
+ },
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user } = await validate(tokenset);
+
+ expect(user).toBeTruthy();
+ });
+
+ it('should log error and reject login when required role path does not exist in token', async () => {
+ const { logger } = require('@librechat/data-schemas');
+ process.env.OPENID_REQUIRED_ROLE = 'app-user';
+ process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'resource_access.nonexistent.roles';
+
+ jwtDecode.mockReturnValue({
+ resource_access: {
+ 'my-client': {
+ roles: ['app-user'],
+ },
+ },
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user, details } = await validate(tokenset);
+
+ expect(logger.error).toHaveBeenCalledWith(
+ expect.stringContaining("Key 'resource_access.nonexistent.roles' not found in id token!"),
+ );
+ expect(user).toBe(false);
+ expect(details.message).toContain('role to log in');
+ });
+
+ it('should handle missing intermediate nested path gracefully', async () => {
+ const { logger } = require('@librechat/data-schemas');
+ process.env.OPENID_REQUIRED_ROLE = 'user';
+ process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'org.team.roles';
+
+ jwtDecode.mockReturnValue({
+ org: {
+ other: 'value',
+ },
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user } = await validate(tokenset);
+
+ expect(logger.error).toHaveBeenCalledWith(
+ expect.stringContaining("Key 'org.team.roles' not found in id token!"),
+ );
+ expect(user).toBe(false);
+ });
+
+ it('should extract admin role from nested path in access token', async () => {
+ process.env.OPENID_ADMIN_ROLE = 'admin';
+ process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'realm_access.roles';
+ process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'access';
+
+ jwtDecode.mockImplementation((token) => {
+ if (token === 'fake_access_token') {
+ return {
+ realm_access: {
+ roles: ['admin', 'user'],
+ },
+ };
+ }
+ return {
+ roles: ['requiredRole'],
+ };
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user } = await validate(tokenset);
+
+ expect(user.role).toBe('ADMIN');
+ });
+
+ it('should extract admin role from nested path in userinfo', async () => {
+ process.env.OPENID_ADMIN_ROLE = 'admin';
+ process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'organization.permissions';
+ process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'userinfo';
+
+ const userinfoWithNestedGroups = {
+ ...tokenset.claims(),
+ organization: {
+ permissions: ['admin', 'write'],
+ },
+ };
+
+ require('openid-client').fetchUserInfo.mockResolvedValue({
+ organization: {
+ permissions: ['admin', 'write'],
+ },
+ });
+
+ jwtDecode.mockReturnValue({
+ roles: ['requiredRole'],
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user } = await validate({
+ ...tokenset,
+ claims: () => userinfoWithNestedGroups,
+ });
+
+ expect(user.role).toBe('ADMIN');
+ });
+
+ it('should handle boolean admin role value', async () => {
+ process.env.OPENID_ADMIN_ROLE = 'admin';
+ process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'is_admin';
+
+ jwtDecode.mockReturnValue({
+ roles: ['requiredRole'],
+ is_admin: true,
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user } = await validate(tokenset);
+
+ expect(user.role).toBe('ADMIN');
+ });
+
+ it('should handle string admin role value matching exactly', async () => {
+ process.env.OPENID_ADMIN_ROLE = 'super-admin';
+ process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'role';
+
+ jwtDecode.mockReturnValue({
+ roles: ['requiredRole'],
+ role: 'super-admin',
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user } = await validate(tokenset);
+
+ expect(user.role).toBe('ADMIN');
+ });
+
+ it('should not set admin role when string value does not match', async () => {
+ process.env.OPENID_ADMIN_ROLE = 'super-admin';
+ process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'role';
+
+ jwtDecode.mockReturnValue({
+ roles: ['requiredRole'],
+ role: 'regular-user',
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user } = await validate(tokenset);
+
+ expect(user.role).toBeUndefined();
+ });
+
+ it('should handle array admin role value', async () => {
+ process.env.OPENID_ADMIN_ROLE = 'site-admin';
+ process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'app_roles';
+
+ jwtDecode.mockReturnValue({
+ roles: ['requiredRole'],
+ app_roles: ['user', 'site-admin', 'moderator'],
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user } = await validate(tokenset);
+
+ expect(user.role).toBe('ADMIN');
+ });
+
+ it('should not set admin when role is not in array', async () => {
+ process.env.OPENID_ADMIN_ROLE = 'site-admin';
+ process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'app_roles';
+
+ jwtDecode.mockReturnValue({
+ roles: ['requiredRole'],
+ app_roles: ['user', 'moderator'],
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user } = await validate(tokenset);
+
+ expect(user.role).toBeUndefined();
+ });
+
+ it('should grant admin when admin role claim is a space-separated string containing the admin role', async () => {
+ // Arrange – IdP returns admin roles as a space-delimited string
+ process.env.OPENID_ADMIN_ROLE = 'site-admin';
+ process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'app_roles';
+
+ jwtDecode.mockReturnValue({
+ roles: ['requiredRole'],
+ app_roles: 'user site-admin moderator',
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert – admin role is granted after splitting the delimited string
+ expect(user.role).toBe('ADMIN');
+ });
+
+ it('should not grant admin when admin role claim is a space-separated string that does not contain the admin role', async () => {
+ // Arrange – delimited string present but admin role is absent
+ process.env.OPENID_ADMIN_ROLE = 'site-admin';
+ process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'app_roles';
+
+ jwtDecode.mockReturnValue({
+ roles: ['requiredRole'],
+ app_roles: 'user moderator',
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ // Act
+ const { user } = await validate(tokenset);
+
+ // Assert – admin role is not granted
+ expect(user.role).toBeUndefined();
+ });
+
+ it('should handle nested path with special characters in keys', async () => {
+ process.env.OPENID_REQUIRED_ROLE = 'app-user';
+ process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'resource_access.my-app-123.roles';
+
+ jwtDecode.mockReturnValue({
+ resource_access: {
+ 'my-app-123': {
+ roles: ['app-user'],
+ },
+ },
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user } = await validate(tokenset);
+
+ expect(user).toBeTruthy();
+ });
+
+ it('should handle empty object at nested path', async () => {
+ const { logger } = require('@librechat/data-schemas');
+ process.env.OPENID_REQUIRED_ROLE = 'user';
+ process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'access.roles';
+
+ jwtDecode.mockReturnValue({
+ access: {},
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user } = await validate(tokenset);
+
+ expect(logger.error).toHaveBeenCalledWith(
+ expect.stringContaining("Key 'access.roles' not found in id token!"),
+ );
+ expect(user).toBe(false);
+ });
+
+ it('should handle null value at intermediate path', async () => {
+ const { logger } = require('@librechat/data-schemas');
+ process.env.OPENID_REQUIRED_ROLE = 'user';
+ process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'data.roles';
+
+ jwtDecode.mockReturnValue({
+ data: null,
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user } = await validate(tokenset);
+
+ expect(logger.error).toHaveBeenCalledWith(
+ expect.stringContaining("Key 'data.roles' not found in id token!"),
+ );
+ expect(user).toBe(false);
+ });
+
+ it('should reject login with invalid admin role token kind', async () => {
+ process.env.OPENID_ADMIN_ROLE = 'admin';
+ process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH = 'roles';
+ process.env.OPENID_ADMIN_ROLE_TOKEN_KIND = 'invalid';
+
+ const { logger } = require('@librechat/data-schemas');
+
+ jwtDecode.mockReturnValue({
+ roles: ['requiredRole', 'admin'],
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ await expect(validate(tokenset)).rejects.toThrow('Invalid admin role token kind');
+
+ expect(logger.error).toHaveBeenCalledWith(
+ expect.stringContaining(
+ "Invalid admin role token kind: invalid. Must be one of 'access', 'id', or 'userinfo'",
+ ),
+ );
+ });
+
+ it('should reject login when roles path returns invalid type (object)', async () => {
+ const { logger } = require('@librechat/data-schemas');
+ process.env.OPENID_REQUIRED_ROLE = 'app-user';
+ process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'roles';
+
+ jwtDecode.mockReturnValue({
+ roles: { admin: true, user: false },
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user, details } = await validate(tokenset);
+
+ expect(logger.error).toHaveBeenCalledWith(
+ expect.stringContaining("Key 'roles' not found in id token!"),
+ );
+ expect(user).toBe(false);
+ expect(details.message).toContain('role to log in');
+ });
+
+ it('should reject login when roles path returns invalid type (number)', async () => {
+ const { logger } = require('@librechat/data-schemas');
+ process.env.OPENID_REQUIRED_ROLE = 'user';
+ process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'roleCount';
+
+ jwtDecode.mockReturnValue({
+ roleCount: 5,
+ });
+
+ await setupOpenId();
+ verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid');
+
+ const { user } = await validate(tokenset);
+
+ expect(logger.error).toHaveBeenCalledWith(
+ expect.stringContaining("Key 'roleCount' not found in id token!"),
+ );
+ expect(user).toBe(false);
+ });
+ });
+
+ describe('OPENID_EMAIL_CLAIM', () => {
+ it('should use the default email when OPENID_EMAIL_CLAIM is not set', async () => {
+ const { user } = await validate(tokenset);
+ expect(user.email).toBe('test@example.com');
+ });
+
+ it('should use the configured claim when OPENID_EMAIL_CLAIM is set', async () => {
+ process.env.OPENID_EMAIL_CLAIM = 'upn';
+ const userinfo = { ...tokenset.claims(), upn: 'user@corp.example.com' };
+
+ const { user } = await validate({ ...tokenset, claims: () => userinfo });
+
+ expect(user.email).toBe('user@corp.example.com');
+ expect(createUser).toHaveBeenCalledWith(
+ expect.objectContaining({ email: 'user@corp.example.com' }),
+ expect.anything(),
+ true,
+ true,
+ );
+ });
+
+ it('should fall back to preferred_username when email is missing and OPENID_EMAIL_CLAIM is not set', async () => {
+ const userinfo = { ...tokenset.claims() };
+ delete userinfo.email;
+
+ const { user } = await validate({ ...tokenset, claims: () => userinfo });
+
+ expect(user.email).toBe('testusername');
+ });
+
+ it('should fall back to upn when email and preferred_username are missing and OPENID_EMAIL_CLAIM is not set', async () => {
+ const userinfo = { ...tokenset.claims(), upn: 'user@corp.example.com' };
+ delete userinfo.email;
+ delete userinfo.preferred_username;
+
+ const { user } = await validate({ ...tokenset, claims: () => userinfo });
+
+ expect(user.email).toBe('user@corp.example.com');
+ });
+
+ it('should ignore empty string OPENID_EMAIL_CLAIM and use default fallback', async () => {
+ process.env.OPENID_EMAIL_CLAIM = '';
+
+ const { user } = await validate(tokenset);
+
+ expect(user.email).toBe('test@example.com');
+ });
+
+ it('should trim whitespace from OPENID_EMAIL_CLAIM and resolve correctly', async () => {
+ process.env.OPENID_EMAIL_CLAIM = ' upn ';
+ const userinfo = { ...tokenset.claims(), upn: 'user@corp.example.com' };
+
+ const { user } = await validate({ ...tokenset, claims: () => userinfo });
+
+ expect(user.email).toBe('user@corp.example.com');
+ });
+
+ it('should ignore whitespace-only OPENID_EMAIL_CLAIM and use default fallback', async () => {
+ process.env.OPENID_EMAIL_CLAIM = ' ';
+
+ const { user } = await validate(tokenset);
+
+ expect(user.email).toBe('test@example.com');
+ });
+
+ it('should fall back to default chain with warning when configured claim is missing from userinfo', async () => {
+ const { logger } = require('@librechat/data-schemas');
+ process.env.OPENID_EMAIL_CLAIM = 'nonexistent_claim';
+
+ const { user } = await validate(tokenset);
+
+ expect(user.email).toBe('test@example.com');
+ expect(logger.warn).toHaveBeenCalledWith(
+ expect.stringContaining('OPENID_EMAIL_CLAIM="nonexistent_claim" not present in userinfo'),
+ );
+ });
+ });
+
+ describe('Tenant-scoped config', () => {
+ it('should call resolveAppConfigForUser for tenant user', async () => {
+ const existingUser = {
+ _id: 'openid-tenant-user',
+ provider: 'openid',
+ openidId: '1234',
+ email: 'test@example.com',
+ tenantId: 'tenant-d',
+ role: 'USER',
+ };
+ findUser.mockResolvedValue(existingUser);
+
+ await validate(tokenset);
+
+ expect(resolveAppConfigForUser).toHaveBeenCalledWith(getAppConfig, existingUser);
+ });
+
+ it('should use baseConfig for new user without calling resolveAppConfigForUser', async () => {
+ findUser.mockResolvedValue(null);
+
+ await validate(tokenset);
+
+ expect(resolveAppConfigForUser).not.toHaveBeenCalled();
+ expect(getAppConfig).toHaveBeenCalledWith({ baseOnly: true });
+ });
+
+ it('should block login when tenant config restricts the domain', async () => {
+ const { isEmailDomainAllowed } = require('@librechat/api');
+ const existingUser = {
+ _id: 'openid-tenant-blocked',
+ provider: 'openid',
+ openidId: '1234',
+ email: 'test@example.com',
+ tenantId: 'tenant-restrict',
+ role: 'USER',
+ };
+ findUser.mockResolvedValue(existingUser);
+ resolveAppConfigForUser.mockResolvedValue({
+ registration: { allowedDomains: ['other.com'] },
+ });
+ isEmailDomainAllowed.mockReturnValueOnce(true).mockReturnValueOnce(false);
+
+ const { user, details } = await validate(tokenset);
+ expect(user).toBe(false);
+ expect(details).toEqual({ message: 'Email domain not allowed' });
+ });
+ });
+});
diff --git a/api/strategies/samlStrategy.js b/api/strategies/samlStrategy.js
index 843baf8a64..4f4bfac158 100644
--- a/api/strategies/samlStrategy.js
+++ b/api/strategies/samlStrategy.js
@@ -5,7 +5,11 @@ const passport = require('passport');
const { ErrorTypes } = require('librechat-data-provider');
const { hashToken, logger } = require('@librechat/data-schemas');
const { Strategy: SamlStrategy } = require('@node-saml/passport-saml');
-const { getBalanceConfig, isEmailDomainAllowed } = require('@librechat/api');
+const {
+ getBalanceConfig,
+ isEmailDomainAllowed,
+ resolveAppConfigForUser,
+} = require('@librechat/api');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { findUser, createUser, updateUser } = require('~/models');
const { getAppConfig } = require('~/server/services/Config');
@@ -174,126 +178,179 @@ function convertToUsername(input, defaultValue = '') {
return defaultValue;
}
+/**
+ * Creates a SAML authentication callback.
+ * @param {boolean} [existingUsersOnly=false] - If true, only existing users will be authenticated.
+ * @returns {Function} The SAML callback function for passport.
+ */
+function createSamlCallback(existingUsersOnly = false) {
+ return async (profile, done) => {
+ try {
+ logger.info(`[samlStrategy] SAML authentication received for NameID: ${profile.nameID}`);
+ logger.debug('[samlStrategy] SAML profile:', profile);
+
+ const userEmail = getEmail(profile) || '';
+
+ const baseConfig = await getAppConfig({ baseOnly: true });
+ if (!isEmailDomainAllowed(userEmail, baseConfig?.registration?.allowedDomains)) {
+ logger.error(
+ `[SAML Strategy] Authentication blocked - email domain not allowed [Email: ${userEmail}]`,
+ );
+ return done(null, false, { message: 'Email domain not allowed' });
+ }
+
+ let user = await findUser({ samlId: profile.nameID });
+ logger.info(
+ `[samlStrategy] User ${user ? 'found' : 'not found'} with SAML ID: ${profile.nameID}`,
+ );
+
+ if (!user) {
+ user = await findUser({ email: userEmail });
+ logger.info(`[samlStrategy] User ${user ? 'found' : 'not found'} with email: ${userEmail}`);
+ }
+
+ if (user && user.provider !== 'saml') {
+ logger.info(
+ `[samlStrategy] User ${user.email} already exists with provider ${user.provider}`,
+ );
+ return done(null, false, {
+ message: ErrorTypes.AUTH_FAILED,
+ });
+ }
+
+ const appConfig = user?.tenantId
+ ? await resolveAppConfigForUser(getAppConfig, user)
+ : baseConfig;
+
+ if (!isEmailDomainAllowed(userEmail, appConfig?.registration?.allowedDomains)) {
+ logger.error(
+ `[SAML Strategy] Authentication blocked - email domain not allowed [Email: ${userEmail}]`,
+ );
+ return done(null, false, { message: 'Email domain not allowed' });
+ }
+
+ const fullName = getFullName(profile);
+
+ const username = convertToUsername(
+ getUserName(profile) || getGivenName(profile) || getEmail(profile),
+ );
+
+ if (!user) {
+ if (existingUsersOnly) {
+ logger.error(
+ `[samlStrategy] Admin auth blocked - user does not exist [Email: ${userEmail}]`,
+ );
+ return done(null, false, { message: 'User does not exist' });
+ }
+
+ user = {
+ provider: 'saml',
+ samlId: profile.nameID,
+ username,
+ email: userEmail,
+ emailVerified: true,
+ name: fullName,
+ };
+ const balanceConfig = getBalanceConfig(appConfig);
+ user = await createUser(user, balanceConfig, true, true);
+ } else {
+ user.provider = 'saml';
+ user.samlId = profile.nameID;
+ user.username = username;
+ user.name = fullName;
+ }
+
+ const picture = getPicture(profile);
+ if (picture && !user.avatar?.includes('manual=true')) {
+ const imageBuffer = await downloadImage(profile.picture);
+ if (imageBuffer) {
+ let fileName;
+ if (crypto) {
+ fileName = (await hashToken(profile.nameID)) + '.png';
+ } else {
+ fileName = profile.nameID + '.png';
+ }
+
+ const { saveBuffer } = getStrategyFunctions(
+ appConfig?.fileStrategy ?? process.env.CDN_PROVIDER,
+ );
+ const imagePath = await saveBuffer({
+ fileName,
+ userId: user._id.toString(),
+ buffer: imageBuffer,
+ });
+ user.avatar = imagePath ?? '';
+ }
+ }
+
+ user = await updateUser(user._id, user);
+
+ logger.info(
+ `[samlStrategy] Login success SAML ID: ${user.samlId} | email: ${user.email} | username: ${user.username}`,
+ {
+ user: {
+ samlId: user.samlId,
+ username: user.username,
+ email: user.email,
+ name: user.name,
+ },
+ },
+ );
+
+ done(null, user);
+ } catch (err) {
+ logger.error('[samlStrategy] Login failed', err);
+ done(err);
+ }
+ };
+}
+
+/**
+ * Returns the base SAML configuration shared by both regular and admin strategies.
+ * @returns {object} The SAML configuration object.
+ */
+function getBaseSamlConfig() {
+ return {
+ entryPoint: process.env.SAML_ENTRY_POINT,
+ issuer: process.env.SAML_ISSUER,
+ idpCert: getCertificateContent(process.env.SAML_CERT),
+ wantAssertionsSigned: process.env.SAML_USE_AUTHN_RESPONSE_SIGNED === 'true' ? false : true,
+ wantAuthnResponseSigned: process.env.SAML_USE_AUTHN_RESPONSE_SIGNED === 'true' ? true : false,
+ };
+}
+
async function setupSaml() {
try {
+ const baseConfig = getBaseSamlConfig();
const samlConfig = {
- entryPoint: process.env.SAML_ENTRY_POINT,
- issuer: process.env.SAML_ISSUER,
+ ...baseConfig,
callbackUrl: process.env.SAML_CALLBACK_URL,
- idpCert: getCertificateContent(process.env.SAML_CERT),
- wantAssertionsSigned: process.env.SAML_USE_AUTHN_RESPONSE_SIGNED === 'true' ? false : true,
- wantAuthnResponseSigned: process.env.SAML_USE_AUTHN_RESPONSE_SIGNED === 'true' ? true : false,
};
- passport.use(
- 'saml',
- new SamlStrategy(samlConfig, async (profile, done) => {
- try {
- logger.info(`[samlStrategy] SAML authentication received for NameID: ${profile.nameID}`);
- logger.debug('[samlStrategy] SAML profile:', profile);
-
- const userEmail = getEmail(profile) || '';
- const appConfig = await getAppConfig();
-
- if (!isEmailDomainAllowed(userEmail, appConfig?.registration?.allowedDomains)) {
- logger.error(
- `[SAML Strategy] Authentication blocked - email domain not allowed [Email: ${userEmail}]`,
- );
- return done(null, false, { message: 'Email domain not allowed' });
- }
-
- let user = await findUser({ samlId: profile.nameID });
- logger.info(
- `[samlStrategy] User ${user ? 'found' : 'not found'} with SAML ID: ${profile.nameID}`,
- );
-
- if (!user) {
- user = await findUser({ email: userEmail });
- logger.info(
- `[samlStrategy] User ${user ? 'found' : 'not found'} with email: ${userEmail}`,
- );
- }
-
- if (user && user.provider !== 'saml') {
- logger.info(
- `[samlStrategy] User ${user.email} already exists with provider ${user.provider}`,
- );
- return done(null, false, {
- message: ErrorTypes.AUTH_FAILED,
- });
- }
-
- const fullName = getFullName(profile);
-
- const username = convertToUsername(
- getUserName(profile) || getGivenName(profile) || getEmail(profile),
- );
-
- if (!user) {
- user = {
- provider: 'saml',
- samlId: profile.nameID,
- username,
- email: userEmail,
- emailVerified: true,
- name: fullName,
- };
- const balanceConfig = getBalanceConfig(appConfig);
- user = await createUser(user, balanceConfig, true, true);
- } else {
- user.provider = 'saml';
- user.samlId = profile.nameID;
- user.username = username;
- user.name = fullName;
- }
-
- const picture = getPicture(profile);
- if (picture && !user.avatar?.includes('manual=true')) {
- const imageBuffer = await downloadImage(profile.picture);
- if (imageBuffer) {
- let fileName;
- if (crypto) {
- fileName = (await hashToken(profile.nameID)) + '.png';
- } else {
- fileName = profile.nameID + '.png';
- }
-
- const { saveBuffer } = getStrategyFunctions(
- appConfig?.fileStrategy ?? process.env.CDN_PROVIDER,
- );
- const imagePath = await saveBuffer({
- fileName,
- userId: user._id.toString(),
- buffer: imageBuffer,
- });
- user.avatar = imagePath ?? '';
- }
- }
-
- user = await updateUser(user._id, user);
-
- logger.info(
- `[samlStrategy] Login success SAML ID: ${user.samlId} | email: ${user.email} | username: ${user.username}`,
- {
- user: {
- samlId: user.samlId,
- username: user.username,
- email: user.email,
- name: user.name,
- },
- },
- );
-
- done(null, user);
- } catch (err) {
- logger.error('[samlStrategy] Login failed', err);
- done(err);
- }
- }),
- );
+ passport.use('saml', new SamlStrategy(samlConfig, createSamlCallback(false)));
+ setupSamlAdmin(baseConfig);
} catch (err) {
logger.error('[samlStrategy]', err);
}
}
+/**
+ * Sets up the SAML strategy specifically for admin authentication.
+ * Rejects users that don't already exist.
+ * @param {object} [baseConfig] - Pre-parsed base SAML config to avoid redundant cert parsing.
+ */
+function setupSamlAdmin(baseConfig) {
+ try {
+ const samlAdminConfig = {
+ ...(baseConfig ?? getBaseSamlConfig()),
+ callbackUrl: `${process.env.DOMAIN_SERVER}/api/admin/oauth/saml/callback`,
+ };
+
+ passport.use('samlAdmin', new SamlStrategy(samlAdminConfig, createSamlCallback(true)));
+ logger.info('[samlStrategy] Admin SAML strategy registered.');
+ } catch (err) {
+ logger.error('[samlStrategy] setupSamlAdmin', err);
+ }
+}
+
module.exports = { setupSaml, getCertificateContent };
diff --git a/api/strategies/samlStrategy.spec.js b/api/strategies/samlStrategy.spec.js
index 1d16719b87..965fb157ef 100644
--- a/api/strategies/samlStrategy.spec.js
+++ b/api/strategies/samlStrategy.spec.js
@@ -30,6 +30,7 @@ jest.mock('@librechat/api', () => ({
tokenCredits: 1000,
startBalance: 1000,
})),
+ resolveAppConfigForUser: jest.fn(async (_getAppConfig, _user) => ({})),
}));
jest.mock('~/server/services/Config/EndpointService', () => ({
config: {},
@@ -47,6 +48,9 @@ const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const { Strategy: SamlStrategy } = require('@node-saml/passport-saml');
+const { findUser } = require('~/models');
+const { resolveAppConfigForUser } = require('@librechat/api');
+const { getAppConfig } = require('~/server/services/Config');
const { setupSaml, getCertificateContent } = require('./samlStrategy');
// Configure fs mock
@@ -54,10 +58,14 @@ jest.mocked(fs).existsSync = jest.fn();
jest.mocked(fs).statSync = jest.fn();
jest.mocked(fs).readFileSync = jest.fn();
-// To capture the verify callback from the strategy, we grab it from the mock constructor
+// To capture the verify callback from the strategy, we grab it from the mock constructor.
+// setupSaml() registers both 'saml' (regular) and 'samlAdmin' strategies, so we capture
+// only the first callback per setupSaml() call (the regular one).
let verifyCallback;
SamlStrategy.mockImplementation((options, verify) => {
- verifyCallback = verify;
+ if (!verifyCallback) {
+ verifyCallback = verify;
+ }
return { name: 'saml', options, verify };
});
@@ -215,6 +223,8 @@ describe('setupSaml', () => {
beforeEach(async () => {
jest.clearAllMocks();
+ // Reset so the mock captures the regular (non-admin) callback on next setupSaml() call
+ verifyCallback = null;
// Configure mocks
const { findUser, createUser, updateUser } = require('~/models');
@@ -440,4 +450,50 @@ u7wlOSk+oFzDIO/UILIA
expect(fetch).not.toHaveBeenCalled();
});
+
+ it('should pass the found user to resolveAppConfigForUser', async () => {
+ const existingUser = {
+ _id: 'tenant-user-id',
+ provider: 'saml',
+ samlId: 'saml-1234',
+ email: 'test@example.com',
+ tenantId: 'tenant-c',
+ role: 'USER',
+ };
+ findUser.mockResolvedValue(existingUser);
+
+ const profile = { ...baseProfile };
+ await validate(profile);
+
+ expect(resolveAppConfigForUser).toHaveBeenCalledWith(getAppConfig, existingUser);
+ });
+
+ it('should use baseConfig for new SAML user without calling resolveAppConfigForUser', async () => {
+ const profile = { ...baseProfile };
+ await validate(profile);
+
+ expect(resolveAppConfigForUser).not.toHaveBeenCalled();
+ expect(getAppConfig).toHaveBeenCalledWith({ baseOnly: true });
+ });
+
+ it('should block login when tenant config restricts the domain', async () => {
+ const { isEmailDomainAllowed } = require('@librechat/api');
+ const existingUser = {
+ _id: 'tenant-blocked',
+ provider: 'saml',
+ samlId: 'saml-1234',
+ email: 'test@example.com',
+ tenantId: 'tenant-restrict',
+ role: 'USER',
+ };
+ findUser.mockResolvedValue(existingUser);
+ resolveAppConfigForUser.mockResolvedValue({
+ registration: { allowedDomains: ['other.com'] },
+ });
+ isEmailDomainAllowed.mockReturnValueOnce(true).mockReturnValueOnce(false);
+
+ const profile = { ...baseProfile };
+ const { user } = await validate(profile);
+ expect(user).toBe(false);
+ });
});
diff --git a/api/strategies/socialLogin.js b/api/strategies/socialLogin.js
index 88fb347042..580e4f3d7e 100644
--- a/api/strategies/socialLogin.js
+++ b/api/strategies/socialLogin.js
@@ -1,21 +1,21 @@
const { logger } = require('@librechat/data-schemas');
const { ErrorTypes } = require('librechat-data-provider');
-const { isEnabled, isEmailDomainAllowed } = require('@librechat/api');
+const { isEnabled, isEmailDomainAllowed, resolveAppConfigForUser } = require('@librechat/api');
const { createSocialUser, handleExistingUser } = require('./process');
const { getAppConfig } = require('~/server/services/Config');
const { findUser } = require('~/models');
const socialLogin =
- (provider, getProfileDetails) => async (accessToken, refreshToken, idToken, profile, cb) => {
+ (provider, getProfileDetails, options = {}) =>
+ async (accessToken, refreshToken, idToken, profile, cb) => {
try {
const { email, id, avatarUrl, username, name, emailVerified } = getProfileDetails({
idToken,
profile,
});
- const appConfig = await getAppConfig();
-
- if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
+ const baseConfig = await getAppConfig({ baseOnly: true });
+ if (!isEmailDomainAllowed(email, baseConfig?.registration?.allowedDomains)) {
logger.error(
`[${provider}Login] Authentication blocked - email domain not allowed [Email: ${email}]`,
);
@@ -41,6 +41,20 @@ const socialLogin =
}
}
+ const appConfig = existingUser?.tenantId
+ ? await resolveAppConfigForUser(getAppConfig, existingUser)
+ : baseConfig;
+
+ if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
+ logger.error(
+ `[${provider}Login] Authentication blocked - email domain not allowed [Email: ${email}]`,
+ );
+ const error = new Error(ErrorTypes.AUTH_FAILED);
+ error.code = ErrorTypes.AUTH_FAILED;
+ error.message = 'Email domain not allowed';
+ return cb(error);
+ }
+
if (existingUser?.provider === provider) {
await handleExistingUser(existingUser, avatarUrl, appConfig, email);
return cb(null, existingUser);
@@ -54,6 +68,13 @@ const socialLogin =
return cb(error);
}
+ if (options.existingUsersOnly) {
+ logger.error(
+ `[${provider}Login] Admin auth blocked - user does not exist [Email: ${email}]`,
+ );
+ return cb(null, false, { message: 'User does not exist' });
+ }
+
const ALLOW_SOCIAL_REGISTRATION = isEnabled(process.env.ALLOW_SOCIAL_REGISTRATION);
if (!ALLOW_SOCIAL_REGISTRATION) {
logger.error(
diff --git a/api/strategies/socialLogin.test.js b/api/strategies/socialLogin.test.js
index ba4778c8b1..4fde397d55 100644
--- a/api/strategies/socialLogin.test.js
+++ b/api/strategies/socialLogin.test.js
@@ -3,6 +3,8 @@ const { ErrorTypes } = require('librechat-data-provider');
const { createSocialUser, handleExistingUser } = require('./process');
const socialLogin = require('./socialLogin');
const { findUser } = require('~/models');
+const { resolveAppConfigForUser } = require('@librechat/api');
+const { getAppConfig } = require('~/server/services/Config');
jest.mock('@librechat/data-schemas', () => {
const actualModule = jest.requireActual('@librechat/data-schemas');
@@ -25,6 +27,10 @@ jest.mock('@librechat/api', () => ({
...jest.requireActual('@librechat/api'),
isEnabled: jest.fn().mockReturnValue(true),
isEmailDomainAllowed: jest.fn().mockReturnValue(true),
+ resolveAppConfigForUser: jest.fn().mockResolvedValue({
+ fileStrategy: 'local',
+ balance: { enabled: false },
+ }),
}));
jest.mock('~/models', () => ({
@@ -66,10 +72,7 @@ describe('socialLogin', () => {
googleId: googleId,
};
- /** Mock findUser to return user on first call (by googleId), null on second call */
- findUser
- .mockResolvedValueOnce(existingUser) // First call: finds by googleId
- .mockResolvedValueOnce(null); // Second call would be by email, but won't be reached
+ findUser.mockResolvedValueOnce(existingUser).mockResolvedValueOnce(null);
const mockProfile = {
id: googleId,
@@ -83,13 +86,9 @@ describe('socialLogin', () => {
await loginFn(null, null, null, mockProfile, callback);
- /** Verify it searched by googleId first */
expect(findUser).toHaveBeenNthCalledWith(1, { googleId: googleId });
-
- /** Verify it did NOT search by email (because it found user by googleId) */
expect(findUser).toHaveBeenCalledTimes(1);
- /** Verify handleExistingUser was called with the new email */
expect(handleExistingUser).toHaveBeenCalledWith(
existingUser,
'https://example.com/avatar.png',
@@ -97,7 +96,6 @@ describe('socialLogin', () => {
newEmail,
);
- /** Verify callback was called with success */
expect(callback).toHaveBeenCalledWith(null, existingUser);
});
@@ -113,7 +111,7 @@ describe('socialLogin', () => {
facebookId: facebookId,
};
- findUser.mockResolvedValue(existingUser); // Always returns user
+ findUser.mockResolvedValue(existingUser);
const mockProfile = {
id: facebookId,
@@ -127,7 +125,6 @@ describe('socialLogin', () => {
await loginFn(null, null, null, mockProfile, callback);
- /** Verify it searched by facebookId first */
expect(findUser).toHaveBeenCalledWith({ facebookId: facebookId });
expect(findUser.mock.calls[0]).toEqual([{ facebookId: facebookId }]);
@@ -150,13 +147,10 @@ describe('socialLogin', () => {
_id: 'user789',
email: email,
provider: 'google',
- googleId: 'old-google-id', // Different googleId (edge case)
+ googleId: 'old-google-id',
};
- /** First call (by googleId) returns null, second call (by email) returns user */
- findUser
- .mockResolvedValueOnce(null) // By googleId
- .mockResolvedValueOnce(existingUser); // By email
+ findUser.mockResolvedValueOnce(null).mockResolvedValueOnce(existingUser);
const mockProfile = {
id: googleId,
@@ -170,13 +164,10 @@ describe('socialLogin', () => {
await loginFn(null, null, null, mockProfile, callback);
- /** Verify both searches happened */
expect(findUser).toHaveBeenNthCalledWith(1, { googleId: googleId });
- /** Email passed as-is; findUser implementation handles case normalization */
expect(findUser).toHaveBeenNthCalledWith(2, { email: email });
expect(findUser).toHaveBeenCalledTimes(2);
- /** Verify warning log */
expect(logger.warn).toHaveBeenCalledWith(
`[${provider}Login] User found by email: ${email} but not by ${provider}Id`,
);
@@ -197,7 +188,6 @@ describe('socialLogin', () => {
googleId: googleId,
};
- /** Both searches return null */
findUser.mockResolvedValue(null);
createSocialUser.mockResolvedValue(newUser);
@@ -213,10 +203,8 @@ describe('socialLogin', () => {
await loginFn(null, null, null, mockProfile, callback);
- /** Verify both searches happened */
expect(findUser).toHaveBeenCalledTimes(2);
- /** Verify createSocialUser was called */
expect(createSocialUser).toHaveBeenCalledWith({
email: email,
avatarUrl: 'https://example.com/avatar.png',
@@ -242,12 +230,10 @@ describe('socialLogin', () => {
const existingUser = {
_id: 'user123',
email: email,
- provider: 'local', // Different provider
+ provider: 'local',
};
- findUser
- .mockResolvedValueOnce(null) // By googleId
- .mockResolvedValueOnce(existingUser); // By email
+ findUser.mockResolvedValueOnce(null).mockResolvedValueOnce(existingUser);
const mockProfile = {
id: googleId,
@@ -261,7 +247,6 @@ describe('socialLogin', () => {
await loginFn(null, null, null, mockProfile, callback);
- /** Verify error callback */
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
code: ErrorTypes.AUTH_FAILED,
@@ -274,4 +259,104 @@ describe('socialLogin', () => {
);
});
});
+
+ describe('Tenant-scoped config', () => {
+ it('should call resolveAppConfigForUser for tenant user', async () => {
+ const provider = 'google';
+ const googleId = 'google-tenant-user';
+ const email = 'tenant@example.com';
+
+ const existingUser = {
+ _id: 'userTenant',
+ email,
+ provider: 'google',
+ googleId,
+ tenantId: 'tenant-b',
+ role: 'USER',
+ };
+
+ findUser.mockResolvedValue(existingUser);
+
+ const mockProfile = {
+ id: googleId,
+ emails: [{ value: email, verified: true }],
+ photos: [{ value: 'https://example.com/avatar.png' }],
+ name: { givenName: 'Tenant', familyName: 'User' },
+ };
+
+ const loginFn = socialLogin(provider, mockGetProfileDetails);
+ const callback = jest.fn();
+
+ await loginFn(null, null, null, mockProfile, callback);
+
+ expect(resolveAppConfigForUser).toHaveBeenCalledWith(getAppConfig, existingUser);
+ });
+
+ it('should use baseConfig for non-tenant user without calling resolveAppConfigForUser', async () => {
+ const provider = 'google';
+ const googleId = 'google-new-tenant';
+ const email = 'new@example.com';
+
+ findUser.mockResolvedValue(null);
+ createSocialUser.mockResolvedValue({
+ _id: 'newUser',
+ email,
+ provider: 'google',
+ googleId,
+ });
+
+ const mockProfile = {
+ id: googleId,
+ emails: [{ value: email, verified: true }],
+ photos: [{ value: 'https://example.com/avatar.png' }],
+ name: { givenName: 'New', familyName: 'User' },
+ };
+
+ const loginFn = socialLogin(provider, mockGetProfileDetails);
+ const callback = jest.fn();
+
+ await loginFn(null, null, null, mockProfile, callback);
+
+ expect(resolveAppConfigForUser).not.toHaveBeenCalled();
+ expect(getAppConfig).toHaveBeenCalledWith({ baseOnly: true });
+ });
+
+ it('should block login when tenant config restricts the domain', async () => {
+ const { isEmailDomainAllowed } = require('@librechat/api');
+ const provider = 'google';
+ const googleId = 'google-tenant-blocked';
+ const email = 'blocked@example.com';
+
+ const existingUser = {
+ _id: 'userBlocked',
+ email,
+ provider: 'google',
+ googleId,
+ tenantId: 'tenant-restrict',
+ role: 'USER',
+ };
+
+ findUser.mockResolvedValue(existingUser);
+ resolveAppConfigForUser.mockResolvedValue({
+ registration: { allowedDomains: ['other.com'] },
+ });
+ isEmailDomainAllowed.mockReturnValueOnce(true).mockReturnValueOnce(false);
+
+ const mockProfile = {
+ id: googleId,
+ emails: [{ value: email, verified: true }],
+ photos: [{ value: 'https://example.com/avatar.png' }],
+ name: { givenName: 'Blocked', familyName: 'User' },
+ };
+
+ const loginFn = socialLogin(provider, mockGetProfileDetails);
+ const callback = jest.fn();
+
+ await loginFn(null, null, null, mockProfile, callback);
+
+ expect(callback).toHaveBeenCalledWith(
+ expect.objectContaining({ message: 'Email domain not allowed' }),
+ );
+ });
+ });
});
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();
+ });
+ });
+});
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/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/utils/tokens.spec.js b/api/utils/tokens.spec.js
index 6cecdb95c8..dfa6762ee5 100644
--- a/api/utils/tokens.spec.js
+++ b/api/utils/tokens.spec.js
@@ -1813,3 +1813,57 @@ describe('GLM Model Tests (Zhipu AI)', () => {
});
});
});
+
+describe('Mistral Model Tests', () => {
+ describe('getModelMaxTokens', () => {
+ test('should return correct tokens for mistral-large-3 (256k context)', () => {
+ expect(getModelMaxTokens('mistral-large-3', EModelEndpoint.custom)).toBe(
+ maxTokensMap[EModelEndpoint.custom]['mistral-large-3'],
+ );
+ });
+
+ test('should match mistral-large-3 for suffixed variants', () => {
+ expect(getModelMaxTokens('mistral-large-3-instruct', EModelEndpoint.custom)).toBe(
+ maxTokensMap[EModelEndpoint.custom]['mistral-large-3'],
+ );
+ });
+
+ test('should not match mistral-large-3 for generic mistral-large', () => {
+ expect(getModelMaxTokens('mistral-large', EModelEndpoint.custom)).toBe(
+ maxTokensMap[EModelEndpoint.custom]['mistral-large'],
+ );
+ expect(getModelMaxTokens('mistral-large-latest', EModelEndpoint.custom)).toBe(
+ maxTokensMap[EModelEndpoint.custom]['mistral-large'],
+ );
+ });
+ });
+
+ describe('matchModelName', () => {
+ test('should match mistral-large-3 exactly', () => {
+ expect(matchModelName('mistral-large-3', EModelEndpoint.custom)).toBe('mistral-large-3');
+ });
+
+ test('should match mistral-large-3 for prefixed/suffixed variants', () => {
+ expect(matchModelName('mistral/mistral-large-3', EModelEndpoint.custom)).toBe(
+ 'mistral-large-3',
+ );
+ expect(matchModelName('mistral-large-3-instruct', EModelEndpoint.custom)).toBe(
+ 'mistral-large-3',
+ );
+ });
+
+ test('should match generic mistral-large for non-3 variants', () => {
+ expect(matchModelName('mistral-large-latest', EModelEndpoint.custom)).toBe('mistral-large');
+ });
+ });
+
+ describe('findMatchingPattern', () => {
+ test('should prefer mistral-large-3 over mistral-large for mistral-large-3 variants', () => {
+ const result = findMatchingPattern(
+ 'mistral-large-3-instruct',
+ maxTokensMap[EModelEndpoint.custom],
+ );
+ expect(result).toBe('mistral-large-3');
+ });
+ });
+});
diff --git a/bun.lock b/bun.lock
index 39d9641ec4..fb1ec00840 100644
--- a/bun.lock
+++ b/bun.lock
@@ -37,7 +37,7 @@
},
"api": {
"name": "@librechat/backend",
- "version": "0.8.3",
+ "version": "0.8.4",
"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.57",
"@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,13 +112,13 @@
"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",
+ "yauzl": "^3.2.1",
"zod": "^3.22.4",
},
"devDependencies": {
@@ -129,13 +130,13 @@
},
"client": {
"name": "@librechat/frontend",
- "version": "0.8.3",
+ "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.25",
+ "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,10 +311,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.57",
"@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 +338,14 @@
"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",
+ "yauzl": "^3.2.1",
"zod": "^3.22.4",
},
},
"packages/client": {
"name": "@librechat/client",
- "version": "0.4.54",
+ "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.302",
+ "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.38",
+ "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,14 +913,16 @@
"@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=="],
+ "@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=="],
@@ -1065,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=="],
@@ -1485,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.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.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"],
@@ -1589,11 +1597,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,59 +1821,55 @@
"@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=="],
"@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-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=="],
@@ -1877,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=="],
@@ -1957,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=="],
@@ -2009,6 +2013,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 +2157,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=="],
@@ -2213,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=="],
@@ -2295,6 +2303,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=="],
@@ -2359,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=="],
@@ -2445,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=="],
@@ -2755,7 +2765,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=="],
@@ -2855,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=="],
@@ -3017,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=="],
@@ -3037,7 +3049,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=="],
@@ -3063,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=="],
@@ -3389,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=="],
@@ -3839,7 +3851,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=="],
@@ -3873,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=="],
@@ -4003,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=="],
@@ -4021,8 +4033,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=="],
@@ -4051,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=="],
@@ -4295,9 +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-web-to-node-stream": ["readable-web-to-node-stream@3.0.2", "", { "dependencies": { "readable-stream": "^3.6.0" } }, "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw=="],
+ "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=="],
@@ -4379,7 +4387,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 +4571,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 +4635,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 +4657,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 +4741,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=="],
@@ -4779,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=="],
@@ -4959,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=="],
@@ -5225,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=="],
@@ -5367,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=="],
@@ -5397,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=="],
@@ -5439,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=="],
@@ -5453,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=="],
@@ -5473,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=="],
@@ -5505,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=="],
@@ -5529,6 +5733,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 +5755,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=="],
@@ -5567,10 +5787,14 @@
"@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=="],
+ "@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,18 +5821,16 @@
"@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/@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.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/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/@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=="],
"@librechat/frontend/lucide-react": ["lucide-react@0.394.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, "sha512-PzTbJ0bsyXRhH59k5qe7MpTd5MxlpYZUcM9kGSwvPGAfnn0J6FElDwu2EX6Vuh//F7y60rcVJiFQ7EK9DCMgfw=="],
@@ -5629,45 +5851,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=="],
@@ -5911,33 +6097,77 @@
"@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=="],
+
+ "@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=="],
@@ -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,9 @@
"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=="],
"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=="],
@@ -6001,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=="],
@@ -6025,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=="],
@@ -6087,8 +6325,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=="],
@@ -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=="],
@@ -6151,12 +6391,16 @@
"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=="],
"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=="],
@@ -6167,26 +6411,46 @@
"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=="],
+ "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=="],
+ "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=="],
"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=="],
@@ -6199,8 +6463,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=="],
@@ -6215,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=="],
@@ -6233,8 +6501,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 +6525,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=="],
@@ -6279,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=="],
@@ -6291,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=="],
@@ -6305,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=="],
@@ -6349,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=="],
@@ -6365,8 +6633,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=="],
@@ -6375,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=="],
@@ -6399,6 +6667,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=="],
@@ -6411,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=="],
@@ -6425,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=="],
@@ -6455,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=="],
@@ -6467,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=="],
@@ -6493,13 +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=="],
- "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/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=="],
@@ -6717,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=="],
@@ -6805,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=="],
@@ -6827,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=="],
@@ -6863,6 +7433,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 +7445,52 @@
"@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/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=="],
"@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 +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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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=="],
@@ -7025,8 +7711,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 +7729,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,42 +7841,52 @@
"@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/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/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=="],
- "@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=="],
@@ -7223,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=="],
@@ -7237,12 +7919,16 @@
"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=="],
"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 +7951,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=="],
@@ -7287,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=="],
@@ -7303,8 +8007,28 @@
"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/@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=="],
@@ -7317,36 +8041,68 @@
"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=="],
"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=="],
+ "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=="],
"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/@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=="],
@@ -7369,40 +8125,42 @@
"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=="],
- "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=="],
@@ -7433,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=="],
@@ -7445,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=="],
@@ -7457,65 +8219,9 @@
"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=="],
-
- "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=="],
@@ -7637,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=="],
@@ -7697,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=="],
@@ -7753,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=="],
@@ -7765,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=="],
@@ -7813,6 +8625,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,7 +8737,67 @@
"@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/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=="],
@@ -7937,9 +8811,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=="],
@@ -7951,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=="],
@@ -7971,8 +8839,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=="],
@@ -7987,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=="],
@@ -7999,6 +8875,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=="],
@@ -8015,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=="],
@@ -8073,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=="],
@@ -8119,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=="],
@@ -8483,40 +9367,48 @@
"@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=="],
- "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="],
+ "@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=="],
- "@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=="],
+ "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"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=="],
- "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 1c698d08a3..375e4418a7 100644
--- a/client/jest.config.cjs
+++ b/client/jest.config.cjs
@@ -1,4 +1,4 @@
-/** v0.8.3 */
+/** v0.8.4 */
module.exports = {
roots: ['/src'],
testEnvironment: 'jsdom',
@@ -41,7 +41,9 @@ module.exports = {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'jest-file-loader',
},
- transformIgnorePatterns: ['node_modules/?!@zattoo/use-double-click'],
+ transformIgnorePatterns: [
+ '/node_modules/(?!(@zattoo/use-double-click|@dicebear|@react-dnd|react-dnd.*|dnd-core|filenamify|filename-reserved-regex|heic-to|lowlight|highlight\\.js|fault|react-markdown|unified|bail|trough|devlop|is-.*|parse-entities|stringify-entities|character-.*|trim-lines|style-to-object|inline-style-parser|html-url-attributes|escape-string-regexp|longest-streak|zwitch|ccount|markdown-table|comma-separated-tokens|space-separated-tokens|web-namespaces|property-information|remark-.*|rehype-.*|recma-.*|hast.*|mdast-.*|unist-.*|vfile.*|micromark.*|estree-util-.*|decode-named-character-reference)/)/',
+ ],
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect', '/test/setupTests.js'],
clearMocks: true,
};
diff --git a/client/nginx.conf b/client/nginx.conf
index c91c47a23f..906b3af128 100644
--- a/client/nginx.conf
+++ b/client/nginx.conf
@@ -86,9 +86,15 @@ server {
# location /api {
# proxy_pass http://api:3080/api;
+# proxy_set_header X-Forwarded-Proto $scheme;
+# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+# proxy_set_header Host $host;
# }
# location / {
# proxy_pass http://api:3080;
+# proxy_set_header X-Forwarded-Proto $scheme;
+# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+# proxy_set_header Host $host;
# }
#}
diff --git a/client/package.json b/client/package.json
index 250afc9990..5fe9cddcc7 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "@librechat/frontend",
- "version": "v0.8.3",
+ "version": "v0.8.4",
"description": "",
"type": "module",
"scripts": {
@@ -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",
@@ -95,7 +95,7 @@
"react-hook-form": "^7.43.9",
"react-i18next": "^15.4.0",
"react-markdown": "^9.0.1",
- "react-resizable-panels": "^3.0.6",
+ "react-resizable-panels": "^4.7.4",
"react-router-dom": "^6.30.3",
"react-speech-recognition": "^3.10.0",
"react-textarea-autosize": "^8.4.0",
@@ -122,7 +122,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",
+ "@happy-dom/jest-environment": "^20.8.9",
"@tanstack/react-query-devtools": "^4.29.0",
"@testing-library/dom": "^9.3.0",
"@testing-library/jest-dom": "^5.16.5",
diff --git a/client/src/@types/react.d.ts b/client/src/@types/react.d.ts
new file mode 100644
index 0000000000..edf0b7af3f
--- /dev/null
+++ b/client/src/@types/react.d.ts
@@ -0,0 +1,8 @@
+import 'react';
+
+declare module 'react' {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ interface HTMLAttributes {
+ inert?: boolean | '' | undefined;
+ }
+}
diff --git a/client/src/Providers/ActivePanelContext.tsx b/client/src/Providers/ActivePanelContext.tsx
index 4a8d6ccfc4..46b2a189b7 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() {
@@ -35,3 +35,11 @@ export function useActivePanel() {
}
return context;
}
+
+/** Returns `active` when it matches a known link, otherwise the first link's id. */
+export function resolveActivePanel(active: string, links: { id: string }[]): string {
+ if (links.length > 0 && links.some((l) => l.id === active)) {
+ return active;
+ }
+ return links[0]?.id ?? active;
+}
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/MessagesViewContext.tsx b/client/src/Providers/MessagesViewContext.tsx
index f1cae204a4..c44972918c 100644
--- a/client/src/Providers/MessagesViewContext.tsx
+++ b/client/src/Providers/MessagesViewContext.tsx
@@ -140,6 +140,55 @@ export function useMessagesOperations() {
);
}
+type OptionalMessagesOps = Pick<
+ MessagesViewContextValue,
+ 'ask' | 'regenerate' | 'handleContinue' | 'getMessages' | 'setMessages'
+>;
+
+const NOOP_OPS: OptionalMessagesOps = {
+ ask: () => {},
+ regenerate: () => {},
+ handleContinue: () => {},
+ getMessages: () => undefined,
+ setMessages: () => {},
+};
+
+/**
+ * Hook for components that need message operations but may render outside MessagesViewProvider
+ * (e.g. the /search route). Returns no-op stubs when the provider is absent — UI actions will
+ * be silently discarded rather than crashing. Callers must use optional chaining on
+ * `getMessages()` results, as it returns `undefined` outside the provider.
+ */
+export function useOptionalMessagesOperations(): OptionalMessagesOps {
+ const context = useContext(MessagesViewContext);
+ const ask = context?.ask;
+ const regenerate = context?.regenerate;
+ const handleContinue = context?.handleContinue;
+ const getMessages = context?.getMessages;
+ const setMessages = context?.setMessages;
+ return useMemo(
+ () => ({
+ ask: ask ?? NOOP_OPS.ask,
+ regenerate: regenerate ?? NOOP_OPS.regenerate,
+ handleContinue: handleContinue ?? NOOP_OPS.handleContinue,
+ getMessages: getMessages ?? NOOP_OPS.getMessages,
+ setMessages: setMessages ?? NOOP_OPS.setMessages,
+ }),
+ [ask, regenerate, handleContinue, getMessages, setMessages],
+ );
+}
+
+/**
+ * Hook for components that need conversation data but may render outside MessagesViewProvider
+ * (e.g. the /search route). Returns `undefined` for both fields when the provider is absent.
+ */
+export function useOptionalMessagesConversation() {
+ const context = useContext(MessagesViewContext);
+ const conversation = context?.conversation;
+ const conversationId = context?.conversationId;
+ return useMemo(() => ({ conversation, conversationId }), [conversation, conversationId]);
+}
+
/** Hook for components that only need message state */
export function useMessagesState() {
const { index, latestMessageId, latestMessageDepth, setLatestMessage } = useMessagesViewContext();
diff --git a/client/src/Providers/PromptGroupsContext.tsx b/client/src/Providers/PromptGroupsContext.tsx
index 7c9dbe8258..3df373b165 100644
--- a/client/src/Providers/PromptGroupsContext.tsx
+++ b/client/src/Providers/PromptGroupsContext.tsx
@@ -2,9 +2,9 @@ import React, { createContext, useContext, ReactNode, useMemo } from 'react';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import type { TPromptGroup } from 'librechat-data-provider';
import type { PromptOption } from '~/common';
-import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
import { usePromptGroupsNav, useHasAccess } from '~/hooks';
import { useGetAllPromptGroups } from '~/data-provider';
+import { CategoryIcon } from '~/components/Prompts';
import { mapPromptGroups } from '~/utils';
type AllPromptGroupsData =
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..0f2f89e8f7
--- /dev/null
+++ b/client/src/Providers/__tests__/ActivePanelContext.spec.tsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { render, fireEvent, screen } from '@testing-library/react';
+import '@testing-library/jest-dom/extend-expect';
+import {
+ ActivePanelProvider,
+ resolveActivePanel,
+ useActivePanel,
+} from '~/Providers/ActivePanelContext';
+
+const STORAGE_KEY = 'side:active-panel';
+
+function TestConsumer() {
+ const { active, setActive } = useActivePanel();
+ return (
+
+ {active}
+ setActive('bookmarks')} />
+
+ );
+}
+
+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();
+ });
+});
+
+describe('resolveActivePanel', () => {
+ const links = [{ id: 'conversations' }, { id: 'prompts' }, { id: 'files' }];
+
+ it('returns active when it matches a link', () => {
+ expect(resolveActivePanel('prompts', links)).toBe('prompts');
+ });
+
+ it('falls back to first link when active does not match', () => {
+ expect(resolveActivePanel('hide-panel', links)).toBe('conversations');
+ });
+
+ it('returns active unchanged when links is empty', () => {
+ expect(resolveActivePanel('agents', [])).toBe('agents');
+ });
+
+ it('falls back to the only link when active is stale', () => {
+ expect(resolveActivePanel('agents', [{ id: 'conversations' }])).toBe('conversations');
+ });
+});
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/__tests__/MessagesViewContext.spec.tsx b/client/src/Providers/__tests__/MessagesViewContext.spec.tsx
new file mode 100644
index 0000000000..88cd6f702d
--- /dev/null
+++ b/client/src/Providers/__tests__/MessagesViewContext.spec.tsx
@@ -0,0 +1,53 @@
+import { renderHook } from '@testing-library/react';
+import {
+ useOptionalMessagesOperations,
+ useOptionalMessagesConversation,
+} from '../MessagesViewContext';
+
+describe('useOptionalMessagesOperations', () => {
+ it('returns noop stubs when rendered outside MessagesViewProvider', () => {
+ const { result } = renderHook(() => useOptionalMessagesOperations());
+
+ expect(result.current.ask).toBeInstanceOf(Function);
+ expect(result.current.regenerate).toBeInstanceOf(Function);
+ expect(result.current.handleContinue).toBeInstanceOf(Function);
+ expect(result.current.getMessages).toBeInstanceOf(Function);
+ expect(result.current.setMessages).toBeInstanceOf(Function);
+ });
+
+ it('noop stubs do not throw when called', () => {
+ const { result } = renderHook(() => useOptionalMessagesOperations());
+
+ expect(() => result.current.ask({} as never)).not.toThrow();
+ expect(() => result.current.regenerate({} as never)).not.toThrow();
+ expect(() => result.current.handleContinue({} as never)).not.toThrow();
+ expect(() => result.current.setMessages([])).not.toThrow();
+ });
+
+ it('getMessages returns undefined outside the provider', () => {
+ const { result } = renderHook(() => useOptionalMessagesOperations());
+ expect(result.current.getMessages()).toBeUndefined();
+ });
+
+ it('returns stable references across re-renders', () => {
+ const { result, rerender } = renderHook(() => useOptionalMessagesOperations());
+ const first = result.current;
+ rerender();
+ expect(result.current).toBe(first);
+ });
+});
+
+describe('useOptionalMessagesConversation', () => {
+ it('returns undefined fields when rendered outside MessagesViewProvider', () => {
+ const { result } = renderHook(() => useOptionalMessagesConversation());
+ expect(result.current.conversation).toBeUndefined();
+ expect(result.current.conversationId).toBeUndefined();
+ });
+
+ it('returns stable references across re-renders', () => {
+ const { result, rerender } = renderHook(() => useOptionalMessagesConversation());
+ const first = result.current;
+ rerender();
+ expect(result.current).toBe(first);
+ });
+});
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/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/common/types.ts b/client/src/common/types.ts
index d47ff02bd8..6ca408685f 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:
| {
@@ -362,6 +355,28 @@ export type TOptions = {
export type TAskFunction = (props: TAskProps, options?: TOptions) => void;
+/**
+ * Stable context object passed from non-memo'd wrapper components (Message, MessageContent)
+ * to memo'd inner components (MessageRender, ContentRender) via props.
+ *
+ * This avoids subscribing to ChatContext inside memo'd components, which would bypass React.memo
+ * and cause unnecessary re-renders when `isSubmitting` changes during streaming.
+ *
+ * The `isSubmitting` property should use a getter backed by a ref so it returns the current
+ * value at call-time (for callback guards) without being a reactive dependency.
+ */
+export type TMessageChatContext = {
+ ask: (...args: Parameters) => void;
+ index: number;
+ regenerate: (message: t.TMessage, options?: { addedConvo?: t.TConversation | null }) => void;
+ conversation: t.TConversation | null;
+ latestMessageId: string | undefined;
+ latestMessageDepth: number | undefined;
+ handleContinue: (e: React.MouseEvent) => void;
+ /** Should be a getter backed by a ref — reads current value without triggering re-renders */
+ readonly isSubmitting: boolean;
+};
+
export type TMessageProps = {
conversation?: t.TConversation | null;
messageId?: string | null;
@@ -561,11 +576,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..816705a0db 100644
--- a/client/src/components/Agents/Marketplace.tsx
+++ b/client/src/components/Agents/Marketplace.tsx
@@ -1,23 +1,16 @@
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 CategoryTabs from './CategoryTabs';
import SearchBar from './SearchBar';
import AgentGrid from './AgentGrid';
-import store from '~/store';
+import { cn } from '~/utils';
interface AgentMarketplaceProps {
className?: string;
@@ -34,13 +27,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 +48,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 +173,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 +194,136 @@ 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 */}
+
+ {/* 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 +331,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 +346,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 +354,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 +364,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 +386,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/Artifacts/ArtifactPreview.tsx b/client/src/components/Artifacts/ArtifactPreview.tsx
index c125889c88..8257f76887 100644
--- a/client/src/components/Artifacts/ArtifactPreview.tsx
+++ b/client/src/components/Artifacts/ArtifactPreview.tsx
@@ -6,7 +6,7 @@ import type {
} from '@codesandbox/sandpack-react/unstyled';
import type { TStartupConfig } from 'librechat-data-provider';
import type { ArtifactFiles } from '~/common';
-import { sharedFiles, sharedOptions } from '~/utils/artifacts';
+import { sharedFiles, buildSandpackOptions } from '~/utils/artifacts';
export const ArtifactPreview = memo(function ({
files,
@@ -39,15 +39,10 @@ export const ArtifactPreview = memo(function ({
};
}, [currentCode, files, fileKey]);
- const options: typeof sharedOptions = useMemo(() => {
- if (!startupConfig) {
- return sharedOptions;
- }
- return {
- ...sharedOptions,
- bundlerURL: template === 'static' ? startupConfig.staticBundlerURL : startupConfig.bundlerURL,
- };
- }, [startupConfig, template]);
+ const options: SandpackProviderProps['options'] = useMemo(
+ () => buildSandpackOptions(template, startupConfig),
+ [startupConfig, template],
+ );
if (Object.keys(artifactFiles).length === 0) {
return null;
diff --git a/client/src/components/Artifacts/Artifacts.tsx b/client/src/components/Artifacts/Artifacts.tsx
index 74b2e20180..e2a322b1ad 100644
--- a/client/src/components/Artifacts/Artifacts.tsx
+++ b/client/src/components/Artifacts/Artifacts.tsx
@@ -1,15 +1,16 @@
-import { useRef, useState, useEffect } from 'react';
+import { useRef, useState, useEffect, useCallback } from 'react';
+import copy from 'copy-to-clipboard';
import * as Tabs from '@radix-ui/react-tabs';
import { Code, Play, RefreshCw, X } from 'lucide-react';
import { useSetRecoilState, useResetRecoilState } from 'recoil';
import { Button, Spinner, useMediaQuery, Radio } from '@librechat/client';
import type { SandpackPreviewRef } from '@codesandbox/sandpack-react';
+import CopyButton from '~/components/Messages/Content/CopyButton';
import { useShareContext, useMutationState } from '~/Providers';
import useArtifacts from '~/hooks/Artifacts/useArtifacts';
import DownloadArtifact from './DownloadArtifact';
import ArtifactVersion from './ArtifactVersion';
import ArtifactTabs from './ArtifactTabs';
-import { CopyCodeButton } from './Code';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
import store from '~/store';
@@ -30,6 +31,7 @@ export default function Artifacts() {
const [height, setHeight] = useState(90);
const [isDragging, setIsDragging] = useState(false);
const [blurAmount, setBlurAmount] = useState(0);
+ const [isCopied, setIsCopied] = useState(false);
const dragStartY = useRef(0);
const dragStartHeight = useRef(90);
const setArtifactsVisible = useSetRecoilState(store.artifactsVisibility);
@@ -86,6 +88,16 @@ export default function Artifacts() {
setCurrentArtifactId,
} = useArtifacts();
+ const handleCopyArtifact = useCallback(() => {
+ const content = currentArtifact?.content ?? '';
+ if (!content) {
+ return;
+ }
+ copy(content, { format: 'text/plain' });
+ setIsCopied(true);
+ setTimeout(() => setIsCopied(false), 3000);
+ }, [currentArtifact?.content]);
+
const handleDragStart = (e: React.PointerEvent) => {
setIsDragging(true);
dragStartY.current = e.clientY;
@@ -216,7 +228,7 @@ export default function Artifacts() {
{/* Header */}
@@ -234,6 +246,7 @@ export default function Artifacts() {
value={activeTab}
onChange={setActiveTab}
disabled={isMutating && activeTab !== 'code'}
+ buttonClassName="h-9 px-3 gap-1.5"
/>
)}
@@ -249,6 +262,7 @@ export default function Artifacts() {
)}
-
+
diff --git a/client/src/components/Artifacts/Code.tsx b/client/src/components/Artifacts/Code.tsx
index 001b010908..225c94b014 100644
--- a/client/src/components/Artifacts/Code.tsx
+++ b/client/src/components/Artifacts/Code.tsx
@@ -1,9 +1,8 @@
-import React, { memo, useState } from 'react';
-import copy from 'copy-to-clipboard';
-import { Button } from '@librechat/client';
-import { Copy, CircleCheckBig } from 'lucide-react';
-import { handleDoubleClick } from '~/utils';
-import { useLocalize } from '~/hooks';
+import React, { memo, useEffect, useRef, useState } from 'react';
+import rehypeKatex from 'rehype-katex';
+import ReactMarkdown from 'react-markdown';
+import rehypeHighlight from 'rehype-highlight';
+import { handleDoubleClick, langSubset } from '~/utils';
type TCodeProps = {
inline: boolean;
@@ -26,28 +25,70 @@ export const code: React.ElementType = memo(({ inline, className, children }: TC
return {children};
});
-export const CopyCodeButton: React.FC<{ content: string }> = ({ content }) => {
- const localize = useLocalize();
- const [isCopied, setIsCopied] = useState(false);
+const rehypePlugins = [
+ [rehypeKatex],
+ [
+ rehypeHighlight,
+ {
+ detect: true,
+ ignoreMissing: true,
+ subset: langSubset,
+ },
+ ],
+];
- const handleCopy = () => {
- copy(content, { format: 'text/plain' });
- setIsCopied(true);
- setTimeout(() => setIsCopied(false), 3000);
- };
+export const CodeMarkdown = memo(
+ ({ content = '', isSubmitting }: { content: string; isSubmitting: boolean }) => {
+ const scrollRef = useRef(null);
+ const [userScrolled, setUserScrolled] = useState(false);
- return (
-
- {isCopied ? (
-
- ) : (
-
- )}
-
- );
-};
+ 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 (
+
+
+ {content}
+
+
+ );
+ },
+);
diff --git a/client/src/components/Artifacts/DownloadArtifact.tsx b/client/src/components/Artifacts/DownloadArtifact.tsx
index c8fb6a12fe..b6d2873c46 100644
--- a/client/src/components/Artifacts/DownloadArtifact.tsx
+++ b/client/src/components/Artifacts/DownloadArtifact.tsx
@@ -38,6 +38,7 @@ const DownloadArtifact = ({ artifact }: { artifact: Artifact }) => {
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"
>
-
+
);
}
diff --git a/client/src/components/Chat/ChatView.tsx b/client/src/components/Chat/ChatView.tsx
index 66dec68f64..d2bd7edf0d 100644
--- a/client/src/components/Chat/ChatView.tsx
+++ b/client/src/components/Chat/ChatView.tsx
@@ -6,7 +6,7 @@ import { useParams } from 'react-router-dom';
import { Constants, buildTree } from 'librechat-data-provider';
import type { TMessage } from 'librechat-data-provider';
import type { ChatFormValues } from '~/common';
-import { ChatContext, AddedChatContext, useFileMapContext, ChatFormProvider } from '~/Providers';
+import { ChatContext, AddedChatContext, ChatFormProvider, useFileMapContext } from '~/Providers';
import { useAddedResponse, useResumeOnLoad, useAdaptiveSSE, useChatHelpers } from '~/hooks';
import ConversationStarters from './Input/ConversationStarters';
import { useGetMessagesByConvoId } from '~/data-provider';
@@ -34,6 +34,10 @@ function ChatView({ index = 0 }: { index?: number }) {
const rootSubmission = useRecoilValue(store.submissionByIndex(index));
const centerFormOnLanding = useRecoilValue(store.centerFormOnLanding);
+ const methods = useForm({
+ defaultValues: { text: '' },
+ });
+
const fileMap = useFileMapContext();
const { data: messagesTree = null, isLoading } = useGetMessagesByConvoId(conversationId ?? '', {
@@ -56,10 +60,6 @@ function ChatView({ index = 0 }: { index?: number }) {
// Wait for messages to load before resuming to avoid race condition
useResumeOnLoad(conversationId, chatHelpers.getMessages, index, !isLoading);
- const methods = useForm({
- defaultValues: { text: '' },
- });
-
let content: JSX.Element | null | undefined;
const isLandingPage =
(!messagesTree || messagesTree.length === 0) &&
@@ -82,7 +82,7 @@ function ChatView({ index = 0 }: { index?: number }) {
- {!isLoading && }
+
<>