diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..2fb10cc0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,29 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/workspace-mcp-server" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + assignees: + - "google-gemini" + labels: + - "dependencies" + - "npm" + commit-message: + prefix: "chore" + include: "scope" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + assignees: + - "google-gemini" + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "chore" + include: "scope" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..cd6328f8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,88 @@ +name: CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x, 22.x, 24.x] + + steps: + - uses: actions/checkout@v5 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: package-lock.json + + - name: Install libsecret + run: sudo apt-get update && sudo apt-get install -y libsecret-1-0 + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Run type checking + run: npx tsc --noEmit --project workspace-mcp-server + + - name: Run tests with coverage + run: npm run test:ci + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + directory: ./workspace-mcp-server/coverage + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + build: + runs-on: ubuntu-latest + needs: test + + steps: + - uses: actions/checkout@v5 + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: '20.x' + cache: 'npm' + cache-dependency-path: package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Upload build artifacts + uses: actions/upload-artifact@v5 + with: + name: dist + path: workspace-mcp-server/dist/ + + security: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - name: Run security audit + run: npm audit --audit-level=moderate + continue-on-error: true + + - name: Check for known vulnerabilities + run: npx audit-ci --moderate + continue-on-error: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..6b409994 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,53 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Build extension + run: npm run build --workspace=workspace-mcp-server + + - name: Create release assets + run: npm run release + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: release/workspace-mcp-server.tar.gz + asset_name: workspace-mcp-server.tar.gz + asset_content_type: application/gzip \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..84d3ddd3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# macOS +.DS_Store + +# logs +logs + +# Dependencies +node_modules/ + +# Build outputs +dist/ + +# Environment files +.env +.env.local + +# Logs +*.log + +# Coverage +coverage/ + +# Editor files +*.swp +*.swo +*~ +.idea/ +.vscode/ + +# Auth tokens +token.json +gemini-cli-workspace-token.json +.gemini-cli-workspace-master-key + +.gemini/ +commit_message.txt + +# Release directory +release/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..9a683562 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,190 @@ +# How to Contribute + +We would love to accept your patches and contributions to this project. + +## Before you begin + +### Sign our Contributor License Agreement + +Contributions to this project must be accompanied by a +[Contributor License Agreement](https://cla.developers.google.com/about) (CLA). +You (or your employer) retain the copyright to your contribution; this simply +gives us permission to use and redistribute your contributions as part of the +project. + +If you or your current employer have already signed the Google CLA (even if it +was for a different project), you probably don't need to do it again. + +Visit to see your current agreements or to +sign a new one. + +### Review our Community Guidelines + +This project follows [Google's Open Source Community +Guidelines](https://opensource.google/conduct/). + +## Contribution Process + +### Code Reviews + +All submissions, including submissions by project members, require review. We +use [GitHub pull requests](https://docs.github.com/articles/about-pull-requests) +for this purpose. + +### Self Assigning Issues + +If you're looking for an issue to work on, check out our list of issues that are labeled ["help wanted"](https://github.com/google-gemini/gemini-cli-workspace/issues?q=is%3Aissue+state%3Aopen+label%3A%22help+wanted%22). + +To assign an issue to yourself, simply add a comment with the text `/assign`. The comment must contain only that text and nothing else. This command will assign the issue to you, provided it is not already assigned. + +Please note that you can have a maximum of 3 issues assigned to you at any given time. + +### Pull Request Guidelines + +To help us review and merge your PRs quickly, please follow these guidelines. PRs that do not meet these standards may be closed. + +#### 1. Link to an Existing Issue + +All PRs should be linked to an existing issue in our tracker. This ensures that every change has been discussed and is aligned with the project's goals before any code is written. + +- **For bug fixes:** The PR should be linked to the bug report issue. +- **For features:** The PR should be linked to the feature request or proposal issue that has been approved by a maintainer. + +If an issue for your change doesn't exist, please **open one first** and wait for feedback before you start coding. + +#### 2. Keep It Small and Focused + +We favor small, atomic PRs that address a single issue or add a single, self-contained feature. + +- **Do:** Create a PR that fixes one specific bug or adds one specific feature. +- **Don't:** Bundle multiple unrelated changes (e.g., a bug fix, a new feature, and a refactor) into a single PR. + +Large changes should be broken down into a series of smaller, logical PRs that can be reviewed and merged independently. + +#### 3. Use Draft PRs for Work in Progress + +If you'd like to get early feedback on your work, please use GitHub's **Draft Pull Request** feature. This signals to the maintainers that the PR is not yet ready for a formal review but is open for discussion and initial feedback. + +#### 4. Ensure All Checks Pass + +Before submitting your PR, ensure that all automated checks are passing by running `npm run test && npm run lint`. This command runs all tests, linting, and other style checks. + +#### 5. Write Clear Commit Messages and a Good PR Description + +Your PR should have a clear, descriptive title and a detailed description of the changes. Follow the [Conventional Commits](https://www.conventionalcommits.org/) standard for your commit messages. + +- **Good PR Title:** `feat(cli): Add --json flag to 'config get' command` +- **Bad PR Title:** `Made some changes` + +In the PR description, explain the "why" behind your changes and link to the relevant issue (e.g., `Fixes #123`). + +## Development Setup and Workflow + +This section guides contributors on how to build, modify, and understand the development setup of this project. + +### Setting Up the Development Environment + +**Prerequisites:** + +1. **Node.js**: + - **Development:** Please use Node.js `~20.19.0`. This specific version is required due to an upstream development dependency issue. You can use a tool like [nvm](https://github.com/nvm-sh/nvm) to manage Node.js versions. + - **Production:** For running the CLI in a production environment, any version of Node.js `>=20` is acceptable. +2. **Git** + +### Build Process + +To clone the repository: + +```bash +git clone https://github.com/google-gemini/gemini-cli-workspace.git # Or your fork's URL +cd gemini-cli-workspace +``` + +To install dependencies defined in `package.json` as well as root dependencies: + +```bash +npm install +``` + +To build the entire project (all packages): + +```bash +npm run build +``` + +This command typically compiles TypeScript to JavaScript, bundles assets, and prepares the packages for execution. Refer to `scripts/build.js` and `package.json` scripts for more details on what happens during the build. + +### Running Tests + +This project contains unit tests. + +#### Unit Tests + +To execute the unit test suite for the project: + +```bash +npm run test +``` + +This will run tests located in the `workspace-mcp-server/src/__tests__` directory. Ensure tests pass before submitting any changes. For a more comprehensive check, it is recommended to run `npm run test && npm run lint`. + +### Linting and Style Checks + +To ensure code quality and formatting consistency, run the linter and tests: + +```bash +npm run test && npm run lint +``` + +This command will run ESLint, Prettier, all tests, and other checks as defined in the project's `package.json`. + +_ProTip_ + +after cloning create a git precommit hook file to ensure your commits are always clean. + +```bash +echo " +# Run npm build and check for errors +#!/bin/sh +# Run tests and linting before commit +if ! (npm run test && npm run lint); then + echo "Pre-commit checks failed. Commit aborted." + exit 1 +fi +" > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit +``` + +#### Formatting + +To separately format the code in this project by running the following command from the root directory: + +```bash +npm run format +``` + +This command uses Prettier to format the code according to the project's style guidelines. + +#### Linting + +To separately lint the code in this project, run the following command from the root directory: + +```bash +npm run lint +``` + +### Coding Conventions + +- Please adhere to the coding style, patterns, and conventions used throughout the existing codebase. +- Consult [GEMINI.md](https://github.com/google-gemini/gemini-cli-workspace/blob/main/GEMINI.md) (typically found in the project root) for specific instructions related to AI-assisted development, including conventions for comments, and Git usage. +- **Imports:** Pay special attention to import paths. The project uses ESLint to enforce restrictions on relative imports between packages. + +### Project Structure + +- `workspace-mcp-server/`: The main workspace for the MCP server. + - `src/`: Contains the source code for the server. + - `__tests__/`: Contains all the tests. + - `auth/`: Handles authentication. + - `services/`: Contains the business logic for each service. + - `utils/`: Contains utility functions. + - `config/`: Contains configuration files. +- `scripts/`: Utility scripts for building, testing, and development tasks. \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 00000000..959c8bf5 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,21 @@ +This is a Gemini extension that provides tools for interacting with Google Workspace services like Google Docs. + +### Building and Running + +* **Install dependencies:** `npm install` +* **Build the project:** `npm run build --prefix workspace-mcp-server` + +### Development Conventions + +This project uses TypeScript and the Model Context Protocol (MCP) SDK to create a Gemini extension. The main entry point is `src/index.ts`, which initializes the MCP server and registers the available tools. + +The business logic for each service is separated into its own file in the `src/services` directory. For example, `src/services/DocsService.ts` contains the logic for interacting with the Google Docs API. + +Authentication is handled by the `src/auth/AuthManager.ts` file, which uses the `@google-cloud/local-auth` library to obtain and refresh OAuth 2.0 credentials. + +### Adding New Tools + +To add a new tool, you need to: + +1. Add a new method to the appropriate service file in `src/services`. +2. In `src/index.ts`, register the new tool with the MCP server by calling `server.registerTool()`. You will need to provide a name for the tool, a description, and the input schema using the `zod` library. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md index 5c82e71f..f9779ffa 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,57 @@ -# workspace -Access Google Workspace when using Gemini CLI +# Gemini Workspace Extension + +[![Build Status](https://github.com/google-gemini/gemini-cli-workspace/actions/workflows/ci.yml/badge.svg)](https://github.com/google-gemini/gemini-cli-workspace/actions/workflows/ci.yml) + +Google Workspace MCP Server Extension. + +This project is a Gemini extension that provides tools for interacting with Google Workspace services like Google Docs, Google Sheets, Google Slides, Google Calendar, Gmail, and Google Drive. + +## Installation + +To install the dependencies, run the following command: + +```bash +npm install +``` + +## Usage + +To build the project, run the following command: + +```bash +npm run build +``` + +To run the tests, run the following command: + +```bash +npm run test +``` + +To start the server, run the following command: + +```bash +npm start +``` + +## Important security consideration: Indirect Prompt Injection Risk + +When exposing any language model to untrusted data, there's a risk of an [indirect prompt injection attack](https://en.wikipedia.org/wiki/Prompt_injection). Agentic tools like Gemini CLI, connected to MCP servers, have access to a wide array of tools and APIs. + +This MCP server grants the agent the ability to read, modify, and delete your Google Account data, as well as other data shared with you. + +* Never use this with untrusted tools +* Never include untrusted inputs into the model context. This includes asking Gemini CLI to process mail, documents, or other resources from unverified sources. +* Untrusted inputs may contain hidden instructions that could hijack your CLI session. Attackers can then leverage this to modify, steal, or destroy your data. +* Always carefully review actions taken by Gemini CLI on your behalf to ensure they are correct and align with your intentions. + +## Contributing + +Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file for details on how to contribute to this project. + +## 📄 Legal + +- **License**: [Apache License 2.0](LICENSE) +- **Terms of Service**: [Terms of Service](https://policies.google.com/terms) +- **Privacy Policy**: [Privacy Policy](https://policies.google.com/privacy) +- **Security**: [Security Policy](SECURITY.md) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..857588c0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,6 @@ +# Reporting Security Issues + +To report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz). +We use g.co/vulnz for our intake, and do coordination and disclosure here on +GitHub (including using GitHub Security Advisory). The Google Security Team will +respond within 5 working days of your report on g.co/vulnz. diff --git a/cloud_function/index.js b/cloud_function/index.js new file mode 100644 index 00000000..058b6de5 --- /dev/null +++ b/cloud_function/index.js @@ -0,0 +1,256 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Import required packages +const functions = require('@google-cloud/functions-framework'); +const { SecretManagerServiceClient } = require('@google-cloud/secret-manager'); +const axios = require('axios'); +const { URL } = require('node:url'); + +// --- Configuration loaded from Environment Variables --- +// These are set in the Google Cloud Function's configuration +const CLIENT_ID = process.env.CLIENT_ID; +const SECRET_NAME = process.env.SECRET_NAME; +const REDIRECT_URI = process.env.REDIRECT_URI; + +// Fail fast if required environment variables are missing +if (!CLIENT_ID || !SECRET_NAME || !REDIRECT_URI) { + throw new Error( + 'Missing required environment variables: CLIENT_ID, SECRET_NAME, and REDIRECT_URI must be set.' + ); +} + +// --- Configuration for local storage (used in instructions) --- +const KEYCHAIN_SERVICE_NAME = 'gemini-cli-workspace-oauth'; +const KEYCHAIN_ACCOUNT_NAME = 'main-account'; +// --- END CONFIGURATION --- + +// Initialize the Secret Manager client +const secretClient = new SecretManagerServiceClient(); + +/** + * Helper function to access a secret from Secret Manager. + */ +async function getClientSecret() { + try { + console.log(`Accessing secret: ${SECRET_NAME}`); // Log which secret is being accessed + const [version] = await secretClient.accessSecretVersion({ + name: SECRET_NAME, + }); + const payload = version.payload.data.toString('utf8'); + console.log('Successfully accessed secret.'); + return payload; + } catch (error) { + console.error('Failed to access secret version:', error); + throw new Error('Could not retrieve client secret.'); + } +} + +/** + * HTTP Cloud Function that handles the OAuth 2.0 redirect. + */ +functions.http('oauthCallback', async (req, res) => { + const code = req.query.code; + const state = req.query.state; // The state is the base64 encoded local redirect URI + console.log(`Received request with code: ${code ? 'present' : 'missing'}`); + console.log(`Received state: ${state ? 'present' : 'missing'}`); + + if (!code) { + console.error('Missing authorization code in request query parameters.'); + return res.status(400).send('Error: Missing authorization code.'); + } + + try { + const clientSecret = await getClientSecret(); + + console.log(`Exchanging code for token using redirect_uri: ${REDIRECT_URI}`); + const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', { + client_id: CLIENT_ID, + client_secret: clientSecret, + code: code, + grant_type: 'authorization_code', + redirect_uri: REDIRECT_URI, + }); + + console.log('Token exchange successful.'); + const { access_token, refresh_token, expires_in, scope, token_type } = tokenResponse.data; + + // Calculate expiry_date (timestamp in milliseconds) + const expiry_date = Date.now() + (expires_in * 1000); + + // If state is present, decode it and decide whether to redirect or show manual page. + if (state) { + try { + // SECURITY: Enforce a reasonable size limit on the state parameter to prevent DoS. + if (state.length > 4096) { + throw new Error('State parameter exceeds size limit of 4KB.'); + } + + const payload = JSON.parse(Buffer.from(state, 'base64').toString('utf8')); + + // If not in manual mode and a URI is present, perform the redirect. + if (payload && payload.manual === false && payload.uri) { + + const redirectUrl = new URL(payload.uri); + + // SECURITY: Validate the redirect URI to prevent open redirect attacks. + if (redirectUrl.hostname !== 'localhost' && redirectUrl.hostname !== '127.0.0.1') { + throw new Error(`Invalid redirect hostname: ${redirectUrl.hostname}. Must be localhost or 127.0.0.1.`); + } + + console.log(`Automated flow detected. Redirecting to: ${payload.uri}`); + + const finalUrl = redirectUrl; // Use the validated URL object + finalUrl.searchParams.append('access_token', access_token); + if (refresh_token) { + finalUrl.searchParams.append('refresh_token', refresh_token); + } + finalUrl.searchParams.append('scope', scope); + finalUrl.searchParams.append('token_type', token_type); + finalUrl.searchParams.append('expiry_date', expiry_date.toString()); + + // SECURITY: Pass the CSRF token back to the client for validation. + if (payload.csrf) { + finalUrl.searchParams.append('state', payload.csrf); + } + + return res.redirect(302, finalUrl.toString()); + } + } catch (e) { + console.error('Error processing state or redirect. Falling back to manual page.', e); + } + } + + // --- Fallback to manual instructions --- + console.log('Manual flow detected or state was invalid. Rendering instructions page.'); + const credentialsJson = JSON.stringify({ + refresh_token: refresh_token, + scope: scope, + token_type: token_type, + access_token: access_token, + expiry_date: expiry_date + }, null, 2); // Pretty print JSON + + // 4. Display the JSON and add a copy button + instructions + res.set('Content-Type', 'text/html'); + res.status(200).send(` + + + OAuth Token Generated + + + +
+

Success! Credentials Ready

+

Copy the JSON block below. You'll need to store this as the password/secret in your operating system's keychain.

+ +

Credentials JSON

+ + + Copied! + +
+

Keychain Storage Instructions:

+
    +
  1. Open your OS Keychain/Credential Manager.
  2. +
  3. Create a new secure entry (e.g., a "Generic Password" on macOS, a "Windows Credential", or similar on Linux).
  4. +
  5. Set the **Service** (or equivalent field) to: ${KEYCHAIN_SERVICE_NAME}
  6. +
  7. Set the **Account** (or username field) to: ${KEYCHAIN_ACCOUNT_NAME}
  8. +
  9. Paste the copied JSON into the **Password/Secret** field.
  10. +
  11. Save the entry.
  12. +
+

Your local MCP server will now be able to find and use these credentials automatically.

+

(If keychain is unavailable, the server falls back to an encrypted file, but keychain is recommended.)

+
+
+ + + + + `); + + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + console.error('Error during token exchange:', error.response.data); + } else { + console.error('Error during token exchange:', error instanceof Error ? error.message : error); + } + res.status(500).send('An error occurred during the token exchange. Check function logs for details.'); + } +}); diff --git a/cloud_function/package.json b/cloud_function/package.json new file mode 100644 index 00000000..80383db8 --- /dev/null +++ b/cloud_function/package.json @@ -0,0 +1,10 @@ +{ + "name": "oauth-handler", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "@google-cloud/functions-framework": "^3.0.0", + "@google-cloud/secret-manager": "^5.0.0", + "axios": "^1.0.0" + } +} \ No newline at end of file diff --git a/commands/calendar/get-schedule.toml b/commands/calendar/get-schedule.toml new file mode 100644 index 00000000..8e2862c1 --- /dev/null +++ b/commands/calendar/get-schedule.toml @@ -0,0 +1,17 @@ +description = "Show your schedule for today, or the date specified" +prompt = """ +Please show me my schedule for today. To do that use the following steps: + +1) Use the people.getMe tool to get my information. +2) Use the time.getTimeZone to get my local time zone. +3) Either use the time.getCurrentDate or the user supplied date here: {{args}} to understand the current date. +4) Call calendar.list to identify the calendar associated with me from step 1. +5) Call calendar.listEvents to identify events on my calendar for the date we determined in step 3. +6) Format the list of events in my local time zone from step 2 in the following format: + +> HH:MM - HH:MM: Event Title (attendance status) - Event Location +> HH:MM - HH:MM: Event 2 Title (attendance status) - Event Location + +Note: If a calendar event has conflicting timezone information, prioritize the dateTime field over the timeZone field. + +""" diff --git a/commands/drive/search.toml b/commands/drive/search.toml new file mode 100644 index 00000000..4d4d091a --- /dev/null +++ b/commands/drive/search.toml @@ -0,0 +1,14 @@ +description = "Searches Google Drive for files matching a query." +prompt = """ +You are tasked with searching Google Drive for files. + +1) The user's search query is: {{args}} +2) Call the `drive.search` tool, passing the user's query as the `query` argument. +3) The `drive.search` tool returns a JSON object containing a list of files. +4) Format the result into a clear, readable list, showing only the `name` and `id` for each file found. +5) If no files are found, state that clearly. + +Example Output Format: +> File Name: My Document (ID: 1a2b3c4d5e6f7g8h9i0j) +> File Name: Project Report (ID: k1l2m3n4o5p6q7r8s9t0) +""" diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..adbb5dc9 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,102 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +const tseslint = require('@typescript-eslint/eslint-plugin'); +const tsParser = require('@typescript-eslint/parser'); +const licenseHeader = require('eslint-plugin-license-header'); +const importPlugin = require('eslint-plugin-import'); + +module.exports = [ + { + ignores: ['**/dist/', '*.js', '**/node_modules/', '**/coverage/', '!eslint.config.js'], + }, + { + files: ['workspace-mcp-server/src/**/*.ts'], + ignores: ['**/*.test.ts', '**/*.spec.ts'], + languageOptions: { + parser: tsParser, + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + ecmaVersion: 2020, + sourceType: 'module', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + }, + rules: { + ...tseslint.configs.recommended.rules, + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + 'prefer-const': 'warn', + }, + }, + { + files: ['workspace-mcp-server/src/**/*.test.ts', 'workspace-mcp-server/src/**/*.spec.ts'], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + }, + rules: { + ...tseslint.configs.recommended.rules, + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + 'prefer-const': 'warn', + }, + }, + { + files: ['./**/*.{tsx,ts,js}'], + ignores: ['workspace-mcp-server/src/index.ts'], // Has shebang which conflicts with license header + plugins: { + 'license-header': licenseHeader, + import: importPlugin, + }, + rules: { + 'license-header/header': [ + 'error', + [ + '/**', + ' * @license', + ' * Copyright 2025 Google LLC', + ' * SPDX-License-Identifier: Apache-2.0', + ' */', + ], + ], + 'import/enforce-node-protocol-usage': ['error', 'always'], + }, + }, + { + files: ['workspace-mcp-server/src/index.ts'], + plugins: { + import: importPlugin, + }, + rules: { + 'import/enforce-node-protocol-usage': ['error', 'always'], + }, + }, +]; \ No newline at end of file diff --git a/gemini-extension.json b/gemini-extension.json new file mode 100644 index 00000000..48d17742 --- /dev/null +++ b/gemini-extension.json @@ -0,0 +1,14 @@ +{ + "name": "google-workspace", + "version": "0.0.1", + "contextFileName": "workspace-mcp-server${/}WORKSPACE-Context.md", + "mcpServers": { + "google-workspace": { + "command": "node", + "args": [ + "scripts${/}start.js" + ], + "cwd": "${extensionPath}" + } + } +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..ca43b1b8 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,46 @@ +/** @type {import('jest').Config} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + projects: [ + { + displayName: 'workspace-mcp-server', + testMatch: ['/workspace-mcp-server/src/**/*.test.ts', '/workspace-mcp-server/src/**/*.spec.ts'], + transform: { + '^.+\\.ts$': ['ts-jest', { + tsconfig: { + strict: false + } + }], + }, + transformIgnorePatterns: [ + 'node_modules/(?!(marked)/)', + ], + moduleNameMapper: { + '^@/(.*)$': '/workspace-mcp-server/src/$1', + '\\.wasm$': '/workspace-mcp-server/src/__tests__/mocks/wasm.js', + '^marked$': '/workspace-mcp-server/src/__tests__/mocks/marked.js', + }, + setupFilesAfterEnv: ['/workspace-mcp-server/src/__tests__/setup.ts'], + collectCoverageFrom: [ + '/workspace-mcp-server/src/**/*.ts', + '!/workspace-mcp-server/src/**/*.d.ts', + '!/workspace-mcp-server/src/**/*.test.ts', + '!/workspace-mcp-server/src/**/*.spec.ts', + '!/workspace-mcp-server/src/index.ts', + ], + coverageDirectory: '/coverage', + coverageThreshold: { + global: { + branches: 45, + functions: 65, + lines: 60, + statements: 60, + }, + }, + } + ], + coverageReporters: ['text', 'lcov', 'html'], + testTimeout: 10000, + verbose: true, +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..c183e848 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,11335 @@ +{ + "name": "gemini-workspace-extension", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gemini-workspace-extension", + "version": "1.0.0", + "license": "Apache-2.0", + "workspaces": [ + "workspace-mcp-server" + ], + "dependencies": { + "@google-apps/chat": "^0.19.0", + "@google-cloud/local-auth": "^3.0.1", + "@googleapis/docs": "^5.0.1", + "@googleapis/drive": "^15.0.0", + "@modelcontextprotocol/sdk": "^1.17.5", + "dompurify": "^3.1.6", + "google-auth-library": "^10.4.0", + "googleapis": "^159.0.0", + "jsdom": "^27.0.0", + "keytar": "^7.9.0", + "marked": "^16.2.1" + }, + "bin": { + "gemini-workspace-server": "workspace-mcp-server/dist/index.js" + }, + "devDependencies": { + "@jest/globals": "^30.0.5", + "@types/dompurify": "^3.0.5", + "@types/jest": "^30.0.0", + "@types/jsdom": "^21.1.7", + "@types/node": "^24.2.1", + "@typescript-eslint/eslint-plugin": "^8.44.0", + "@typescript-eslint/parser": "^8.44.0", + "@vercel/ncc": "^0.38.3", + "archiver": "^7.0.1", + "esbuild": "^0.25.9", + "eslint": "^9.36.0", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-license-header": "^0.8.0", + "jest": "^30.1.3", + "minimist": "^1.2.8", + "ts-jest": "^29.4.1", + "ts-node": "^10.9.2", + "typescript": "^5.9.2" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", + "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.1" + } + }, + "node_modules/@asamuzakjp/css-color/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==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.0.tgz", + "integrity": "sha512-GrYRsKf8oVnPHsA+4dOAnPybrhT3cQ0xykXxjj2DaOni5xOlV1T8/Nqo+iNUO7wh9bs3jViIFsxJKFzDTU/ulQ==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/dom-selector/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==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@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" + } + }, + "node_modules/@babel/core/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" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "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-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "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-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "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/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz", + "integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@google-apps/chat": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@google-apps/chat/-/chat-0.19.1.tgz", + "integrity": "sha512-K7/bdQuWJp02QJp0AWDxPwvVfrNcdc71IYyDTAmEtt/zBmrwBJOqcPohqa8X59gF+G4y4xLqO8XjEk8c3+HyPw==", + "license": "Apache-2.0", + "dependencies": { + "google-gax": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/local-auth": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/local-auth/-/local-auth-3.0.1.tgz", + "integrity": "sha512-YJ3GFbksfHyEarbVHPSCzhKpjbnlAhdzg2SEf79l6ODukrSM1qUOqfopY232Xkw26huKSndyzmJz+A6b2WYn7Q==", + "license": "Apache-2.0", + "dependencies": { + "arrify": "^2.0.1", + "google-auth-library": "^9.0.0", + "open": "^7.0.3", + "server-destroy": "^1.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/local-auth/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@google-cloud/local-auth/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/@google-cloud/local-auth/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@google-cloud/local-auth/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@googleapis/docs": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@googleapis/docs/-/docs-5.0.1.tgz", + "integrity": "sha512-RvS48srbAlh0lhOI9/ng+U4gVFLMFgjervh3RU2rzMnwzrHUUikOKj6oikKRN+h16Bw3Hucz7jHP2m8u8p6VrQ==", + "license": "Apache-2.0", + "dependencies": { + "googleapis-common": "^8.0.2-rc.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@googleapis/drive": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-15.0.0.tgz", + "integrity": "sha512-PGuhTZMrS8HCJnFpYRhCU92GXgWEszku6XA517rYv1ohNB3nJFrVvjdMvzr/QOD4Q8R4Kg3fFLl6ULpKewLc0A==", + "license": "Apache-2.0", + "dependencies": { + "googleapis-common": "^8.0.2-rc.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.0.tgz", + "integrity": "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "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/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz", + "integrity": "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "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" + }, + "engines": { + "node": ">=18" + } + }, + "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", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "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" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "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": "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/@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==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "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/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz", + "integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", + "integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/type-utils": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", + "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", + "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", + "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", + "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", + "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", + "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", + "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.1", + "@typescript-eslint/tsconfig-utils": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", + "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", + "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vercel/ncc": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz", + "integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==", + "dev": true, + "license": "MIT", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@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" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.0.tgz", + "integrity": "sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.17.tgz", + "integrity": "sha512-j5zJcx6golJYTG6c05LUZ3Z8Gi+M62zRT/ycz4Xq4iCOdpcxwg7ngEYD4KA0eWZC7U17qh/Smq8bYbACJ0ipBA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "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.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "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" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "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": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssstyle": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz", + "integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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==", + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.237", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", + "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "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" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/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" + } + }, + "node_modules/eslint-plugin-license-header": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-license-header/-/eslint-plugin-license-header-0.8.0.tgz", + "integrity": "sha512-khTCz6G3JdoQfwrtY4XKl98KW4PpnWUKuFx8v+twIRhJADEyYglMDC0td8It75C1MZ88gcvMusWuUlJsos7gYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "requireindex": "^1.2.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.2.tgz", + "integrity": "sha512-/Szrn8nr+2TsQT1Gp8iIe/BEytJmbyfrbFh419DfGQSkEgNEhbPi7JRJuughjkTzPWgU9gBQf5AVu3DbHt0OXA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.1.tgz", + "integrity": "sha512-dTCcAe9fRQf06ELwel6lWWFrEbstwjUBYEhr5VRGoC+iPDZQucHppCowaIp8b8v92tU1G4X4H3b/Y6zXZxkMsQ==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "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": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/google-auth-library": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.4.1.tgz", + "integrity": "sha512-VlvZ+QDWng3aPh++0BSQlSJyjn4qgLLTmqylAR3as0dr6YwPkZpHcZAngAFr68TDVCUSQVRTkV73K/D3m7vEIg==", + "license": "Apache-2.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" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.4.tgz", + "integrity": "sha512-HmQ6zIYBs2EikTk+kjeHmtHprNTEpsnVaKONw9cwZZwUNCkUb+D5RYrJpCxyjdvIDvJp3wLbVReolJLRZRms1g==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "duplexify": "^4.1.3", + "google-auth-library": "^10.1.0", + "google-logging-utils": "^1.1.1", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^3.0.0", + "protobufjs": "^7.5.3", + "retry-request": "^8.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz", + "integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "159.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-159.0.0.tgz", + "integrity": "sha512-halby2+lQwHNxUDk70aQNXP1BlBwdwr7svTJZvDi7vKwrWbVMKhVrZ86h8p3zRcWbO4qAgLQ4ODAf8TgD3DhGQ==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.2.0", + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/googleapis-common": { + "version": "8.0.2-rc.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.2-rc.0.tgz", + "integrity": "sha512-JTcxRvmFa9Ec1uyfMEimEMeeKq1sHNZX3vn2qmoUMtnvixXXvcqTcbDZvEZXkEWpGlPlOf4joyep6/qs0BrLyg==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.0.0-rc.1", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/googleapis/node_modules/googleapis-common": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.0.tgz", + "integrity": "sha512-66if47It7y+Sab3HMkwEXx1kCq9qUC9px8ZXoj1CMrmLmUw81GpbnsNlXnlyZyGbGPGcj+tDD9XsZ23m7GLaJQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.1.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.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-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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-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-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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-util/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-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz", + "integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/dom-selector": "^6.5.4", + "cssstyle": "^5.3.0", + "data-urls": "^6.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", + "parse5": "^7.3.0", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0", + "ws": "^8.18.2", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/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==", + "dev": true, + "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/lazystream/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marked": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.0.tgz", + "integrity": "sha512-CTPAcRBq57cn3R8n3hwc2REddc28hjR7RzDXQ+lXLmMJYqn20BaI2cGw6QjgZGIgVfp2Wdfw4aMzgNteQ6qJgQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "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", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.78.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", + "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.25", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz", + "integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "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/pretty-format/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/process": { + "version": "0.11.10", + "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" + } + }, + "node_modules/process-nextick-args": { + "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/proto3-json-serializer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.3.tgz", + "integrity": "sha512-iUi7jGLuECChuoUwtvf6eXBDcFXTHAt5GM6ckvtD3RqD+j2wW0GW6WndPOu9IWeUk7n933lzrskcNMHJy2tFSw==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "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/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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" + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.5" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/retry-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.2.tgz", + "integrity": "sha512-JzFPAfklk1kjR1w76f0QOIhoDkNkSqW8wYKT08n9yysTmZfB+RQ2QoXoTAeOi1HD9ZipTyTAZg3c4pM/jeqgSw==", + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "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/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "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/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar-fs/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teeny-request": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.0.tgz", + "integrity": "sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/teeny-request/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==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/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==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/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==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/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", + "dev": true, + "license": "ISC", + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tldts": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.17" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", + "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "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" + }, + "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" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "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.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "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/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "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/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/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/workspace-mcp-server": { + "resolved": "workspace-mcp-server", + "link": true + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, + "workspace-mcp-server": { + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "esbuild": "^0.25.10" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..73a2b5ea --- /dev/null +++ b/package.json @@ -0,0 +1,74 @@ +{ + "name": "gemini-workspace-extension", + "version": "1.0.0", + "description": "Google Workspace MCP Server Extension", + "private": true, + "bin": { + "gemini-workspace-server": "workspace-mcp-server/dist/index.js" + }, + "workspaces": [ + "workspace-mcp-server" + ], + "scripts": { + "prepare": "npm run build", + "build": "npm run build --workspaces --if-present", + "test": "npm run test --workspaces --if-present", + "test:watch": "npm run test:watch --workspaces --if-present", + "test:coverage": "npm run test:coverage --workspaces --if-present", + "test:ci": "npm run test:ci --workspaces --if-present", + "start": "npm run start --workspaces --if-present", + "clear-auth": "npm run build:clear-auth -w workspace-mcp-server && node scripts/clear-auth.js", + "clean": "rm -rf release && npm run clean --workspaces --if-present", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "release": "node scripts/release.js" + }, + "dependencies": { + "@google-apps/chat": "^0.19.0", + "@google-cloud/local-auth": "^3.0.1", + "@googleapis/docs": "^5.0.1", + "@googleapis/drive": "^15.0.0", + "@modelcontextprotocol/sdk": "^1.17.5", + "dompurify": "^3.1.6", + "google-auth-library": "^10.4.0", + "googleapis": "^159.0.0", + "jsdom": "^27.0.0", + "keytar": "^7.9.0", + "marked": "^16.2.1" + }, + "devDependencies": { + "@jest/globals": "^30.0.5", + "@types/dompurify": "^3.0.5", + "@types/jest": "^30.0.0", + "@types/jsdom": "^21.1.7", + "@types/node": "^24.2.1", + "@typescript-eslint/eslint-plugin": "^8.44.0", + "@typescript-eslint/parser": "^8.44.0", + "@vercel/ncc": "^0.38.3", + "archiver": "^7.0.1", + "esbuild": "^0.25.9", + "eslint": "^9.36.0", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-license-header": "^0.8.0", + "jest": "^30.1.3", + "minimist": "^1.2.8", + "ts-jest": "^29.4.1", + "ts-node": "^10.9.2", + "typescript": "^5.9.2" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/google-gemini/gemini-cli-workspace.git" + }, + "keywords": [ + "mcp", + "google-workspace", + "gmail", + "google-docs", + "google-drive", + "google-calendar", + "google-chat" + ], + "author": "Allen Hutchison", + "license": "Apache-2.0" +} diff --git a/scripts/clear-auth.js b/scripts/clear-auth.js new file mode 100644 index 00000000..df55fbab --- /dev/null +++ b/scripts/clear-auth.js @@ -0,0 +1,19 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +const { OAuthCredentialStorage } = require('../workspace-mcp-server/dist/clear-auth.js'); + +async function clearAuth() { + try { + await OAuthCredentialStorage.clearCredentials(); + console.log('Authentication credentials cleared successfully.'); + } catch (error) { + console.error('Failed to clear authentication credentials:', error); + process.exit(1); + } +} + +clearAuth(); diff --git a/scripts/release.js b/scripts/release.js new file mode 100644 index 00000000..eb7ba489 --- /dev/null +++ b/scripts/release.js @@ -0,0 +1,125 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +const fs = require('node:fs'); +const path = require('node:path'); +const archiver = require('archiver'); +const argv = require('minimist')(process.argv.slice(2)); + +const deleteFilesByExtension = (dir, ext) => { + if (!fs.existsSync(dir)) { + return; + } + + const files = fs.readdirSync(dir); + for (const file of files) { + const filePath = path.join(dir, file); + const stat = fs.lstatSync(filePath); + if (stat.isDirectory()) { + deleteFilesByExtension(filePath, ext); + } else if (filePath.endsWith(ext)) { + fs.unlinkSync(filePath); + } + } +}; + +const main = async () => { + const name = 'workspace-mcp-server'; + const extension = 'tar.gz'; + + const rootDir = path.join(__dirname, '..'); + const releaseDir = path.join(rootDir, 'release'); + fs.rmSync(releaseDir, { recursive: true, force: true }); + const archiveName = `${name}.${extension}`; + const archiveDir = path.join(releaseDir, name); + const workspaceMcpServerDir = path.join(rootDir, 'workspace-mcp-server'); + + // Create the release directory + fs.mkdirSync(releaseDir, { recursive: true }); + + // Create the platform-specific directory + fs.mkdirSync(archiveDir, { recursive: true }); + + // Copy the dist directory + fs.cpSync( + path.join(workspaceMcpServerDir, 'dist'), + path.join(archiveDir, 'dist'), + { recursive: true } + ); + + // Clean up the dist directory + const distDir = path.join(archiveDir, 'dist'); + deleteFilesByExtension(distDir, '.d.ts'); + deleteFilesByExtension(distDir, '.map'); + fs.rmSync(path.join(distDir, '__tests__'), { recursive: true, force: true }); + fs.rmSync(path.join(distDir, 'auth'), { recursive: true, force: true }); + fs.rmSync(path.join(distDir, 'services'), { recursive: true, force: true }); + fs.rmSync(path.join(distDir, 'utils'), { recursive: true, force: true }); + + const version = process.env.GITHUB_REF_NAME || '0.0.1'; + + // Generate the gemini-extension.json file + const geminiExtensionJson = { + name: 'google-workspace', + version, + contextFileName: 'WORKSPACE-Context.md', + cwd: '${extensionPath}', + mcpServers: { + 'google-workspace': { + command: 'node', + args: ['dist/index.js'], + }, + }, + }; + fs.writeFileSync( + path.join(archiveDir, 'gemini-extension.json'), + JSON.stringify(geminiExtensionJson, null, 2) + ); + + // Copy the WORKSPACE-Context.md file + fs.copyFileSync( + path.join(workspaceMcpServerDir, 'WORKSPACE-Context.md'), + path.join(archiveDir, 'WORKSPACE-Context.md') + ); + + // Copy the config directory + fs.cpSync( + path.join(workspaceMcpServerDir, 'config'), + path.join(archiveDir, 'config'), + { recursive: true } + ); + + // Create the archive + const output = fs.createWriteStream(path.join(releaseDir, archiveName)); + const archive = archiver('tar', { + gzip: true, + }); + + const archivePromise = new Promise((resolve, reject) => { + output.on('close', function () { + console.log(archive.pointer() + ' total bytes'); + console.log( + 'archiver has been finalized and the output file descriptor has closed.' + ); + resolve(); + }); + + archive.on('error', function (err) { + reject(err); + }); + }); + + archive.pipe(output); + archive.directory(archiveDir, false); + archive.finalize(); + + await archivePromise; +}; + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/start.js b/scripts/start.js new file mode 100755 index 00000000..599b8455 --- /dev/null +++ b/scripts/start.js @@ -0,0 +1,45 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +const { spawn } = require('node:child_process'); +const path = require('node:path'); + +function runCommand(command, args, options) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, options); + + // Pipe stderr to the parent process's stderr if it's available. + // This is more efficient than listening for 'data' events. + if (child.stderr) { + child.stderr.pipe(process.stderr); + } + + child.on('close', (code) => { + if (code !== 0) { + reject(new Error(`Command failed with code ${code}: ${command} ${args.join(' ')}`)); + } else { + resolve(); + } + }); + child.on('error', (err) => { + reject(err); + }); + }); +} + +async function main() { + try { + await runCommand('npm', ['install'], { stdio: ['ignore', 'ignore', 'pipe'] }); + + const indexPath = path.join(__dirname, '..', 'workspace-mcp-server', 'dist', 'index.js'); + await runCommand('node', [indexPath, '--debug'], { stdio: 'inherit' }); + } catch (error) { + console.error(error); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..562192dc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": [ + "workspace-mcp-server/src/**/*" + ], + "exclude": [ + "node_modules", + "**/node_modules", + "**/dist", + "**/*.test.ts", + "**/*.spec.ts" + ] +} \ No newline at end of file diff --git a/workspace-mcp-server/.github/dependabot.yml b/workspace-mcp-server/.github/dependabot.yml new file mode 100644 index 00000000..2fb10cc0 --- /dev/null +++ b/workspace-mcp-server/.github/dependabot.yml @@ -0,0 +1,29 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/workspace-mcp-server" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + assignees: + - "google-gemini" + labels: + - "dependencies" + - "npm" + commit-message: + prefix: "chore" + include: "scope" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + assignees: + - "google-gemini" + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "chore" + include: "scope" \ No newline at end of file diff --git a/workspace-mcp-server/.github/workflows/ci.yml b/workspace-mcp-server/.github/workflows/ci.yml new file mode 100644 index 00000000..99adb155 --- /dev/null +++ b/workspace-mcp-server/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +name: CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: workspace-mcp-server/package-lock.json + + - name: Install dependencies + run: npm ci + working-directory: workspace-mcp-server + + - name: Run linter + run: npm run lint --if-present + working-directory: workspace-mcp-server + + - name: Run type checking + run: npx tsc --noEmit + working-directory: workspace-mcp-server + + - name: Run tests + run: npm test + working-directory: workspace-mcp-server + + - name: Generate coverage report + run: npm run test:coverage + working-directory: workspace-mcp-server + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + directory: ./workspace-mcp-server/coverage + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + build: + runs-on: ubuntu-latest + needs: test + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + cache-dependency-path: workspace-mcp-server/package-lock.json + + - name: Install dependencies + run: npm ci + working-directory: workspace-mcp-server + + - name: Build + run: npm run build + working-directory: workspace-mcp-server + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: workspace-mcp-server/dist/ + + security: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Run security audit + run: npm audit --audit-level=moderate + working-directory: workspace-mcp-server + continue-on-error: true + + - name: Check for known vulnerabilities + run: npx audit-ci --moderate + working-directory: workspace-mcp-server + continue-on-error: true \ No newline at end of file diff --git a/workspace-mcp-server/.github/workflows/release.yml b/workspace-mcp-server/.github/workflows/release.yml new file mode 100644 index 00000000..313e2d61 --- /dev/null +++ b/workspace-mcp-server/.github/workflows/release.yml @@ -0,0 +1,53 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + cache-dependency-path: workspace-mcp-server/package-lock.json + + - name: Install dependencies + run: npm ci + working-directory: workspace-mcp-server + + - name: Run tests + run: npm test + working-directory: workspace-mcp-server + + - name: Build + run: npm run build + working-directory: workspace-mcp-server + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./workspace-mcp-server/dist/index.js + asset_name: workspace-mcp-server.js + asset_content_type: application/javascript \ No newline at end of file diff --git a/workspace-mcp-server/WORKSPACE-Context.md b/workspace-mcp-server/WORKSPACE-Context.md new file mode 100644 index 00000000..88ba323a --- /dev/null +++ b/workspace-mcp-server/WORKSPACE-Context.md @@ -0,0 +1,223 @@ +# Google Workspace Extension - Behavioral Guide + +This guide provides behavioral instructions for effectively using the Google Workspace Extension tools. For detailed parameter documentation, refer to the tool descriptions in the extension itself. + +## 🎯 Core Principles + +### 1. User Context First +**Always establish user context at the beginning of interactions:** +- Use `people.getMe()` to understand who the user is +- Use `time.getTimeZone()` to get the user's local timezone +- Apply this context throughout all interactions +- All time-based operations should respect the user's timezone + +### 2. Safety and Transparency +**Never execute write operations without explicit confirmation:** +- Preview all changes before executing +- Show complete details in a readable format +- Wait for clear user approval +- Give users the opportunity to review and cancel + +### 3. Smart Tool Usage +**Choose the right approach for each task:** +- Tools automatically handle URL-to-ID conversion - don't extract IDs manually +- Batch related operations when possible +- Use pagination for large result sets +- Apply appropriate formats based on the use case + +## 📋 Output Formatting Standards + +### Lists and Search Results +Always format multiple items as **numbered lists** for better readability: + +✅ **Correct:** +``` +Found 3 documents: +1. Budget Report 2024 +2. Q3 Sales Presentation +3. Team Meeting Notes +``` + +❌ **Incorrect:** +``` +Found 3 documents: +- Budget Report 2024 +- Q3 Sales Presentation +- Team Meeting Notes +``` + +### Write Operation Previews +Before any write operation, show a clear preview: + +``` +I'll create this calendar event: + +Title: Team Standup +Date: January 15, 2025 +Time: 10:00 AM - 10:30 AM (EST) +Attendees: team@example.com + +Should I create this event? +``` + +## 🔄 Multi-Tool Workflows + +### Creating and Organizing Documents +When creating documents in specific folders: +1. Create the document first +2. Then move it to the folder (if specified) +3. Confirm successful completion + +### Calendar Scheduling Workflow +1. Get user's timezone with `time.getTimeZone()` +2. Check availability with `calendar.listEvents()` +3. Create event with proper timezone handling +4. Always show times in user's local timezone + +### Email Search and Response +1. Search with `gmail.search()` using appropriate query syntax +2. Get full content with `gmail.get()` if needed +3. Preview any reply before sending +4. Use threading context when responding + +### Adding/Removing Labels from Emails +1. For system labels, including "INBOX", "SPAM", "TRASH", "UNREAD", "STARRED", "IMPORTANT", the ID is the name itself. +2. For user created custom labels, retrieve label ID with `gmail.listLabels()`. +3. Use `gmail.modify()` to add or remove labels from emails with a single call using label IDs. + +## 📅 Calendar Best Practices + +### Understanding "Next Meeting" +When asked about "next meeting" or "today's schedule": +1. **Fetch the full day's context** - Use start of day (00:00:00) to end of day (23:59:59) +2. **Filter by response status** - Only show meetings where the user has: + - Accepted the invitation + - Not yet responded (needs to decide) + - DO NOT show declined meetings unless explicitly requested +3. **Compare with current time** - Identify meetings relative to now +4. **Handle edge cases**: + - If a meeting is in progress, mention it first + - "Next" means the first meeting after current time + - Keep full day context for follow-up questions + +### Meeting Response Filtering +- **Default behavior**: Show only accepted and pending meetings +- **Declined meetings**: Exclude unless user asks "show me all meetings" or "including declined" +- **Use `attendeeResponseStatus`** parameter to filter appropriately +- This respects the user's time by not cluttering their schedule with irrelevant meetings + +### Timezone Management +- Always display times in the user's timezone +- Convert all times appropriately before display +- Include timezone abbreviation (EST, PST, etc.) for clarity + +## 📧 Gmail & Chat Guidelines + +### Search Strategies +- Use Gmail search syntax: `from:email@example.com is:unread` +- Combine multiple criteria for precise results +- Include SPAM/TRASH only when explicitly needed + +### Threading and Context +- Maintain conversation context in replies +- Reference previous messages when relevant +- Use appropriate reply vs. new message based on context + +## 📄 Docs, Sheets, and Slides + +### Format Selection (Sheets) +Choose output format based on use case: +- **text**: Human-readable, good for quick review +- **csv**: Data export, analysis in other tools +- **json**: Programmatic processing, structured data + +### Content Handling +- Docs/Sheets/Slides tools accept URLs directly - no ID extraction needed +- Use markdown for initial document creation when appropriate +- Preserve formatting when reading/modifying content + +## 🚫 Common Pitfalls to Avoid + +### Don't Do This: +- ❌ Use `extractIdFromUrl` when other tools accept URLs +- ❌ Assume timezone without checking +- ❌ Execute writes without preview and confirmation +- ❌ Create files unless explicitly requested +- ❌ Duplicate parameter documentation from tool descriptions + +### Do This Instead: +- ✅ Pass URLs directly to tools that accept them +- ✅ Get user timezone at session start +- ✅ Preview all changes and wait for approval +- ✅ Only create what's requested +- ✅ Focus on behavioral guidance and best practices + +## 🔍 Error Handling Patterns + +### Graceful Degradation +- If a folder doesn't exist, offer to create it +- If search returns no results, suggest alternatives +- If permissions are insufficient, explain clearly + +### Validation Before Action +- Verify file/folder existence before moving +- Check calendar availability before scheduling +- Validate email addresses before sending + +## ⚡ Performance Optimization + +### Batch Operations +- Group related API calls when possible +- Use field masks to request only needed data +- Implement pagination for large datasets + +### Caching Strategy +- Reuse user context throughout session +- Cache frequently accessed metadata +- Minimize redundant API calls + +## 📝 Session Management + +### Beginning of Session +1. Get user profile with `people.getMe()` +2. Get timezone with `time.getTimeZone()` +3. Establish any relevant context + +### During Interaction +- Maintain context awareness +- Apply user preferences consistently +- Handle follow-up questions efficiently + +### End of Session +- Confirm all requested tasks completed +- Provide summary if multiple operations performed +- Ensure no pending confirmations + +## 🎨 Service-Specific Nuances + +### Google Docs +- Support for markdown content creation +- Automatic HTML conversion from markdown +- Position-based text insertion (index 1 for beginning) + +### Google Sheets +- Multiple output formats available +- Range-based operations with A1 notation +- Metadata includes sheet structure information + +### Google Calendar +- Event creation requires both start and end times +- Support for attendee management +- Response status filtering available + +### Gmail +- Full threading support +- Label-based organization +- Draft creation and management + +### Google Chat +- Space vs. DM distinction +- Thread-aware messaging +- Unread message filtering + +Remember: This guide focuses on **how to think** about using these tools effectively. For specific parameter details, refer to the tool descriptions themselves. diff --git a/workspace-mcp-server/esbuild.clear-auth.js b/workspace-mcp-server/esbuild.clear-auth.js new file mode 100644 index 00000000..9b2f7a24 --- /dev/null +++ b/workspace-mcp-server/esbuild.clear-auth.js @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +const esbuild = require('esbuild'); +const path = require('node:path'); + +async function buildClearAuth() { + try { + await esbuild.build({ + entryPoints: ['src/auth/token-storage/oauth-credential-storage.ts'], + bundle: true, + platform: 'node', + target: 'node20', + outfile: 'dist/clear-auth.js', + minify: true, + sourcemap: true, + external: [ + 'keytar', // keytar is a native module and should not be bundled + ], + format: 'cjs', + logLevel: 'info', + }); + + console.log('Clear Auth build completed successfully!'); + } catch (error) { + console.error('Clear Auth build failed:', error); + process.exit(1); + } +} + +buildClearAuth(); diff --git a/workspace-mcp-server/esbuild.config.js b/workspace-mcp-server/esbuild.config.js new file mode 100644 index 00000000..b9668a2e --- /dev/null +++ b/workspace-mcp-server/esbuild.config.js @@ -0,0 +1,45 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +const esbuild = require('esbuild'); +const path = require('node:path'); +const fs = require('node:fs'); + +async function build() { + try { + await esbuild.build({ + entryPoints: ['src/index.ts'], + bundle: true, + platform: 'node', + target: 'node16', + outfile: 'dist/index.js', + minify: true, + sourcemap: true, + // Replace 'open' package with our wrapper + alias: { + 'open': path.resolve(__dirname, 'src/utils/open-wrapper.ts') + }, + // External packages that shouldn't be bundled + external: [ + 'jsdom' + ], + // Add a loader for .node files + loader: { + '.node': 'file' + }, + // Make sure CommonJS modules work properly + format: 'cjs', + logLevel: 'info', + }); + + console.log('Build completed successfully!'); + } catch (error) { + console.error('Build failed:', error); + process.exit(1); + } +} + +build(); \ No newline at end of file diff --git a/workspace-mcp-server/jest.config.js b/workspace-mcp-server/jest.config.js new file mode 100644 index 00000000..11b6c29d --- /dev/null +++ b/workspace-mcp-server/jest.config.js @@ -0,0 +1,12 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** @type {import('jest').Config} */ +module.exports = { + // This workspace's tests are configured in the root jest.config.js + // as part of the 'projects' array. This file is kept for backwards + // compatibility and workspace-specific overrides if needed. +}; \ No newline at end of file diff --git a/workspace-mcp-server/package.json b/workspace-mcp-server/package.json new file mode 100644 index 00000000..214ca8f2 --- /dev/null +++ b/workspace-mcp-server/package.json @@ -0,0 +1,23 @@ +{ + "name": "workspace-mcp-server", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "scripts": { + "test": "cd .. && node --max-old-space-size=4096 node_modules/.bin/jest --runInBand --verbose", + "test:watch": "cd .. && jest --watch", + "test:coverage": "cd .. && node --max-old-space-size=4096 node_modules/.bin/jest --coverage", + "test:ci": "cd .. && node --max-old-space-size=4096 node_modules/.bin/jest --ci --coverage --maxWorkers=2", + "start": "ts-node src/index.ts", + "clean": "rm -rf dist", + "build": "node esbuild.config.js", + "build:clear-auth": "node esbuild.clear-auth.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "devDependencies": { + "esbuild": "^0.25.10" + } +} \ No newline at end of file diff --git a/workspace-mcp-server/src/__tests__/auth/token-storage/base-token-storage.test.ts b/workspace-mcp-server/src/__tests__/auth/token-storage/base-token-storage.test.ts new file mode 100644 index 00000000..ac6a1c11 --- /dev/null +++ b/workspace-mcp-server/src/__tests__/auth/token-storage/base-token-storage.test.ts @@ -0,0 +1,163 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach } from '@jest/globals'; +import { BaseTokenStorage } from '../../../auth/token-storage/base-token-storage'; +import type { OAuthCredentials, OAuthToken } from '../../../auth/token-storage/types'; + +class TestTokenStorage extends BaseTokenStorage { + private storage = new Map(); + + async getCredentials(serverName: string): Promise { + return this.storage.get(serverName) || null; + } + + async setCredentials(credentials: OAuthCredentials): Promise { + this.validateCredentials(credentials); + this.storage.set(credentials.serverName, credentials); + } + + async deleteCredentials(serverName: string): Promise { + this.storage.delete(serverName); + } + + async listServers(): Promise { + return Array.from(this.storage.keys()); + } + + async getAllCredentials(): Promise> { + return new Map(this.storage); + } + + async clearAll(): Promise { + this.storage.clear(); + } + + override validateCredentials(credentials: OAuthCredentials): void { + super.validateCredentials(credentials); + } + + + + override sanitizeServerName(serverName: string): string { + return super.sanitizeServerName(serverName); + } +} + +describe('BaseTokenStorage', () => { + let storage: TestTokenStorage; + + beforeEach(() => { + storage = new TestTokenStorage('gemini-cli-mcp-oauth'); + }); + + describe('validateCredentials', () => { + it('should validate valid credentials with access token', () => { + const credentials: OAuthCredentials = { + serverName: 'test-server', + token: { + accessToken: 'access-token', + tokenType: 'Bearer', + }, + updatedAt: Date.now(), + }; + + expect(() => storage.validateCredentials(credentials)).not.toThrow(); + }); + + it('should validate valid credentials with refresh token', () => { + const credentials: OAuthCredentials = { + serverName: 'test-server', + token: { + refreshToken: 'refresh-token', + tokenType: 'Bearer', + }, + updatedAt: Date.now(), + }; + + expect(() => storage.validateCredentials(credentials)).not.toThrow(); + }); + + it('should throw for missing server name', () => { + const credentials = { + serverName: '', + token: { + accessToken: 'access-token', + tokenType: 'Bearer', + }, + updatedAt: Date.now(), + } as OAuthCredentials; + + expect(() => storage.validateCredentials(credentials)).toThrow( + 'Server name is required', + ); + }); + + it('should throw for missing token', () => { + const credentials = { + serverName: 'test-server', + token: null as unknown as OAuthToken, + updatedAt: Date.now(), + } as OAuthCredentials; + + expect(() => storage.validateCredentials(credentials)).toThrow( + 'Token is required', + ); + }); + + it('should throw for missing access token and refresh token', () => { + const credentials = { + serverName: 'test-server', + token: { + accessToken: '', + tokenType: 'Bearer', + }, + updatedAt: Date.now(), + } as OAuthCredentials; + + expect(() => storage.validateCredentials(credentials)).toThrow( + 'Access token or refresh token is required', + ); + }); + + it('should throw for missing token type', () => { + const credentials = { + serverName: 'test-server', + token: { + accessToken: 'access-token', + tokenType: '', + }, + updatedAt: Date.now(), + } as OAuthCredentials; + + expect(() => storage.validateCredentials(credentials)).toThrow( + 'Token type is required', + ); + }); + }); + + + + describe('sanitizeServerName', () => { + it('should keep valid characters', () => { + expect(storage.sanitizeServerName('test-server.example_123')).toBe( + 'test-server.example_123', + ); + }); + + it('should replace invalid characters with underscore', () => { + expect(storage.sanitizeServerName('test@server#example')).toBe( + 'test_server_example', + ); + }); + + it('should handle special characters', () => { + expect(storage.sanitizeServerName('test server/example:123')).toBe( + 'test_server_example_123', + ); + }); + }); +}); \ No newline at end of file diff --git a/workspace-mcp-server/src/__tests__/auth/token-storage/file-token-storage.test.ts b/workspace-mcp-server/src/__tests__/auth/token-storage/file-token-storage.test.ts new file mode 100644 index 00000000..2947d02a --- /dev/null +++ b/workspace-mcp-server/src/__tests__/auth/token-storage/file-token-storage.test.ts @@ -0,0 +1,390 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + describe, + it, + expect, + beforeEach, + afterEach, + jest, +} from '@jest/globals'; +import * as crypto from 'node:crypto'; +import { promises as fs } from 'node:fs'; +import * as path from 'node:path'; +import { FileTokenStorage } from '../../../auth/token-storage/file-token-storage'; +import type { OAuthCredentials } from '../../../auth/token-storage/types'; +import { + ENCRYPTED_TOKEN_PATH, + ENCRYPTION_MASTER_KEY_PATH, +} from '../../../utils/paths'; + +jest.mock('node:fs', () => ({ + promises: { + readFile: jest.fn(), + writeFile: jest.fn(), + unlink: jest.fn(), + mkdir: jest.fn(), + }, +})); + +jest.mock('node:os', () => ({ + default: { + homedir: jest.fn(() => '/home/test'), + hostname: jest.fn(() => 'test-host'), + userInfo: jest.fn(() => ({ username: 'test-user' })), + }, + homedir: jest.fn(() => '/home/test'), + hostname: jest.fn(() => 'test-host'), + userInfo: jest.fn(() => ({ username: 'test-user' })), +})); + +describe('FileTokenStorage', () => { + let storage: FileTokenStorage; + const mockFs = fs as unknown as { + readFile: ReturnType; + writeFile: ReturnType; + unlink: ReturnType; + mkdir: ReturnType; + }; + + const existingCredentials: OAuthCredentials = { + serverName: 'existing-server', + token: { + accessToken: 'existing-token', + tokenType: 'Bearer', + }, + updatedAt: Date.now() - 10000, + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('when master key does not exist', () => { + it('should create a new master key', async () => { + const error = new Error('File not found'); + (error as NodeJS.ErrnoException).code = 'ENOENT'; + mockFs.readFile.mockRejectedValue(error); + storage = await FileTokenStorage.create('test-storage'); + + expect(mockFs.readFile).toHaveBeenCalledWith(ENCRYPTION_MASTER_KEY_PATH); + expect(mockFs.writeFile).toHaveBeenCalledWith( + ENCRYPTION_MASTER_KEY_PATH, + expect.any(Buffer), + { mode: 0o600 }, + ); + }); + }); + + describe('when master key exists', () => { + it('should load the master key without creating a new one', async () => { + const masterKey = crypto.randomBytes(32); + mockFs.readFile.mockResolvedValue(masterKey); + storage = await FileTokenStorage.create('test-storage'); + expect(mockFs.readFile).toHaveBeenCalledWith(ENCRYPTION_MASTER_KEY_PATH); + expect(mockFs.writeFile).not.toHaveBeenCalled(); + }); + }); + + describe('getCredentials', () => { + beforeEach(async () => { + // All tests assume a master key exists. + const masterKey = crypto.randomBytes(32); + mockFs.readFile.mockResolvedValue(masterKey); + storage = await FileTokenStorage.create('test-storage'); + }); + + it('should return null when file does not exist', async () => { + mockFs.readFile.mockRejectedValue({ code: 'ENOENT' }); + + const result = await storage.getCredentials('test-server'); + expect(result).toBeNull(); + }); + + it('should return credentials even if access token is expired', async () => { + const credentials: OAuthCredentials = { + serverName: 'test-server', + token: { + accessToken: 'access-token', + tokenType: 'Bearer', + expiresAt: Date.now() - 3600000, + }, + updatedAt: Date.now(), + }; + + const encryptedData = (storage as any).encrypt( + JSON.stringify({ 'test-server': credentials }), + ); + mockFs.readFile.mockResolvedValue(encryptedData); + + const result = await storage.getCredentials('test-server'); + expect(result).toEqual(credentials); + }); + + it('should return credentials for valid tokens', async () => { + const credentials: OAuthCredentials = { + serverName: 'test-server', + token: { + accessToken: 'access-token', + tokenType: 'Bearer', + expiresAt: Date.now() + 3600000, + }, + updatedAt: Date.now(), + }; + + const encryptedData = (storage as any).encrypt( + JSON.stringify({ 'test-server': credentials }), + ); + mockFs.readFile.mockResolvedValue(encryptedData); + + const result = await storage.getCredentials('test-server'); + expect(result).toEqual(credentials); + }); + + it('should return null for corrupted files', async () => { + mockFs.readFile.mockResolvedValue('corrupted-data'); + + const result = await storage.getCredentials('test-server'); + expect(result).toBeNull(); + }); + }); + + describe('setCredentials', () => { + beforeEach(async () => { + // All tests assume a master key exists. + const masterKey = crypto.randomBytes(32); + mockFs.readFile.mockResolvedValue(masterKey); + storage = await FileTokenStorage.create('test-storage'); + }); + it('should save credentials with encryption', async () => { + const encryptedData = (storage as any).encrypt( + JSON.stringify({ 'existing-server': existingCredentials }), + ); + mockFs.readFile.mockResolvedValue(encryptedData); + mockFs.mkdir.mockResolvedValue(undefined); + mockFs.writeFile.mockResolvedValue(undefined); + + const credentials: OAuthCredentials = { + serverName: 'test-server', + token: { + accessToken: 'access-token', + tokenType: 'Bearer', + }, + updatedAt: Date.now(), + }; + + await storage.setCredentials(credentials); + + expect(mockFs.mkdir).toHaveBeenCalledWith( + path.dirname(ENCRYPTED_TOKEN_PATH), + { recursive: true, mode: 0o700 }, + ); + expect(mockFs.writeFile).toHaveBeenCalled(); + + const writeCall = mockFs.writeFile.mock.calls[0]; + expect(writeCall[0]).toBe(ENCRYPTED_TOKEN_PATH); + expect(writeCall[1]).toMatch(/^[0-9a-f]+:[0-9a-f]+:[0-9a-f]+$/); + expect(writeCall[2]).toEqual({ mode: 0o600 }); + }); + + it('should update existing credentials', async () => { + const encryptedData = (storage as any).encrypt( + JSON.stringify({ 'existing-server': existingCredentials }), + ); + mockFs.readFile.mockResolvedValue(encryptedData); + mockFs.writeFile.mockResolvedValue(undefined); + + const newCredentials: OAuthCredentials = { + serverName: 'test-server', + token: { + accessToken: 'new-token', + tokenType: 'Bearer', + }, + updatedAt: Date.now(), + }; + + await storage.setCredentials(newCredentials); + + expect(mockFs.writeFile).toHaveBeenCalled(); + const writeCall = mockFs.writeFile.mock.calls[0]; + const decrypted = (storage as any).decrypt(writeCall[1]); + const saved = JSON.parse(decrypted); + + expect(saved['existing-server']).toEqual(existingCredentials); + expect(saved['test-server'].token.accessToken).toBe('new-token'); + }); + }); + + describe('deleteCredentials', () => { + beforeEach(async () => { + // All tests assume a master key exists. + const masterKey = crypto.randomBytes(32); + mockFs.readFile.mockResolvedValue(masterKey); + storage = await FileTokenStorage.create('test-storage'); + }); + it('should throw when credentials do not exist', async () => { + mockFs.readFile.mockRejectedValue({ code: 'ENOENT' }); + + await expect(storage.deleteCredentials('test-server')).rejects.toThrow( + 'No credentials found for test-server', + ); + }); + + it('should delete file when last credential is removed', async () => { + const credentials: OAuthCredentials = { + serverName: 'test-server', + token: { + accessToken: 'access-token', + tokenType: 'Bearer', + }, + updatedAt: Date.now(), + }; + + const encryptedData = (storage as any).encrypt( + JSON.stringify({ 'test-server': credentials }), + ); + mockFs.readFile.mockResolvedValue(encryptedData); + mockFs.unlink.mockResolvedValue(undefined); + + await storage.deleteCredentials('test-server'); + + expect(mockFs.unlink).toHaveBeenCalledWith(ENCRYPTED_TOKEN_PATH); + }); + + it('should update file when other credentials remain', async () => { + const credentials1: OAuthCredentials = { + serverName: 'server1', + token: { + accessToken: 'token1', + tokenType: 'Bearer', + }, + updatedAt: Date.now(), + }; + + const credentials2: OAuthCredentials = { + serverName: 'server2', + token: { + accessToken: 'token2', + tokenType: 'Bearer', + }, + updatedAt: Date.now(), + }; + + const encryptedData = (storage as any).encrypt( + JSON.stringify({ server1: credentials1, server2: credentials2 }), + ); + mockFs.readFile.mockResolvedValue(encryptedData); + mockFs.writeFile.mockResolvedValue(undefined); + + await storage.deleteCredentials('server1'); + + expect(mockFs.writeFile).toHaveBeenCalled(); + expect(mockFs.unlink).not.toHaveBeenCalled(); + + const writeCall = mockFs.writeFile.mock.calls[0]; + const decrypted = (storage as any).decrypt(writeCall[1]); + const saved = JSON.parse(decrypted); + + expect(saved['server1']).toBeUndefined(); + expect(saved['server2']).toEqual(credentials2); + }); + }); + + describe('listServers', () => { + beforeEach(async () => { + // All tests assume a master key exists. + const masterKey = crypto.randomBytes(32); + mockFs.readFile.mockResolvedValue(masterKey); + storage = await FileTokenStorage.create('test-storage'); + }); + it('should return empty list when file does not exist', async () => { + mockFs.readFile.mockRejectedValue({ code: 'ENOENT' }); + + const result = await storage.listServers(); + expect(result).toEqual([]); + }); + + it('should return list of server names', async () => { + const credentials: Record = { + server1: { + serverName: 'server1', + token: { accessToken: 'token1', tokenType: 'Bearer' }, + updatedAt: Date.now(), + }, + server2: { + serverName: 'server2', + token: { accessToken: 'token2', tokenType: 'Bearer' }, + updatedAt: Date.now(), + }, + }; + + const encryptedData = (storage as any).encrypt( + JSON.stringify(credentials), + ); + mockFs.readFile.mockResolvedValue(encryptedData); + + const result = await storage.listServers(); + expect(result).toEqual(['server1', 'server2']); + }); + }); + + describe('clearAll', () => { + beforeEach(async () => { + // All tests assume a master key exists. + const masterKey = crypto.randomBytes(32); + mockFs.readFile.mockResolvedValue(masterKey); + storage = await FileTokenStorage.create('test-storage'); + }); + it('should delete the token file', async () => { + mockFs.unlink.mockResolvedValue(undefined); + + await storage.clearAll(); + + expect(mockFs.unlink).toHaveBeenCalledWith(ENCRYPTED_TOKEN_PATH); + }); + + it('should not throw when file does not exist', async () => { + mockFs.unlink.mockRejectedValue({ code: 'ENOENT' }); + + await expect(storage.clearAll()).resolves.not.toThrow(); + }); + }); + + describe('encryption', () => { + beforeEach(async () => { + // All tests assume a master key exists. + const masterKey = crypto.randomBytes(32); + mockFs.readFile.mockResolvedValue(masterKey); + storage = await FileTokenStorage.create('test-storage'); + }); + it('should encrypt and decrypt data correctly', () => { + const original = 'test-data-123'; + const encrypted = (storage as any).encrypt(original); + const decrypted = (storage as any).decrypt(encrypted); + + expect(decrypted).toBe(original); + expect(encrypted).not.toBe(original); + expect(encrypted).toMatch(/^[0-9a-f]+:[0-9a-f]+:[0-9a-f]+$/); + }); + + it('should produce different encrypted output each time', () => { + const original = 'test-data'; + const encrypted1 = (storage as any).encrypt(original); + const encrypted2 = (storage as any).encrypt(original); + + expect(encrypted1).not.toBe(encrypted2); + expect((storage as any).decrypt(encrypted1)).toBe(original); + expect((storage as any).decrypt(encrypted2)).toBe(original); + }); + + it('should throw on invalid encrypted data format', () => { + expect(() => (storage as any).decrypt('invalid-data')).toThrow( + 'Invalid encrypted data format', + ); + }); + }); +}); \ No newline at end of file diff --git a/workspace-mcp-server/src/__tests__/auth/token-storage/hybrid-token-storage.test.ts b/workspace-mcp-server/src/__tests__/auth/token-storage/hybrid-token-storage.test.ts new file mode 100644 index 00000000..036b51ae --- /dev/null +++ b/workspace-mcp-server/src/__tests__/auth/token-storage/hybrid-token-storage.test.ts @@ -0,0 +1,261 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; +import { type OAuthCredentials, TokenStorageType } from '../../../auth/token-storage/types'; + +// Mock paths +const KEYCHAIN_TOKEN_STORAGE_PATH = '../../../auth/token-storage/keychain-token-storage'; +const FILE_TOKEN_STORAGE_PATH = '../../../auth/token-storage/file-token-storage'; +const HYBRID_TOKEN_STORAGE_PATH = '../../../auth/token-storage/hybrid-token-storage'; + +interface MockStorage { + isAvailable?: ReturnType; + getCredentials: ReturnType; + setCredentials: ReturnType; + deleteCredentials: ReturnType; + listServers: ReturnType; + getAllCredentials: ReturnType; + clearAll: ReturnType; +} + +describe('HybridTokenStorage', () => { + let HybridTokenStorage: typeof import('../../../auth/token-storage/hybrid-token-storage').HybridTokenStorage; + let storage: import('../../../auth/token-storage/hybrid-token-storage').HybridTokenStorage; + let mockKeychainStorage: MockStorage; + let mockFileStorage: MockStorage; + const originalEnv = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; + process.env['GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE'] = 'false'; + + mockKeychainStorage = { + isAvailable: jest.fn(), + getCredentials: jest.fn(), + setCredentials: jest.fn(), + deleteCredentials: jest.fn(), + listServers: jest.fn(), + getAllCredentials: jest.fn(), + clearAll: jest.fn(), + }; + + mockFileStorage = { + getCredentials: jest.fn(), + setCredentials: jest.fn(), + deleteCredentials: jest.fn(), + listServers: jest.fn(), + getAllCredentials: jest.fn(), + clearAll: jest.fn(), + }; + + jest.doMock(KEYCHAIN_TOKEN_STORAGE_PATH, () => ({ + KeychainTokenStorage: jest.fn().mockImplementation(() => mockKeychainStorage), + })); + + jest.mock(FILE_TOKEN_STORAGE_PATH, () => ({ + FileTokenStorage: { + create: jest.fn().mockImplementation(() => { + return Promise.resolve(mockFileStorage); + }), + }, + })); + + // eslint-disable-next-line @typescript-eslint/no-require-imports + HybridTokenStorage = require(HYBRID_TOKEN_STORAGE_PATH).HybridTokenStorage; + storage = new HybridTokenStorage('test-service'); + }); + + afterEach(() => { + process.env = originalEnv; + }); + + describe('storage selection', () => { + it('should use keychain when available', async () => { + mockKeychainStorage.isAvailable!.mockResolvedValue(true); + mockKeychainStorage.getCredentials.mockResolvedValue(null); + + await storage.getCredentials('test-server'); + + expect(mockKeychainStorage.isAvailable).toHaveBeenCalled(); + expect(mockKeychainStorage.getCredentials).toHaveBeenCalledWith( + 'test-server', + ); + expect(await storage.getStorageType()).toBe(TokenStorageType.KEYCHAIN); + }); + + it('should use file storage when GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE is set', async () => { + process.env['GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE'] = 'true'; + mockFileStorage.getCredentials.mockResolvedValue(null); + + await storage.getCredentials('test-server'); + + expect(mockKeychainStorage.isAvailable).not.toHaveBeenCalled(); + expect(mockFileStorage.getCredentials).toHaveBeenCalledWith( + 'test-server', + ); + expect(await storage.getStorageType()).toBe( + TokenStorageType.ENCRYPTED_FILE, + ); + }); + + it('should fall back to file storage when keychain is unavailable', async () => { + mockKeychainStorage.isAvailable!.mockResolvedValue(false); + mockFileStorage.getCredentials.mockResolvedValue(null); + + await storage.getCredentials('test-server'); + + expect(mockKeychainStorage.isAvailable).toHaveBeenCalled(); + expect(mockFileStorage.getCredentials).toHaveBeenCalledWith( + 'test-server', + ); + expect(await storage.getStorageType()).toBe( + TokenStorageType.ENCRYPTED_FILE, + ); + }); + + it('should fall back to file storage when keychain throws error', async () => { + mockKeychainStorage.isAvailable!.mockRejectedValue( + new Error('Keychain error'), + ); + mockFileStorage.getCredentials.mockResolvedValue(null); + + await storage.getCredentials('test-server'); + + expect(mockKeychainStorage.isAvailable).toHaveBeenCalled(); + expect(mockFileStorage.getCredentials).toHaveBeenCalledWith( + 'test-server', + ); + expect(await storage.getStorageType()).toBe( + TokenStorageType.ENCRYPTED_FILE, + ); + }); + + it('should cache storage selection', async () => { + mockKeychainStorage.isAvailable!.mockResolvedValue(true); + mockKeychainStorage.getCredentials.mockResolvedValue(null); + + await storage.getCredentials('test-server'); + await storage.getCredentials('another-server'); + + expect(mockKeychainStorage.isAvailable).toHaveBeenCalledTimes(1); + }); + }); + + describe('getCredentials', () => { + it('should delegate to selected storage', async () => { + const credentials: OAuthCredentials = { + serverName: 'test-server', + token: { + accessToken: 'access-token', + tokenType: 'Bearer', + }, + updatedAt: Date.now(), + }; + + mockKeychainStorage.isAvailable!.mockResolvedValue(true); + mockKeychainStorage.getCredentials.mockResolvedValue(credentials); + + const result = await storage.getCredentials('test-server'); + + expect(result).toEqual(credentials); + expect(mockKeychainStorage.getCredentials).toHaveBeenCalledWith( + 'test-server', + ); + }); + }); + + describe('setCredentials', () => { + it('should delegate to selected storage', async () => { + const credentials: OAuthCredentials = { + serverName: 'test-server', + token: { + accessToken: 'access-token', + tokenType: 'Bearer', + }, + updatedAt: Date.now(), + }; + + mockKeychainStorage.isAvailable!.mockResolvedValue(true); + mockKeychainStorage.setCredentials.mockResolvedValue(undefined); + + await storage.setCredentials(credentials); + + expect(mockKeychainStorage.setCredentials).toHaveBeenCalledWith( + credentials, + ); + }); + }); + + describe('deleteCredentials', () => { + it('should delegate to selected storage', async () => { + mockKeychainStorage.isAvailable!.mockResolvedValue(true); + mockKeychainStorage.deleteCredentials.mockResolvedValue(undefined); + + await storage.deleteCredentials('test-server'); + + expect(mockKeychainStorage.deleteCredentials).toHaveBeenCalledWith( + 'test-server', + ); + }); + }); + + describe('listServers', () => { + it('should delegate to selected storage', async () => { + const servers = ['server1', 'server2']; + mockKeychainStorage.isAvailable!.mockResolvedValue(true); + mockKeychainStorage.listServers.mockResolvedValue(servers); + + const result = await storage.listServers(); + + expect(result).toEqual(servers); + expect(mockKeychainStorage.listServers).toHaveBeenCalled(); + }); + }); + + describe('getAllCredentials', () => { + it('should delegate to selected storage', async () => { + const credentialsMap = new Map([ + [ + 'server1', + { + serverName: 'server1', + token: { accessToken: 'token1', tokenType: 'Bearer' }, + updatedAt: Date.now(), + }, + ], + [ + 'server2', + { + serverName: 'server2', + token: { accessToken: 'token2', tokenType: 'Bearer' }, + updatedAt: Date.now(), + }, + ], + ]); + + mockKeychainStorage.isAvailable!.mockResolvedValue(true); + mockKeychainStorage.getAllCredentials.mockResolvedValue(credentialsMap); + + const result = await storage.getAllCredentials(); + + expect(result).toEqual(credentialsMap); + expect(mockKeychainStorage.getAllCredentials).toHaveBeenCalled(); + }); + }); + + describe('clearAll', () => { + it('should delegate to selected storage', async () => { + mockKeychainStorage.isAvailable!.mockResolvedValue(true); + mockKeychainStorage.clearAll.mockResolvedValue(undefined); + + await storage.clearAll(); + + expect(mockKeychainStorage.clearAll).toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file diff --git a/workspace-mcp-server/src/__tests__/auth/token-storage/keychain-token-storage.test.ts b/workspace-mcp-server/src/__tests__/auth/token-storage/keychain-token-storage.test.ts new file mode 100644 index 00000000..300284df --- /dev/null +++ b/workspace-mcp-server/src/__tests__/auth/token-storage/keychain-token-storage.test.ts @@ -0,0 +1,352 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; +import type { KeychainTokenStorage } from '../../../auth/token-storage/keychain-token-storage'; +import type { OAuthCredentials } from '../../../auth/token-storage/types'; +import type keytar from 'keytar'; + +// Mock the entire keytar module. +jest.mock('keytar'); + +// We will get a reference to the mock inside `beforeEach`. +let mockKeytar: jest.Mocked; + +const mockServiceName = 'service-name'; +const mockCryptoRandomBytesString = 'random-string'; + +jest.mock('node:crypto', () => ({ + randomBytes: jest.fn(() => ({ + toString: jest.fn(() => mockCryptoRandomBytesString), + })), +})); + +describe('KeychainTokenStorage', () => { + let storage: KeychainTokenStorage; + + beforeEach(async () => { + jest.resetAllMocks(); + // Reset modules to ensure a clean state for each test. + jest.resetModules(); + + // Dynamically import the mocked keytar and cast it to our mocked type. + // This MUST be done after resetting modules. + mockKeytar = (await import('keytar')).default as jest.Mocked; + + // Now import the module we are testing, which will use the mock above. + const { KeychainTokenStorage } = await import( + '../../../auth/token-storage/keychain-token-storage' + ); + storage = new KeychainTokenStorage(mockServiceName); + }); + + afterEach(() => { + jest.restoreAllMocks(); + jest.useRealTimers(); + }); + + const validCredentials = { + serverName: 'test-server', + token: { + accessToken: 'access-token', + tokenType: 'Bearer', + expiresAt: Date.now() + 3600000, + }, + updatedAt: Date.now(), + } as OAuthCredentials; + + describe('checkKeychainAvailability', () => { + it('should return true if keytar is available and functional', async () => { + mockKeytar.setPassword.mockResolvedValue(undefined); + mockKeytar.getPassword.mockResolvedValue('test'); + mockKeytar.deletePassword.mockResolvedValue(true); + + const isAvailable = await storage.checkKeychainAvailability(); + expect(isAvailable).toBe(true); + expect(mockKeytar.setPassword).toHaveBeenCalledWith( + mockServiceName, + `__keychain_test__${mockCryptoRandomBytesString}`, + 'test', + ); + expect(mockKeytar.getPassword).toHaveBeenCalledWith( + mockServiceName, + `__keychain_test__${mockCryptoRandomBytesString}`, + ); + expect(mockKeytar.deletePassword).toHaveBeenCalledWith( + mockServiceName, + `__keychain_test__${mockCryptoRandomBytesString}`, + ); + }); + + it('should return false if keytar fails to set password', async () => { + mockKeytar.setPassword.mockRejectedValue(new Error('write error')); + const isAvailable = await storage.checkKeychainAvailability(); + expect(isAvailable).toBe(false); + }); + + it('should return false if retrieved password does not match', async () => { + mockKeytar.setPassword.mockResolvedValue(undefined); + mockKeytar.getPassword.mockResolvedValue('wrong-password'); + mockKeytar.deletePassword.mockResolvedValue(true); + const isAvailable = await storage.checkKeychainAvailability(); + expect(isAvailable).toBe(false); + }); + + it('should cache the availability result', async () => { + mockKeytar.setPassword.mockResolvedValue(undefined); + mockKeytar.getPassword.mockResolvedValue('test'); + mockKeytar.deletePassword.mockResolvedValue(true); + + await storage.checkKeychainAvailability(); + await storage.checkKeychainAvailability(); + + expect(mockKeytar.setPassword).toHaveBeenCalledTimes(1); + }); + }); + + describe('with keychain unavailable', () => { + beforeEach(async () => { + // Force keychain to be unavailable + mockKeytar.setPassword.mockRejectedValue(new Error('keychain error')); + await storage.checkKeychainAvailability(); + }); + + it('getCredentials should throw', async () => { + await expect(storage.getCredentials('server')).rejects.toThrow( + 'Keychain is not available', + ); + }); + + it('setCredentials should throw', async () => { + await expect(storage.setCredentials(validCredentials)).rejects.toThrow( + 'Keychain is not available', + ); + }); + + it('deleteCredentials should throw', async () => { + await expect(storage.deleteCredentials('server')).rejects.toThrow( + 'Keychain is not available', + ); + }); + + it('listServers should throw', async () => { + await expect(storage.listServers()).rejects.toThrow( + 'Keychain is not available', + ); + }); + + it('getAllCredentials should throw', async () => { + await expect(storage.getAllCredentials()).rejects.toThrow( + 'Keychain is not available', + ); + }); + }); + + describe('with keychain available', () => { + beforeEach(async () => { + mockKeytar.setPassword.mockResolvedValue(undefined); + mockKeytar.getPassword.mockResolvedValue('test'); + mockKeytar.deletePassword.mockResolvedValue(true); + await storage.checkKeychainAvailability(); + // Reset mocks after availability check + jest.resetAllMocks(); + }); + + describe('getCredentials', () => { + it('should return null if no credentials are found', async () => { + mockKeytar.getPassword.mockResolvedValue(null); + const result = await storage.getCredentials('test-server'); + expect(result).toBeNull(); + expect(mockKeytar.getPassword).toHaveBeenCalledWith( + mockServiceName, + 'test-server', + ); + }); + + it('should return credentials if found and not expired', async () => { + mockKeytar.getPassword.mockResolvedValue( + JSON.stringify(validCredentials), + ); + const result = await storage.getCredentials('test-server'); + expect(result).toEqual(validCredentials); + }); + + it('should return credentials even if access token is expired', async () => { + const expiredCreds = { + ...validCredentials, + token: { ...validCredentials.token, expiresAt: Date.now() - 1000 }, + }; + mockKeytar.getPassword.mockResolvedValue(JSON.stringify(expiredCreds)); + const result = await storage.getCredentials('test-server'); + expect(result).toEqual(expiredCreds); + }); + + it('should throw if stored data is corrupted JSON', async () => { + mockKeytar.getPassword.mockResolvedValue('not-json'); + await expect(storage.getCredentials('test-server')).rejects.toThrow( + 'Failed to parse stored credentials for test-server', + ); + }); + }); + + describe('setCredentials', () => { + it('should save credentials to keychain', async () => { + jest.useFakeTimers(); + mockKeytar.setPassword.mockResolvedValue(undefined); + await storage.setCredentials(validCredentials); + expect(mockKeytar.setPassword).toHaveBeenCalledWith( + mockServiceName, + 'test-server', + JSON.stringify({ ...validCredentials, updatedAt: Date.now() }), + ); + }); + + it('should throw if saving to keychain fails', async () => { + mockKeytar.setPassword.mockRejectedValue( + new Error('keychain write error'), + ); + await expect(storage.setCredentials(validCredentials)).rejects.toThrow( + 'keychain write error', + ); + }); + }); + + describe('deleteCredentials', () => { + it('should delete credentials from keychain', async () => { + mockKeytar.deletePassword.mockResolvedValue(true); + await storage.deleteCredentials('test-server'); + expect(mockKeytar.deletePassword).toHaveBeenCalledWith( + mockServiceName, + 'test-server', + ); + }); + + it('should throw if no credentials were found to delete', async () => { + mockKeytar.deletePassword.mockResolvedValue(false); + await expect(storage.deleteCredentials('test-server')).rejects.toThrow( + 'No credentials found for test-server', + ); + }); + + it('should throw if deleting from keychain fails', async () => { + mockKeytar.deletePassword.mockRejectedValue( + new Error('keychain delete error'), + ); + await expect(storage.deleteCredentials('test-server')).rejects.toThrow( + 'keychain delete error', + ); + }); + }); + + describe('listServers', () => { + it('should return a list of server names', async () => { + mockKeytar.findCredentials.mockResolvedValue([ + { account: 'server1', password: '' }, + { account: 'server2', password: '' }, + ]); + const result = await storage.listServers(); + expect(result).toEqual(['server1', 'server2']); + }); + + it('should not include internal test keys in the server list', async () => { + mockKeytar.findCredentials.mockResolvedValue([ + { account: 'server1', password: '' }, + { + account: `__keychain_test__${mockCryptoRandomBytesString}`, + password: '', + }, + { account: 'server2', password: '' }, + ]); + const result = await storage.listServers(); + expect(result).toEqual(['server1', 'server2']); + }); + + it('should return an empty array on error', async () => { + mockKeytar.findCredentials.mockRejectedValue(new Error('find error')); + const result = await storage.listServers(); + expect(result).toEqual([]); + }); + }); + + describe('getAllCredentials', () => { + it('should return a map of all valid credentials', async () => { + const creds2 = { + ...validCredentials, + serverName: 'server2', + }; + const expiredCreds = { + ...validCredentials, + serverName: 'expired-server', + token: { ...validCredentials.token, expiresAt: Date.now() - 1000 }, + }; + const structurallyInvalidCreds = { + serverName: 'invalid-server', + }; + + mockKeytar.findCredentials.mockResolvedValue([ + { + account: 'test-server', + password: JSON.stringify(validCredentials), + }, + { account: 'server2', password: JSON.stringify(creds2) }, + { + account: 'expired-server', + password: JSON.stringify(expiredCreds), + }, + { account: 'bad-server', password: 'not-json' }, + { + account: 'invalid-server', + password: JSON.stringify(structurallyInvalidCreds), + }, + ]); + + const result = await storage.getAllCredentials(); + expect(result.size).toBe(3); + expect(result.get('test-server')).toEqual(validCredentials); + expect(result.get('server2')).toEqual(creds2); + expect(result.get('expired-server')).toEqual(expiredCreds); + expect(result.has('bad-server')).toBe(false); + expect(result.has('invalid-server')).toBe(false); + }); + }); + + describe('clearAll', () => { + it('should delete all credentials for the service', async () => { + mockKeytar.findCredentials.mockResolvedValue([ + { account: 'server1', password: '' }, + { account: 'server2', password: '' }, + ]); + mockKeytar.deletePassword.mockResolvedValue(true); + + await storage.clearAll(); + + expect(mockKeytar.deletePassword).toHaveBeenCalledTimes(2); + expect(mockKeytar.deletePassword).toHaveBeenCalledWith( + mockServiceName, + 'server1', + ); + expect(mockKeytar.deletePassword).toHaveBeenCalledWith( + mockServiceName, + 'server2', + ); + }); + + it('should throw an aggregated error if deletions fail', async () => { + mockKeytar.findCredentials.mockResolvedValue([ + { account: 'server1', password: '' }, + { account: 'server2', password: '' }, + ]); + mockKeytar.deletePassword + .mockResolvedValueOnce(true) + .mockRejectedValueOnce(new Error('delete failed')); + + await expect(storage.clearAll()).rejects.toThrow( + 'Failed to clear some credentials: delete failed', + ); + }); + }); + }); +}); \ No newline at end of file diff --git a/workspace-mcp-server/src/__tests__/auth/token-storage/oauth-credential-storage.test.ts b/workspace-mcp-server/src/__tests__/auth/token-storage/oauth-credential-storage.test.ts new file mode 100644 index 00000000..fb106c29 --- /dev/null +++ b/workspace-mcp-server/src/__tests__/auth/token-storage/oauth-credential-storage.test.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach, jest } from '@jest/globals'; +import { OAuthCredentialStorage } from '../../../auth/token-storage/oauth-credential-storage'; +import { HybridTokenStorage } from '../../../auth/token-storage/hybrid-token-storage'; +import { type Credentials } from 'google-auth-library'; +import { type OAuthCredentials } from '../../../auth/token-storage/types'; + +// Mock the HybridTokenStorage dependency +jest.mock('../../../auth/token-storage/hybrid-token-storage'); + +describe('OAuthCredentialStorage', () => { + const mockGoogleCredentials: Credentials = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + expiry_date: 1234567890, + token_type: 'Bearer', + scope: 'test-scope', + }; + + const mockMcpCredentials: OAuthCredentials = { + serverName: 'main-account', + token: { + accessToken: 'test-access-token', + refreshToken: 'test-refresh-token', + expiresAt: 1234567890, + tokenType: 'Bearer', + scope: 'test-scope', + }, + updatedAt: expect.any(Number) as any, + }; + + let getCredentialsMock: any; + let setCredentialsMock: any; + let deleteCredentialsMock: any; + + beforeEach(() => { + jest.clearAllMocks(); + + getCredentialsMock = jest + .spyOn(HybridTokenStorage.prototype, 'getCredentials') + .mockResolvedValue(null); + setCredentialsMock = jest + .spyOn(HybridTokenStorage.prototype, 'setCredentials') + .mockResolvedValue(undefined); + deleteCredentialsMock = jest + .spyOn(HybridTokenStorage.prototype, 'deleteCredentials') + .mockResolvedValue(undefined); + }); + + describe('loadCredentials', () => { + it('should load credentials from HybridTokenStorage if available', async () => { + getCredentialsMock.mockResolvedValue(mockMcpCredentials); + + const credentials = await OAuthCredentialStorage.loadCredentials(); + + expect(getCredentialsMock).toHaveBeenCalledWith('main-account'); + expect(credentials).toEqual(mockGoogleCredentials); + }); + + it('should return null if no credentials found', async () => { + getCredentialsMock.mockResolvedValue(null); + + const credentials = await OAuthCredentialStorage.loadCredentials(); + + expect(getCredentialsMock).toHaveBeenCalledWith('main-account'); + expect(credentials).toBeNull(); + }); + + it('should throw an error if loading fails', async () => { + getCredentialsMock.mockRejectedValue(new Error('Storage error')); + + await expect(OAuthCredentialStorage.loadCredentials()).rejects.toThrow( + 'Storage error', + ); + }); + }); + + describe('saveCredentials', () => { + it('should save credentials to HybridTokenStorage', async () => { + setCredentialsMock.mockResolvedValue(undefined); + + await OAuthCredentialStorage.saveCredentials(mockGoogleCredentials); + + expect(setCredentialsMock).toHaveBeenCalledWith(mockMcpCredentials); + }); + }); + + describe('clearCredentials', () => { + it('should delete credentials from HybridTokenStorage', async () => { + deleteCredentialsMock.mockResolvedValue(undefined); + + await OAuthCredentialStorage.clearCredentials(); + + expect(deleteCredentialsMock).toHaveBeenCalledWith('main-account'); + }); + + it('should throw an error if clearing from HybridTokenStorage fails', async () => { + deleteCredentialsMock.mockRejectedValue(new Error('Clear error')); + + await expect(OAuthCredentialStorage.clearCredentials()).rejects.toThrow( + 'Clear error', + ); + }); + }); +}); \ No newline at end of file diff --git a/workspace-mcp-server/src/__tests__/mocks/marked.js b/workspace-mcp-server/src/__tests__/mocks/marked.js new file mode 100644 index 00000000..e0eaaf30 --- /dev/null +++ b/workspace-mcp-server/src/__tests__/mocks/marked.js @@ -0,0 +1,56 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +const markedMock = jest.fn((text) => { + // Simple mock implementation that returns HTML + return `

${text}

`; +}); + +// Add parse method to the marked function +markedMock.parse = jest.fn((text) => { + // Return a promise that resolves to HTML + return Promise.resolve(`

${text}

`); +}); + +markedMock.parseInline = jest.fn((text) => { + // Simple markdown to HTML conversion for testing + return text + .replace(/\*\*(.*?)\*\*/g, '$1') // **bold** -> bold + .replace(/\*(.*?)\*/g, '$1') // *italic* -> italic + .replace(/_(.*?)_/g, '$1') // _italic_ -> italic + .replace(/`(.*?)`/g, '$1'); // `code` -> code +}); +markedMock.use = jest.fn(); +markedMock.setOptions = jest.fn(); +markedMock.getDefaults = jest.fn(); +markedMock.defaults = {}; +markedMock.Renderer = jest.fn(); +markedMock.TextRenderer = jest.fn(); +markedMock.Lexer = jest.fn(); +markedMock.Parser = jest.fn(); +markedMock.Tokenizer = jest.fn(); +markedMock.Slugger = jest.fn(); +markedMock.lexer = jest.fn(); +markedMock.parser = jest.fn(); + +module.exports = { + marked: markedMock, + Marked: jest.fn(), + lexer: markedMock.lexer, + parser: markedMock.parser, + Renderer: markedMock.Renderer, + TextRenderer: markedMock.TextRenderer, + Lexer: markedMock.Lexer, + Parser: markedMock.Parser, + Tokenizer: markedMock.Tokenizer, + Slugger: markedMock.Slugger, + parse: markedMock.parse, + parseInline: markedMock.parseInline, + use: markedMock.use, + setOptions: markedMock.setOptions, + getDefaults: markedMock.getDefaults, + defaults: markedMock.defaults, +}; \ No newline at end of file diff --git a/workspace-mcp-server/src/__tests__/mocks/wasm.js b/workspace-mcp-server/src/__tests__/mocks/wasm.js new file mode 100644 index 00000000..a275406c --- /dev/null +++ b/workspace-mcp-server/src/__tests__/mocks/wasm.js @@ -0,0 +1,7 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +module.exports = {}; \ No newline at end of file diff --git a/workspace-mcp-server/src/__tests__/services/CalendarService.test.ts b/workspace-mcp-server/src/__tests__/services/CalendarService.test.ts new file mode 100644 index 00000000..c7771842 --- /dev/null +++ b/workspace-mcp-server/src/__tests__/services/CalendarService.test.ts @@ -0,0 +1,1029 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +import { CalendarService } from '../../services/CalendarService'; +import { google } from 'googleapis'; + +// Mock the googleapis module +jest.mock('googleapis'); +jest.mock('../../utils/logger'); + +describe('CalendarService', () => { + let calendarService: CalendarService; + let mockAuthManager: any; + let mockCalendarAPI: any; + + beforeEach(() => { + // Clear all mocks before each test + jest.clearAllMocks(); + + // Create mock AuthManager + mockAuthManager = { + getAuthenticatedClient: jest.fn(), + }; + + // Create mock Calendar API + mockCalendarAPI = { + calendarList: { + list: jest.fn(), + }, + events: { + list: jest.fn(), + insert: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + get: jest.fn(), + patch: jest.fn(), + }, + freebusy: { + query: jest.fn(), + }, + }; + + // Mock the google.calendar constructor + (google.calendar as jest.Mock) = jest.fn().mockReturnValue(mockCalendarAPI); + + // Create CalendarService instance + calendarService = new CalendarService(mockAuthManager); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('initialize', () => { + it('should initialize the Calendar API client', async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient); + + await calendarService.initialize(); + + expect(mockAuthManager.getAuthenticatedClient).toHaveBeenCalledTimes(1); + expect(google.calendar).toHaveBeenCalledWith( + expect.objectContaining({ + version: 'v3', + auth: mockAuthClient, + }) + ); + }); + }); + + describe('listCalendars', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient); + }); + + it('should list all calendars', async () => { + const mockCalendars = [ + { id: 'primary', summary: 'Primary Calendar' }, + { id: 'work', summary: 'Work Calendar' }, + { id: 'personal', summary: 'Personal Calendar' }, + ]; + + mockCalendarAPI.calendarList.list.mockResolvedValue({ + data: { + items: mockCalendars, + }, + }); + + const result = await calendarService.listCalendars(); + + expect(mockCalendarAPI.calendarList.list).toHaveBeenCalledTimes(1); + + const expectedResult = mockCalendars.map(c => ({ + id: c.id, + summary: c.summary + })); + expect(JSON.parse(result.content[0].text)).toEqual(expectedResult); + }); + + it('should handle empty calendar list', async () => { + mockCalendarAPI.calendarList.list.mockResolvedValue({ + data: { + items: [], + }, + }); + + const result = await calendarService.listCalendars(); + + expect(mockCalendarAPI.calendarList.list).toHaveBeenCalledTimes(1); + expect(JSON.parse(result.content[0].text)).toEqual([]); + }); + + it('should handle API errors gracefully', async () => { + const apiError = new Error('Calendar API failed'); + mockCalendarAPI.calendarList.list.mockRejectedValue(apiError); + + const result = await calendarService.listCalendars(); + + expect(JSON.parse(result.content[0].text)).toEqual({ error: 'Calendar API failed' }); + }); + + it('should handle undefined items in response', async () => { + mockCalendarAPI.calendarList.list.mockResolvedValue({ + data: {}, + }); + + const result = await calendarService.listCalendars(); + + expect(JSON.parse(result.content[0].text)).toEqual([]); + }); + }); + + describe('createEvent', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient); + mockCalendarAPI.calendarList.list.mockResolvedValue({ + data: { + items: [{ id: 'primary-calendar-id', primary: true }], + }, + }); + }); + + it('should create a calendar event without a calendarId', async () => { + const eventInput = { + summary: 'Team Meeting', + start: { dateTime: '2024-01-15T10:00:00-07:00' }, + end: { dateTime: '2024-01-15T11:00:00-07:00' }, + }; + + const mockCreatedEvent = { + id: 'event123', + summary: 'Team Meeting', + start: eventInput.start, + end: eventInput.end, + status: 'confirmed', + }; + + mockCalendarAPI.events.insert.mockResolvedValue({ + data: mockCreatedEvent, + }); + + const result = await calendarService.createEvent(eventInput); + + expect(mockCalendarAPI.events.insert).toHaveBeenCalledWith({ + calendarId: 'primary-calendar-id', + requestBody: { + summary: 'Team Meeting', + start: eventInput.start, + end: eventInput.end, + }, + }); + + expect(JSON.parse(result.content[0].text)).toEqual(mockCreatedEvent); + }); + + it('should create a calendar event', async () => { + const eventInput = { + calendarId: 'primary', + summary: 'Team Meeting', + start: { dateTime: '2024-01-15T10:00:00-07:00' }, + end: { dateTime: '2024-01-15T11:00:00-07:00' }, + }; + + const mockCreatedEvent = { + id: 'event123', + summary: 'Team Meeting', + start: eventInput.start, + end: eventInput.end, + status: 'confirmed', + }; + + mockCalendarAPI.events.insert.mockResolvedValue({ + data: mockCreatedEvent, + }); + + const result = await calendarService.createEvent(eventInput); + + expect(mockCalendarAPI.events.insert).toHaveBeenCalledWith({ + calendarId: 'primary', + requestBody: { + summary: 'Team Meeting', + start: eventInput.start, + end: eventInput.end, + }, + }); + + expect(JSON.parse(result.content[0].text)).toEqual(mockCreatedEvent); + }); + + it('should handle event creation errors', async () => { + const eventInput = { + calendarId: 'primary', + summary: 'Invalid Event', + start: { dateTime: 'invalid-date' }, + end: { dateTime: 'invalid-date' }, + }; + + // The validation now catches this before it reaches the API + const result = await calendarService.createEvent(eventInput); + + const errorResponse = JSON.parse(result.content[0].text); + expect(errorResponse.error).toBe('Invalid input format'); + expect(errorResponse.details).toContain('Invalid ISO 8601 datetime format'); + }); + }); + + describe('listEvents', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient); + mockCalendarAPI.calendarList.list.mockResolvedValue({ + data: { + items: [{ id: 'primary-calendar-id', primary: true }], + }, + }); + }); + + it('should list events for a calendar without a calendarId', async () => { + const mockEvents = [ + { + id: 'event1', + summary: 'Meeting 1', + start: { dateTime: '2024-01-15T09:00:00Z' }, + end: { dateTime: '2024-01-15T10:00:00Z' }, + status: 'confirmed', + }, + { + id: 'event2', + summary: 'Meeting 2', + start: { dateTime: '2024-01-15T14:00:00Z' }, + end: { dateTime: '2024-01-15T15:00:00Z' }, + status: 'confirmed', + }, + ]; + + mockCalendarAPI.events.list.mockResolvedValue({ + data: { + items: mockEvents, + }, + }); + + const result = await calendarService.listEvents({ + timeMin: '2024-01-15T00:00:00Z', + timeMax: '2024-01-16T00:00:00Z', + }); + + expect(mockCalendarAPI.events.list).toHaveBeenCalledWith({ + calendarId: 'primary-calendar-id', + timeMin: '2024-01-15T00:00:00Z', + timeMax: '2024-01-16T00:00:00Z', + singleEvents: true, + fields: 'items(id,summary,start,end,description,htmlLink,attendees,status)', + }); + + expect(JSON.parse(result.content[0].text)).toEqual(mockEvents); + }); + + it('should list events for a calendar', async () => { + const mockEvents = [ + { + id: 'event1', + summary: 'Meeting 1', + start: { dateTime: '2024-01-15T09:00:00Z' }, + end: { dateTime: '2024-01-15T10:00:00Z' }, + status: 'confirmed', + }, + { + id: 'event2', + summary: 'Meeting 2', + start: { dateTime: '2024-01-15T14:00:00Z' }, + end: { dateTime: '2024-01-15T15:00:00Z' }, + status: 'confirmed', + }, + ]; + + mockCalendarAPI.events.list.mockResolvedValue({ + data: { + items: mockEvents, + }, + }); + + const result = await calendarService.listEvents({ + calendarId: 'primary', + timeMin: '2024-01-15T00:00:00Z', + timeMax: '2024-01-16T00:00:00Z', + }); + + expect(mockCalendarAPI.events.list).toHaveBeenCalledWith({ + calendarId: 'primary', + timeMin: '2024-01-15T00:00:00Z', + timeMax: '2024-01-16T00:00:00Z', + singleEvents: true, + fields: 'items(id,summary,start,end,description,htmlLink,attendees,status)', + }); + + expect(JSON.parse(result.content[0].text)).toEqual(mockEvents); + }); + + it('should list events with a default timeMax', async () => { + const mockEvents = [ + { + id: 'event1', + summary: 'Meeting 1', + start: { dateTime: '2024-01-15T09:00:00Z' }, + end: { dateTime: '2024-01-15T10:00:00Z' }, + status: 'confirmed', + }, + { + id: 'event2', + summary: 'Meeting 2', + start: { dateTime: '2024-01-15T14:00:00Z' }, + end: { dateTime: '2024-01-15T15:00:00Z' }, + status: 'confirmed', + }, + ]; + + mockCalendarAPI.events.list.mockResolvedValue({ + data: { + items: mockEvents, + }, + }); + + const result = await calendarService.listEvents({ + calendarId: 'primary', + timeMin: '2024-01-15T00:00:00Z', + }); + + expect(mockCalendarAPI.events.list).toHaveBeenCalledWith( + expect.objectContaining({ + timeMax: expect.any(String), + }), + ); + + expect(JSON.parse(result.content[0].text)).toEqual(mockEvents); + }); + + it('should filter out cancelled events', async () => { + const mockEvents = [ + { + id: 'event1', + summary: 'Active Meeting', + status: 'confirmed', + }, + { + id: 'event2', + summary: 'Cancelled Meeting', + status: 'cancelled', + }, + { + id: 'event3', + summary: 'Another Active Meeting', + status: 'confirmed', + }, + ]; + + mockCalendarAPI.events.list.mockResolvedValue({ + data: { + items: mockEvents, + }, + }); + + const result = await calendarService.listEvents({ + calendarId: 'primary', + }); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult).toHaveLength(2); + expect(parsedResult.map((e: any) => e.id)).toEqual(['event1', 'event3']); + }); + + it('should filter events based on attendee response status', async () => { + const mockEvents = [ + { + id: 'event1', + summary: 'Meeting I accepted', + status: 'confirmed', + attendees: [ + { email: 'me@example.com', self: true, responseStatus: 'accepted' }, + { email: 'other@example.com', responseStatus: 'tentative' }, + ], + }, + { + id: 'event2', + summary: 'Meeting I declined', + status: 'confirmed', + attendees: [ + { email: 'me@example.com', self: true, responseStatus: 'declined' }, + { email: 'other@example.com', responseStatus: 'accepted' }, + ], + }, + { + id: 'event3', + summary: 'Meeting needs response', + status: 'confirmed', + attendees: [ + { email: 'me@example.com', self: true, responseStatus: 'needsAction' }, + ], + }, + ]; + + mockCalendarAPI.events.list.mockResolvedValue({ + data: { + items: mockEvents, + }, + }); + + const result = await calendarService.listEvents({ + calendarId: 'primary', + attendeeResponseStatus: ['accepted', 'needsAction'], + }); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult).toHaveLength(2); + expect(parsedResult.map((e: any) => e.id)).toEqual(['event1', 'event3']); + }); + + it('should include events with no attendees', async () => { + const mockEvents = [ + { + id: 'event1', + summary: 'Personal Task', + status: 'confirmed', + // No attendees property + }, + { + id: 'event2', + summary: 'Meeting with attendees', + status: 'confirmed', + attendees: [ + { email: 'me@example.com', self: true, responseStatus: 'accepted' }, + ], + }, + ]; + + mockCalendarAPI.events.list.mockResolvedValue({ + data: { + items: mockEvents, + }, + }); + + const result = await calendarService.listEvents({ + calendarId: 'primary', + }); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult).toHaveLength(2); + }); + + it('should filter out events without summary', async () => { + const mockEvents = [ + { + id: 'event1', + summary: 'Valid Event', + status: 'confirmed', + }, + { + id: 'event2', + // No summary + status: 'confirmed', + }, + { + id: 'event3', + summary: null, + status: 'confirmed', + }, + ]; + + mockCalendarAPI.events.list.mockResolvedValue({ + data: { + items: mockEvents, + }, + }); + + const result = await calendarService.listEvents({ + calendarId: 'primary', + }); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult).toHaveLength(1); + expect(parsedResult[0].id).toBe('event1'); + }); + + it('should handle API errors gracefully', async () => { + const apiError = new Error('Events API failed'); + mockCalendarAPI.events.list.mockRejectedValue(apiError); + + const result = await calendarService.listEvents({ + calendarId: 'primary', + }); + + expect(JSON.parse(result.content[0].text)).toEqual({ error: 'Events API failed' }); + }); + + it('should handle empty events list', async () => { + mockCalendarAPI.events.list.mockResolvedValue({ + data: { + items: [], + }, + }); + + const result = await calendarService.listEvents({ + calendarId: 'primary', + }); + + expect(JSON.parse(result.content[0].text)).toEqual([]); + }); + + it('should use default attendeeResponseStatus when not provided', async () => { + const mockEvents = [ + { + id: 'event1', + summary: 'Meeting', + status: 'confirmed', + attendees: [ + { email: 'me@example.com', self: true, responseStatus: 'accepted' }, + ], + }, + ]; + + mockCalendarAPI.events.list.mockResolvedValue({ + data: { + items: mockEvents, + }, + }); + + await calendarService.listEvents({ + calendarId: 'primary', + }); + + expect(mockCalendarAPI.events.list).toHaveBeenCalledWith( + expect.objectContaining({ + calendarId: 'primary', + }) + ); + }); + }); + + describe('findFreeTime', () => { + it('should find a free time slot', async () => { + const busyData = { + 'user1@example.com': { + busy: [ + { start: '2024-01-15T09:00:00Z', end: '2024-01-15T10:00:00Z' }, + { start: '2024-01-15T14:00:00Z', end: '2024-01-15T15:00:00Z' }, + ], + }, + 'user2@example.com': { + busy: [ + { start: '2024-01-15T10:30:00Z', end: '2024-01-15T11:30:00Z' }, + ], + }, + }; + + mockCalendarAPI.freebusy.query.mockResolvedValue({ + data: { calendars: busyData }, + }); + + const result = await calendarService.findFreeTime({ + attendees: ['user1@example.com', 'user2@example.com'], + timeMin: '2024-01-15T08:00:00Z', + timeMax: '2024-01-15T18:00:00Z', + duration: 60, + }); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult.start).toBeDefined(); + expect(parsedResult.end).toBeDefined(); + expect(new Date(parsedResult.end).getTime() - new Date(parsedResult.start).getTime()).toBe(60 * 60 * 1000); + }); + + it('should return an error if no free time is found', async () => { + const busyData = { + 'user1@example.com': { + busy: [ + { start: '2024-01-15T08:00:00Z', end: '2024-01-15T18:00:00Z' }, + ], + }, + }; + + mockCalendarAPI.freebusy.query.mockResolvedValue({ + data: { calendars: busyData }, + }); + + const result = await calendarService.findFreeTime({ + attendees: ['user1@example.com'], + timeMin: '2024-01-15T08:00:00Z', + timeMax: '2024-01-15T18:00:00Z', + duration: 60, + }); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult.error).toBe('No available free time found'); + }); + + it('should handle the "me" attendee', async () => { + mockCalendarAPI.calendarList.list.mockResolvedValue({ + data: { + items: [{ id: 'primary-calendar-id', primary: true }], + }, + }); + + const busyData = { + 'primary-calendar-id': { + busy: [], + }, + }; + + mockCalendarAPI.freebusy.query.mockResolvedValue({ + data: { calendars: busyData }, + }); + + const result = await calendarService.findFreeTime({ + attendees: ['me'], + timeMin: '2024-01-15T08:00:00Z', + timeMax: '2024-01-15T18:00:00Z', + duration: 30, + }); + + expect(mockCalendarAPI.freebusy.query).toHaveBeenCalledWith({ + requestBody: { + items: [{ id: 'primary-calendar-id' }], + timeMin: '2024-01-15T08:00:00Z', + timeMax: '2024-01-15T18:00:00Z', + }, + }); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult.start).toBeDefined(); + }); + }); + + describe('updateEvent', () => { + beforeEach(async () => { + mockCalendarAPI.calendarList.list.mockResolvedValue({ + data: { + items: [{ id: 'primary', primary: true }], + }, + }); + }); + + it('should update an event', async () => { + const updatedEvent = { + id: 'event123', + summary: 'Updated Meeting', + start: { dateTime: '2024-01-15T14:00:00Z' }, + end: { dateTime: '2024-01-15T15:00:00Z' }, + attendees: [{ email: 'new@example.com' }], + }; + + mockCalendarAPI.events.update.mockResolvedValue({ data: updatedEvent }); + + const result = await calendarService.updateEvent({ + eventId: 'event123', + summary: 'Updated Meeting', + start: { dateTime: '2024-01-15T14:00:00Z' }, + end: { dateTime: '2024-01-15T15:00:00Z' }, + attendees: ['new@example.com'], + }); + + expect(mockCalendarAPI.events.update).toHaveBeenCalledWith({ + calendarId: 'primary', + eventId: 'event123', + requestBody: { + summary: 'Updated Meeting', + start: { dateTime: '2024-01-15T14:00:00Z' }, + end: { dateTime: '2024-01-15T15:00:00Z' }, + attendees: [{ email: 'new@example.com' }], + }, + }); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult.id).toBe('event123'); + expect(parsedResult.summary).toBe('Updated Meeting'); + }); + + it('should handle update errors', async () => { + const apiError = new Error('Update failed'); + mockCalendarAPI.events.update.mockRejectedValue(apiError); + + const result = await calendarService.updateEvent({ + eventId: 'event123', + summary: 'Updated Meeting', + }); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult.error).toBe('Update failed'); + }); + + it('should only send fields that are provided', async () => { + const updatedEvent = { + id: 'event123', + summary: 'Updated Meeting Only', + }; + + mockCalendarAPI.events.update.mockResolvedValue({ data: updatedEvent }); + + await calendarService.updateEvent({ + eventId: 'event123', + summary: 'Updated Meeting Only', + }); + + expect(mockCalendarAPI.events.update).toHaveBeenCalledWith({ + calendarId: 'primary', + eventId: 'event123', + requestBody: { + summary: 'Updated Meeting Only', + }, + }); + }); + }); + + describe('respondToEvent', () => { + beforeEach(async () => { + mockCalendarAPI.calendarList.list.mockResolvedValue({ + data: { + items: [{ id: 'primary', primary: true }], + }, + }); + }); + + it('should accept a meeting invitation', async () => { + const mockEvent = { + id: 'event123', + summary: 'Team Meeting', + attendees: [ + { email: 'me@example.com', self: true, responseStatus: 'needsAction' }, + { email: 'other@example.com', responseStatus: 'accepted' }, + ], + }; + + const updatedEvent = { + ...mockEvent, + attendees: [ + { email: 'me@example.com', self: true, responseStatus: 'accepted' }, + { email: 'other@example.com', responseStatus: 'accepted' }, + ], + }; + + mockCalendarAPI.events.get.mockResolvedValue({ data: mockEvent }); + mockCalendarAPI.events.patch.mockResolvedValue({ data: updatedEvent }); + + const result = await calendarService.respondToEvent({ + eventId: 'event123', + responseStatus: 'accepted', + }); + + expect(mockCalendarAPI.events.get).toHaveBeenCalledWith({ + calendarId: 'primary', + eventId: 'event123', + }); + + expect(mockCalendarAPI.events.patch).toHaveBeenCalledWith({ + calendarId: 'primary', + eventId: 'event123', + sendNotifications: true, + requestBody: { + attendees: expect.arrayContaining([ + expect.objectContaining({ + email: 'me@example.com', + self: true, + responseStatus: 'accepted', + }), + ]), + }, + }); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult.eventId).toBe('event123'); + expect(parsedResult.responseStatus).toBe('accepted'); + expect(parsedResult.message).toContain('Successfully accepted'); + }); + + it('should decline a meeting invitation with a message', async () => { + const mockEvent = { + id: 'event123', + summary: 'Team Meeting', + attendees: [ + { email: 'me@example.com', self: true, responseStatus: 'needsAction' }, + { email: 'other@example.com', responseStatus: 'accepted' }, + ], + }; + + const updatedEvent = { + ...mockEvent, + attendees: [ + { email: 'me@example.com', self: true, responseStatus: 'declined', comment: 'Sorry, I have a conflict' }, + { email: 'other@example.com', responseStatus: 'accepted' }, + ], + }; + + mockCalendarAPI.events.get.mockResolvedValue({ data: mockEvent }); + mockCalendarAPI.events.patch.mockResolvedValue({ data: updatedEvent }); + + const result = await calendarService.respondToEvent({ + eventId: 'event123', + responseStatus: 'declined', + responseMessage: 'Sorry, I have a conflict', + }); + + expect(mockCalendarAPI.events.patch).toHaveBeenCalledWith({ + calendarId: 'primary', + eventId: 'event123', + sendNotifications: true, + requestBody: { + attendees: expect.arrayContaining([ + expect.objectContaining({ + email: 'me@example.com', + self: true, + responseStatus: 'declined', + comment: 'Sorry, I have a conflict', + }), + ]), + }, + }); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult.responseStatus).toBe('declined'); + expect(parsedResult.message).toContain('with message'); + }); + + it('should mark attendance as tentative', async () => { + const mockEvent = { + id: 'event123', + summary: 'Team Meeting', + attendees: [ + { email: 'me@example.com', self: true, responseStatus: 'needsAction' }, + ], + }; + + mockCalendarAPI.events.get.mockResolvedValue({ data: mockEvent }); + mockCalendarAPI.events.patch.mockResolvedValue({ data: { ...mockEvent, attendees: [{ ...mockEvent.attendees[0], responseStatus: 'tentative' }] } }); + + const result = await calendarService.respondToEvent({ + eventId: 'event123', + responseStatus: 'tentative', + sendNotification: false, + }); + + expect(mockCalendarAPI.events.patch).toHaveBeenCalledWith({ + calendarId: 'primary', + eventId: 'event123', + sendNotifications: false, + requestBody: { + attendees: expect.arrayContaining([ + expect.objectContaining({ + responseStatus: 'tentative', + }), + ]), + }, + }); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult.responseStatus).toBe('tentative'); + }); + + it('should handle events with no attendees', async () => { + const mockEvent = { + id: 'event123', + summary: 'Personal Event', + // No attendees + }; + + mockCalendarAPI.events.get.mockResolvedValue({ data: mockEvent }); + + const result = await calendarService.respondToEvent({ + eventId: 'event123', + responseStatus: 'accepted', + }); + + expect(mockCalendarAPI.events.patch).not.toHaveBeenCalled(); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult.error).toBe('Event has no attendees'); + }); + + it('should handle when user is not an attendee', async () => { + const mockEvent = { + id: 'event123', + summary: 'Meeting', + attendees: [ + { email: 'other1@example.com', responseStatus: 'accepted' }, + { email: 'other2@example.com', responseStatus: 'tentative' }, + ], + }; + + mockCalendarAPI.events.get.mockResolvedValue({ data: mockEvent }); + + const result = await calendarService.respondToEvent({ + eventId: 'event123', + responseStatus: 'accepted', + }); + + expect(mockCalendarAPI.events.patch).not.toHaveBeenCalled(); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult.error).toBe('You are not an attendee of this event'); + }); + + it('should use custom calendar ID when provided', async () => { + const mockEvent = { + id: 'event123', + summary: 'Team Meeting', + attendees: [ + { email: 'me@example.com', self: true, responseStatus: 'needsAction' }, + ], + }; + + mockCalendarAPI.events.get.mockResolvedValue({ data: mockEvent }); + mockCalendarAPI.events.patch.mockResolvedValue({ data: { ...mockEvent, attendees: [{ ...mockEvent.attendees[0], responseStatus: 'accepted' }] } }); + + await calendarService.respondToEvent({ + eventId: 'event123', + calendarId: 'custom-calendar-id', + responseStatus: 'accepted', + }); + + expect(mockCalendarAPI.events.get).toHaveBeenCalledWith({ + calendarId: 'custom-calendar-id', + eventId: 'event123', + }); + + expect(mockCalendarAPI.events.patch).toHaveBeenCalledWith( + expect.objectContaining({ + calendarId: 'custom-calendar-id', + }) + ); + }); + + it('should handle API errors gracefully', async () => { + const apiError = new Error('Calendar API failed'); + mockCalendarAPI.events.get.mockRejectedValue(apiError); + + const result = await calendarService.respondToEvent({ + eventId: 'event123', + responseStatus: 'accepted', + }); + + const parsedResult = JSON.parse(result.content[0].text); + expect(parsedResult.error).toBe('Calendar API failed'); + }); + }); + + describe('getEvent', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient); + mockCalendarAPI.calendarList.list.mockResolvedValue({ + data: { + items: [{ id: 'primary-calendar-id', primary: true }], + }, + }); + }); + + it('should retrieve a specific event', async () => { + const mockEvent = { + id: 'event123', + summary: 'Test Event', + start: { dateTime: '2024-01-15T10:00:00-07:00' }, + end: { dateTime: '2024-01-15T11:00:00-07:00' }, + }; + + mockCalendarAPI.events.get.mockResolvedValue({ data: mockEvent }); + + const result = await calendarService.getEvent({ eventId: 'event123', calendarId: 'primary' }); + + expect(mockCalendarAPI.events.get).toHaveBeenCalledWith({ + calendarId: 'primary', + eventId: 'event123', + }); + + expect(JSON.parse(result.content[0].text)).toEqual(mockEvent); + }); + + it('should retrieve an event using the primary calendar if no calendarId is provided', async () => { + const mockEvent = { + id: 'event123', + summary: 'Test Event', + start: { dateTime: '2024-01-15T10:00:00-07:00' }, + end: { dateTime: '2024-01-15T11:00:00-07:00' }, + }; + + mockCalendarAPI.events.get.mockResolvedValue({ data: mockEvent }); + + const result = await calendarService.getEvent({ eventId: 'event123' }); + + expect(mockCalendarAPI.events.get).toHaveBeenCalledWith({ + calendarId: 'primary-calendar-id', + eventId: 'event123', + }); + + expect(JSON.parse(result.content[0].text)).toEqual(mockEvent); + }); + + it('should handle API errors when getting an event', async () => { + const apiError = new Error('Event not found'); + mockCalendarAPI.events.get.mockRejectedValue(apiError); + + const result = await calendarService.getEvent({ eventId: 'non-existent-event', calendarId: 'primary' }); + + expect(JSON.parse(result.content[0].text)).toEqual({ error: 'Event not found' }); + }); + }); +}); \ No newline at end of file diff --git a/workspace-mcp-server/src/__tests__/services/ChatService.test.ts b/workspace-mcp-server/src/__tests__/services/ChatService.test.ts new file mode 100644 index 00000000..67ba7b15 --- /dev/null +++ b/workspace-mcp-server/src/__tests__/services/ChatService.test.ts @@ -0,0 +1,591 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +import { ChatService } from '../../services/ChatService'; +import { AuthManager } from '../../auth/AuthManager'; +import { google } from 'googleapis'; + +// Mock the googleapis module +jest.mock('googleapis'); +jest.mock('../../utils/logger'); + +describe('ChatService', () => { + let chatService: ChatService; + let mockAuthManager: jest.Mocked; + let mockChatAPI: any; + let mockPeopleAPI: any; + + beforeEach(() => { + // Clear all mocks before each test + jest.clearAllMocks(); + + // Create mock AuthManager + mockAuthManager = { + getAuthenticatedClient: jest.fn(), + loadSavedCredentialsIfExist: jest.fn(), + saveCredentials: jest.fn(), + authorize: jest.fn(), + } as any; + + // Create mock Chat API + mockChatAPI = { + spaces: { + list: jest.fn(), + setup: jest.fn(), + messages: { + create: jest.fn(), + list: jest.fn(), + }, + members: { + list: jest.fn(), + }, + }, + }; + + // Create mock People API + mockPeopleAPI = { + people: { + get: jest.fn(), + searchContacts: jest.fn(), + }, + }; + + // Mock the google constructors + (google.chat as jest.Mock) = jest.fn().mockReturnValue(mockChatAPI); + (google.people as jest.Mock) = jest.fn().mockReturnValue(mockPeopleAPI); + + // Create ChatService instance + chatService = new ChatService(mockAuthManager); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('initialize', () => { + it('should initialize Chat and People API clients', async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + + await chatService.initialize(); + + expect(mockAuthManager.getAuthenticatedClient).toHaveBeenCalledTimes(1); + expect(google.chat).toHaveBeenCalledWith( + expect.objectContaining({ + version: 'v1', + auth: mockAuthClient, + }) + ); + expect(google.people).toHaveBeenCalledWith( + expect.objectContaining({ + version: 'v1', + auth: mockAuthClient, + }) + ); + }); + }); + + describe('listSpaces', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await chatService.initialize(); + }); + + it('should list all chat spaces', async () => { + const mockSpaces = [ + { name: 'spaces/space1', displayName: 'Team Chat' }, + { name: 'spaces/space2', displayName: 'Project Discussion' }, + ]; + + mockChatAPI.spaces.list.mockResolvedValue({ + data: { + spaces: mockSpaces, + }, + }); + + const result = await chatService.listSpaces(); + + expect(mockChatAPI.spaces.list).toHaveBeenCalledWith({}); + expect(JSON.parse(result.content[0].text)).toEqual(mockSpaces); + }); + + it('should handle empty spaces list', async () => { + mockChatAPI.spaces.list.mockResolvedValue({ + data: { + spaces: [], + }, + }); + + const result = await chatService.listSpaces(); + + expect(JSON.parse(result.content[0].text)).toEqual([]); + }); + + it('should handle API errors gracefully', async () => { + const apiError = new Error('Chat API failed'); + mockChatAPI.spaces.list.mockRejectedValue(apiError); + + const result = await chatService.listSpaces(); + + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('An error occurred while listing chat spaces.'); + expect(response.details).toBe('Chat API failed'); + }); + }); + + describe('sendMessage', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await chatService.initialize(); + }); + + it('should send a message to a space', async () => { + const mockResponse = { + name: 'spaces/space1/messages/msg1', + text: 'Hello, team!', + createTime: '2024-01-01T00:00:00Z', + }; + + mockChatAPI.spaces.messages.create.mockResolvedValue({ + data: mockResponse, + }); + + const result = await chatService.sendMessage({ + spaceName: 'spaces/space1', + message: 'Hello, team!', + }); + + expect(mockChatAPI.spaces.messages.create).toHaveBeenCalledWith({ + parent: 'spaces/space1', + requestBody: { + text: 'Hello, team!', + }, + }); + expect(JSON.parse(result.content[0].text)).toEqual(mockResponse); + }); + + it('should handle message sending errors', async () => { + const apiError = new Error('Failed to send message'); + mockChatAPI.spaces.messages.create.mockRejectedValue(apiError); + + const result = await chatService.sendMessage({ + spaceName: 'spaces/space1', + message: 'Test message', + }); + + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('An error occurred while sending the message.'); + expect(response.details).toBe('Failed to send message'); + }); + }); + + describe('findSpaceByName', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await chatService.initialize(); + }); + + it('should find spaces by display name', async () => { + const mockSpaces = [ + { name: 'spaces/space1', displayName: 'Team Chat' }, + { name: 'spaces/space2', displayName: 'Project Discussion' }, + { name: 'spaces/space3', displayName: 'Team Chat' }, + ]; + + mockChatAPI.spaces.list.mockResolvedValue({ + data: { + spaces: mockSpaces, + nextPageToken: null, + }, + }); + + const result = await chatService.findSpaceByName({ displayName: 'Team Chat' }); + + expect(mockChatAPI.spaces.list).toHaveBeenCalled(); + const foundSpaces = JSON.parse(result.content[0].text); + expect(foundSpaces).toHaveLength(2); + expect(foundSpaces[0].displayName).toBe('Team Chat'); + expect(foundSpaces[1].displayName).toBe('Team Chat'); + }); + + it('should handle pagination when searching for spaces', async () => { + const mockSpacesPage1 = [ + { name: 'spaces/space1', displayName: 'Other Chat' }, + { name: 'spaces/space2', displayName: 'Another Chat' }, + ]; + const mockSpacesPage2 = [ + { name: 'spaces/space3', displayName: 'Team Chat' }, + ]; + + mockChatAPI.spaces.list + .mockResolvedValueOnce({ + data: { + spaces: mockSpacesPage1, + nextPageToken: 'page2', + }, + }) + .mockResolvedValueOnce({ + data: { + spaces: mockSpacesPage2, + nextPageToken: null, + }, + }); + + const result = await chatService.findSpaceByName({ displayName: 'Team Chat' }); + + expect(mockChatAPI.spaces.list).toHaveBeenCalledTimes(2); + const foundSpaces = JSON.parse(result.content[0].text); + expect(foundSpaces).toHaveLength(1); + expect(foundSpaces[0].displayName).toBe('Team Chat'); + }); + + it('should return error when space not found', async () => { + mockChatAPI.spaces.list.mockResolvedValue({ + data: { + spaces: [ + { name: 'spaces/space1', displayName: 'Other Chat' }, + ], + }, + }); + + const result = await chatService.findSpaceByName({ displayName: 'Non-existent Chat' }); + + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('No space found with display name: Non-existent Chat'); + }); + }); + + describe('getMessages', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await chatService.initialize(); + }); + + it('should list messages from a space', async () => { + const mockMessages = [ + { name: 'spaces/space1/messages/msg1', text: 'Hello' }, + { name: 'spaces/space1/messages/msg2', text: 'How are you?' }, + ]; + + mockChatAPI.spaces.messages.list.mockResolvedValue({ + data: { + messages: mockMessages, + nextPageToken: 'next', + }, + }); + + const result = await chatService.getMessages({ + spaceName: 'spaces/space1', + pageSize: 10, + }); + + expect(mockChatAPI.spaces.messages.list).toHaveBeenCalledWith({ + parent: 'spaces/space1', + pageSize: 10, + pageToken: undefined, + }); + + const response = JSON.parse(result.content[0].text); + expect(response.messages).toEqual(mockMessages); + expect(response.nextPageToken).toBe('next'); + }); + + it('should filter unread messages when unreadOnly is true', async () => { + const mockPerson = { + data: { + metadata: { + sources: [ + { type: 'PROFILE', id: 'user123' }, + ], + }, + }, + }; + + const mockMembers = [ + { + member: { name: 'users/user123' }, + lastReadTime: '2024-01-01T00:00:00Z', + }, + ]; + + const mockMessages = [ + { name: 'spaces/space1/messages/msg1', text: 'Unread message' }, + ]; + + mockPeopleAPI.people.get.mockResolvedValue(mockPerson); + mockChatAPI.spaces.members.list.mockResolvedValue({ + data: { + memberships: mockMembers, + }, + }); + mockChatAPI.spaces.messages.list.mockResolvedValue({ + data: { + messages: mockMessages, + }, + }); + + const result = await chatService.getMessages({ + spaceName: 'spaces/space1', + unreadOnly: true, + }); + + expect(mockPeopleAPI.people.get).toHaveBeenCalledWith({ + resourceName: 'people/me', + personFields: 'metadata', + }); + expect(mockChatAPI.spaces.messages.list).toHaveBeenCalledWith({ + parent: 'spaces/space1', + filter: 'createTime > "2024-01-01T00:00:00Z"', + pageSize: undefined, + pageToken: undefined, + }); + + const response = JSON.parse(result.content[0].text); + expect(response.messages).toEqual(mockMessages); + }); + + it('should handle case when user has no last read time', async () => { + const mockPerson = { + data: { + metadata: { + sources: [ + { type: 'PROFILE', id: 'user123' }, + ], + }, + }, + }; + + const mockMembers = [ + { + member: { name: 'users/user123' }, + // No lastReadTime property + }, + ]; + + const mockMessages = [ + { name: 'spaces/space1/messages/msg1', text: 'All messages are unread' }, + ]; + + mockPeopleAPI.people.get.mockResolvedValue(mockPerson); + mockChatAPI.spaces.members.list.mockResolvedValue({ + data: { + memberships: mockMembers, + }, + }); + mockChatAPI.spaces.messages.list.mockResolvedValue({ + data: { + messages: mockMessages, + }, + }); + + const result = await chatService.getMessages({ + spaceName: 'spaces/space1', + unreadOnly: true, + }); + + // Should list all messages when no last read time + expect(mockChatAPI.spaces.messages.list).toHaveBeenCalledWith({ + parent: 'spaces/space1', + pageSize: undefined, + pageToken: undefined, + }); + + const response = JSON.parse(result.content[0].text); + expect(response.messages).toEqual(mockMessages); + }); + }); + + describe('sendDm', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await chatService.initialize(); + }); + + it('should send a direct message to a user', async () => { + const mockSpace = { + name: 'spaces/dm123', + spaceType: 'DIRECT_MESSAGE', + }; + + const mockMessage = { + name: 'spaces/dm123/messages/msg1', + text: 'Hello!', + }; + + mockChatAPI.spaces.setup.mockResolvedValue({ + data: mockSpace, + }); + + mockChatAPI.spaces.messages.create.mockResolvedValue({ + data: mockMessage, + }); + + const result = await chatService.sendDm({ + email: 'user@example.com', + message: 'Hello!', + }); + + expect(mockChatAPI.spaces.setup).toHaveBeenCalledWith({ + requestBody: { + space: { + spaceType: 'DIRECT_MESSAGE', + }, + memberships: [ + { + member: { + name: 'users/user@example.com', + type: 'HUMAN', + }, + }, + ], + }, + }); + + expect(mockChatAPI.spaces.messages.create).toHaveBeenCalledWith({ + parent: 'spaces/dm123', + requestBody: { + text: 'Hello!', + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response).toEqual(mockMessage); + }); + + it('should handle DM sending errors', async () => { + const apiError = new Error('Failed to setup DM space'); + mockChatAPI.spaces.setup.mockRejectedValue(apiError); + + const result = await chatService.sendDm({ + email: 'user@example.com', + message: 'Test message', + }); + + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('An error occurred while sending the DM.'); + expect(response.details).toBe('Failed to setup DM space'); + }); + }); + + describe('findDmByEmail', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await chatService.initialize(); + }); + + it('should find a DM space by user email using spaces.setup', async () => { + const mockSpace = { + name: 'spaces/dm123', + spaceType: 'DIRECT_MESSAGE', + }; + + mockChatAPI.spaces.setup.mockResolvedValue({ + data: mockSpace, + }); + + const result = await chatService.findDmByEmail({ email: 'user@example.com' }); + + expect(mockChatAPI.spaces.setup).toHaveBeenCalledWith({ + requestBody: { + space: { + spaceType: 'DIRECT_MESSAGE', + }, + memberships: [ + { + member: { + name: 'users/user@example.com', + type: 'HUMAN', + }, + }, + ], + }, + }); + + const foundSpace = JSON.parse(result.content[0].text); + expect(foundSpace).toEqual(mockSpace); + }); + + it('should return an error if spaces.setup fails', async () => { + const apiError = new Error('Failed to setup DM space'); + mockChatAPI.spaces.setup.mockRejectedValue(apiError); + + const result = await chatService.findDmByEmail({ email: 'user@example.com' }); + + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('An error occurred while finding the DM space.'); + expect(response.details).toBe('Failed to setup DM space'); + }); + }); + + describe('createSpace', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await chatService.initialize(); + }); + + it('should create a space and return the space data', async () => { + const mockResponse = { + name: 'spaces/space1', + displayName: 'Test Space', + }; + + mockChatAPI.spaces.setup.mockResolvedValue({ + data: mockResponse, + }); + + const result = await chatService.setUpSpace({ + displayName: 'Test Space', + userNames: ['users/123456', 'users/456789'], + }); + + expect(mockChatAPI.spaces.setup).toHaveBeenCalledWith({ + requestBody: { + space: { + spaceType: 'SPACE', + displayName: 'Test Space', + }, + memberships: [ + { + member: { + name: 'users/123456', + type: 'HUMAN', + }, + }, + { + member: { + name: 'users/456789', + type: 'HUMAN', + }, + }, + ], + }, + }); + expect(JSON.parse(result.content[0].text)).toEqual(mockResponse); + }); + + it('should handle space creation errors', async () => { + const apiError = new Error('Failed to create space'); + mockChatAPI.spaces.setup.mockRejectedValue(apiError); + + const result = await chatService.setUpSpace({ + displayName: 'Test Space', + userNames: ['users/123456'], + }); + + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('An error occurred while creating the space.'); + expect(response.details).toBe('Failed to create space'); + }); + }); +}); diff --git a/workspace-mcp-server/src/__tests__/services/DocsService.test.ts b/workspace-mcp-server/src/__tests__/services/DocsService.test.ts new file mode 100644 index 00000000..a7eb886d --- /dev/null +++ b/workspace-mcp-server/src/__tests__/services/DocsService.test.ts @@ -0,0 +1,548 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +import { DocsService } from '../../services/DocsService'; +import { DriveService } from '../../services/DriveService'; +import { AuthManager } from '../../auth/AuthManager'; +import { google } from 'googleapis'; + +// Mock the googleapis module +jest.mock('googleapis'); +jest.mock('../../utils/logger'); + +describe('DocsService', () => { + let docsService: DocsService; + let mockAuthManager: jest.Mocked; + let mockDriveService: jest.Mocked; + let mockDocsAPI: any; + let mockDriveAPI: any; + + beforeEach(() => { + // Clear all mocks before each test + jest.clearAllMocks(); + + // Create mock AuthManager + mockAuthManager = { + getAuthenticatedClient: jest.fn(), + } as any; + + // Create mock DriveService + mockDriveService = { + findFolder: jest.fn(), + } as any; + + // Create mock Docs API + mockDocsAPI = { + documents: { + get: jest.fn(), + create: jest.fn(), + batchUpdate: jest.fn(), + }, + }; + + mockDriveAPI = { + files: { + create: jest.fn(), + list: jest.fn(), + get: jest.fn(), + update: jest.fn(), + }, + }; + + // Mock the google constructors + (google.docs as jest.Mock) = jest.fn().mockReturnValue(mockDocsAPI); + (google.drive as jest.Mock) = jest.fn().mockReturnValue(mockDriveAPI); + + // Create DocsService instance + docsService = new DocsService(mockAuthManager, mockDriveService); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('initialize', () => { + it('should initialize Docs and Drive API clients', async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + + await docsService.initialize(); + + expect(mockAuthManager.getAuthenticatedClient).toHaveBeenCalledTimes(1); + expect(google.docs).toHaveBeenCalledWith( + expect.objectContaining({ + version: 'v1', + auth: mockAuthClient, + }) + ); + expect(google.drive).toHaveBeenCalledWith( + expect.objectContaining({ + version: 'v3', + auth: mockAuthClient, + }) + ); + }); + }); + + describe('create', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await docsService.initialize(); + }); + + it('should create a blank document', async () => { + const mockDoc = { + data: { + documentId: 'test-doc-id', + title: 'Test Title', + }, + }; + mockDocsAPI.documents.create.mockResolvedValue(mockDoc); + + const result = await docsService.create({ title: 'Test Title' }); + + expect(mockDocsAPI.documents.create).toHaveBeenCalledWith({ + requestBody: { title: 'Test Title' }, + }); + expect(JSON.parse(result.content[0].text)).toEqual({ + documentId: 'test-doc-id', + title: 'Test Title', + }); + }); + + it('should create a document with markdown content', async () => { + const mockFile = { + data: { + id: 'test-doc-id', + name: 'Test Title', + }, + }; + mockDriveAPI.files.create.mockResolvedValue(mockFile); + + const result = await docsService.create({ title: 'Test Title', markdown: '# Hello' }); + + expect(mockDriveAPI.files.create).toHaveBeenCalled(); + expect(JSON.parse(result.content[0].text)).toEqual({ + documentId: 'test-doc-id', + title: 'Test Title', + }); + }); + + it('should move the document to a folder if folderName is provided', async () => { + const mockDoc = { + data: { + documentId: 'test-doc-id', + title: 'Test Title', + }, + }; + mockDocsAPI.documents.create.mockResolvedValue(mockDoc); + mockDriveService.findFolder.mockResolvedValue({ + content: [{ type: 'text', text: JSON.stringify([{ id: 'test-folder-id', name: 'Test Folder' }]) }], + }); + mockDriveAPI.files.get.mockResolvedValue({ data: { parents: ['root'] } }); + + await docsService.create({ title: 'Test Title', folderName: 'Test Folder' }); + + expect(mockDriveService.findFolder).toHaveBeenCalledWith({ folderName: 'Test Folder' }); + expect(mockDriveAPI.files.update).toHaveBeenCalledWith({ + fileId: 'test-doc-id', + addParents: 'test-folder-id', + removeParents: 'root', + fields: 'id, parents', + }); + }); + + it('should handle errors during document creation', async () => { + const apiError = new Error('API Error'); + mockDocsAPI.documents.create.mockRejectedValue(apiError); + + const result = await docsService.create({ title: 'Test Title' }); + + expect(JSON.parse(result.content[0].text)).toEqual({ error: 'API Error' }); + }); + }); + + describe('insertText', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await docsService.initialize(); + }); + + it('should insert text into a document', async () => { + const mockResponse = { + data: { + documentId: 'test-doc-id', + writeControl: {}, + }, + }; + mockDocsAPI.documents.batchUpdate.mockResolvedValue(mockResponse); + + const result = await docsService.insertText({ documentId: 'test-doc-id', text: 'Hello' }); + + expect(mockDocsAPI.documents.batchUpdate).toHaveBeenCalledWith({ + documentId: 'test-doc-id', + requestBody: { + requests: [{ + insertText: { + location: { index: 1 }, + text: 'Hello', + }, + }], + }, + }); + expect(JSON.parse(result.content[0].text)).toEqual({ + documentId: 'test-doc-id', + writeControl: {}, + }); + }); + + it('should handle errors during text insertion', async () => { + const apiError = new Error('API Error'); + mockDocsAPI.documents.batchUpdate.mockRejectedValue(apiError); + + const result = await docsService.insertText({ documentId: 'test-doc-id', text: 'Hello' }); + + expect(JSON.parse(result.content[0].text)).toEqual({ error: 'API Error' }); + }); + }); + + describe('find', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await docsService.initialize(); + }); + + it('should find documents with a given query', async () => { + const mockResponse = { + data: { + files: [{ id: 'test-doc-id', name: 'Test Document' }], + nextPageToken: 'next-page-token', + }, + }; + mockDriveAPI.files.list.mockResolvedValue(mockResponse); + + const result = await docsService.find({ query: 'Test' }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith(expect.objectContaining({ + q: expect.stringContaining("fullText contains 'Test'"), + })); + expect(JSON.parse(result.content[0].text)).toEqual({ + files: [{ id: 'test-doc-id', name: 'Test Document' }], + nextPageToken: 'next-page-token', + }); + }); + + it('should search by title when query starts with title:', async () => { + const mockResponse = { + data: { + files: [{ id: 'test-doc-id', name: 'Test Document' }], + }, + }; + mockDriveAPI.files.list.mockResolvedValue(mockResponse); + + const result = await docsService.find({ query: 'title:Test Document' }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith(expect.objectContaining({ + q: expect.stringContaining("name contains 'Test Document'"), + })); + expect(mockDriveAPI.files.list).toHaveBeenCalledWith(expect.objectContaining({ + q: expect.not.stringContaining("fullText contains"), + })); + expect(JSON.parse(result.content[0].text)).toEqual({ + files: [{ id: 'test-doc-id', name: 'Test Document' }], + }); + }); + + it('should handle errors during find', async () => { + const apiError = new Error('API Error'); + mockDriveAPI.files.list.mockRejectedValue(apiError); + + const result = await docsService.find({ query: 'Test' }); + + expect(JSON.parse(result.content[0].text)).toEqual({ error: 'API Error' }); + }); + }); + + describe('move', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await docsService.initialize(); + }); + + it('should move a document to a folder', async () => { + mockDriveService.findFolder.mockResolvedValue({ + content: [{ type: 'text', text: JSON.stringify([{ id: 'test-folder-id', name: 'Test Folder' }]) }], + }); + mockDriveAPI.files.get.mockResolvedValue({ data: { parents: ['root'] } }); + + const result = await docsService.move({ documentId: 'test-doc-id', folderName: 'Test Folder' }); + + expect(mockDriveService.findFolder).toHaveBeenCalledWith({ folderName: 'Test Folder' }); + expect(mockDriveAPI.files.update).toHaveBeenCalledWith({ + fileId: 'test-doc-id', + addParents: 'test-folder-id', + removeParents: 'root', + fields: 'id, parents', + }); + expect(result.content[0].text).toBe('Moved document test-doc-id to folder Test Folder'); + }); + + it('should handle errors during move', async () => { + const apiError = new Error('API Error'); + mockDriveService.findFolder.mockRejectedValue(apiError); + + const result = await docsService.move({ documentId: 'test-doc-id', folderName: 'Test Folder' }); + + expect(JSON.parse(result.content[0].text)).toEqual({ error: 'API Error' }); + }); + }); + + describe('getText', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await docsService.initialize(); + }); + + it('should extract text from a document', async () => { + const mockDoc = { + data: { + body: { + content: [ + { + paragraph: { + elements: [ + { + textRun: { + content: 'Hello World\n', + }, + }, + ], + }, + }, + ], + }, + }, + }; + mockDocsAPI.documents.get.mockResolvedValue(mockDoc); + + const result = await docsService.getText({ documentId: 'test-doc-id' }); + + expect(result.content[0].text).toBe('Hello World\n'); + }); + + it('should handle errors during getText', async () => { + const apiError = new Error('API Error'); + mockDocsAPI.documents.get.mockRejectedValue(apiError); + + const result = await docsService.getText({ documentId: 'test-doc-id' }); + + expect(JSON.parse(result.content[0].text)).toEqual({ error: 'API Error' }); + }); + }); + + describe('appendText', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await docsService.initialize(); + }); + + it('should append text to a document', async () => { + const mockDoc = { + data: { + body: { + content: [ + { + endIndex: 12, + }, + ], + }, + }, + }; + mockDocsAPI.documents.get.mockResolvedValue(mockDoc); + + const result = await docsService.appendText({ documentId: 'test-doc-id', text: ' Appended' }); + + expect(mockDocsAPI.documents.batchUpdate).toHaveBeenCalledWith({ + documentId: 'test-doc-id', + requestBody: { + requests: [{ + insertText: { + location: { index: 11 }, + text: ' Appended', + }, + }], + }, + }); + expect(result.content[0].text).toBe('Successfully appended text to document test-doc-id'); + }); + + it('should handle errors during appendText', async () => { + const apiError = new Error('API Error'); + mockDocsAPI.documents.get.mockRejectedValue(apiError); + + const result = await docsService.appendText({ documentId: 'test-doc-id', text: ' Appended' }); + + expect(JSON.parse(result.content[0].text)).toEqual({ error: 'API Error' }); + }); + }); + + describe('replaceText', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await docsService.initialize(); + }); + + it('should replace text in a document', async () => { + // Mock the document get call that finds occurrences + mockDocsAPI.documents.get.mockResolvedValue({ + data: { + body: { + content: [ + { + paragraph: { + elements: [ + { textRun: { content: 'Hello world! Hello again!' } } + ] + } + } + ] + } + } + }); + + mockDocsAPI.documents.batchUpdate.mockResolvedValue({ + data: { + documentId: 'test-doc-id', + replies: [] + } + }); + + const result = await docsService.replaceText({ documentId: 'test-doc-id', findText: 'Hello', replaceText: 'Hi' }); + + expect(mockDocsAPI.documents.get).toHaveBeenCalledWith({ + documentId: 'test-doc-id', + fields: 'body', + }); + + expect(mockDocsAPI.documents.batchUpdate).toHaveBeenCalledWith({ + documentId: 'test-doc-id', + requestBody: { + requests: expect.arrayContaining([ + expect.objectContaining({ + replaceAllText: { + replaceText: 'Hi', + containsText: { + text: 'Hello', + matchCase: true, + }, + }, + }) + ]), + }, + }); + expect(result.content[0].text).toBe('Successfully replaced text in document test-doc-id'); + }); + + it('should replace text with markdown formatting', async () => { + // Mock the document get call that finds occurrences + mockDocsAPI.documents.get.mockResolvedValue({ + data: { + body: { + content: [ + { + paragraph: { + elements: [ + { textRun: { content: 'Replace this text and this text too.' } } + ] + } + } + ] + } + } + }); + + mockDocsAPI.documents.batchUpdate.mockResolvedValue({ + data: { + documentId: 'test-doc-id', + replies: [] + } + }); + + const result = await docsService.replaceText({ + documentId: 'test-doc-id', + findText: 'this text', + replaceText: '**bold text**' + }); + + expect(mockDocsAPI.documents.batchUpdate).toHaveBeenCalledWith({ + documentId: 'test-doc-id', + requestBody: { + requests: expect.arrayContaining([ + expect.objectContaining({ + replaceAllText: { + replaceText: 'bold text', + containsText: { + text: 'this text', + matchCase: true, + }, + }, + }), + // Should have formatting requests for both occurrences + expect.objectContaining({ + updateTextStyle: expect.objectContaining({ + textStyle: expect.objectContaining({ + bold: true + }) + }) + }), + expect.objectContaining({ + updateTextStyle: expect.objectContaining({ + textStyle: expect.objectContaining({ + bold: true + }) + }) + }) + ]), + }, + }); + expect(result.content[0].text).toBe('Successfully replaced text in document test-doc-id'); + }); + + it('should handle errors during replaceText', async () => { + // Mock the document get call + mockDocsAPI.documents.get.mockResolvedValue({ + data: { + body: { + content: [ + { + paragraph: { + elements: [ + { textRun: { content: 'Hello world!' } } + ] + } + } + ] + } + } + }); + + const apiError = new Error('API Error'); + mockDocsAPI.documents.batchUpdate.mockRejectedValue(apiError); + + const result = await docsService.replaceText({ documentId: 'test-doc-id', findText: 'Hello', replaceText: 'Hi' }); + + expect(JSON.parse(result.content[0].text)).toEqual({ error: 'API Error' }); + }); + }); +}); diff --git a/workspace-mcp-server/src/__tests__/services/DriveService.test.ts b/workspace-mcp-server/src/__tests__/services/DriveService.test.ts new file mode 100644 index 00000000..1066f4f4 --- /dev/null +++ b/workspace-mcp-server/src/__tests__/services/DriveService.test.ts @@ -0,0 +1,652 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +import { DriveService } from '../../services/DriveService'; +import { AuthManager } from '../../auth/AuthManager'; +import { google } from 'googleapis'; + +// Mock the googleapis module +jest.mock('googleapis'); +jest.mock('../../utils/logger'); + +describe('DriveService', () => { + let driveService: DriveService; + let mockAuthManager: jest.Mocked; + let mockDriveAPI: any; + + beforeEach(() => { + // Clear all mocks before each test + jest.clearAllMocks(); + + // Create mock AuthManager + mockAuthManager = { + getAuthenticatedClient: jest.fn(), + loadSavedCredentialsIfExist: jest.fn(), + saveCredentials: jest.fn(), + authorize: jest.fn(), + } as any; + + // Create mock Drive API + mockDriveAPI = { + files: { + list: jest.fn(), + get: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }, + }; + + // Mock the google.drive constructor + (google.drive as jest.Mock) = jest.fn().mockReturnValue(mockDriveAPI); + + // Create DriveService instance + driveService = new DriveService(mockAuthManager); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('initialize', () => { + it('should initialize the Drive API client with authentication', async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + + await driveService.initialize(); + + expect(mockAuthManager.getAuthenticatedClient).toHaveBeenCalledTimes(1); + expect(google.drive).toHaveBeenCalledWith( + expect.objectContaining({ + version: 'v3', + auth: mockAuthClient, + }) + ); + }); + + it('should handle authentication errors', async () => { + const authError = new Error('Authentication failed'); + mockAuthManager.getAuthenticatedClient.mockRejectedValue(authError); + + await expect(driveService.initialize()).rejects.toThrow('Authentication failed'); + }); + }); + + describe('findFolder', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await driveService.initialize(); + }); + + it('should find folders by name', async () => { + const mockFolders = [ + { id: 'folder1', name: 'TestFolder' }, + { id: 'folder2', name: 'TestFolder' }, + ]; + + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: mockFolders, + }, + }); + + const result = await driveService.findFolder({ folderName: 'TestFolder' }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: "mimeType='application/vnd.google-apps.folder' and name = 'TestFolder'", + fields: 'files(id, name)', + spaces: 'drive', + }); + + expect(JSON.parse(result.content[0].text)).toEqual(mockFolders); + }); + + it('should return empty array when no folders found', async () => { + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: [], + }, + }); + + const result = await driveService.findFolder({ folderName: 'NonExistentFolder' }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledTimes(1); + expect(JSON.parse(result.content[0].text)).toEqual([]); + }); + + it('should handle API errors gracefully', async () => { + const apiError = new Error('API request failed'); + mockDriveAPI.files.list.mockRejectedValue(apiError); + + const result = await driveService.findFolder({ folderName: 'TestFolder' }); + + expect(JSON.parse(result.content[0].text)).toEqual({ error: 'API request failed' }); + }); + }); + + describe('search', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await driveService.initialize(); + }); + + it('should search files with custom query', async () => { + const mockFiles = [ + { id: 'file1', name: 'Document.pdf', modifiedTime: '2024-01-01T00:00:00Z' }, + { id: 'file2', name: 'Spreadsheet.xlsx', modifiedTime: '2024-01-02T00:00:00Z' }, + ]; + + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: mockFiles, + nextPageToken: 'next-token', + }, + }); + + const result = await driveService.search({ + query: "name contains 'Document'", + pageSize: 20, + }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: "name contains 'Document'", + pageSize: 20, + pageToken: undefined, + corpus: undefined, + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.files).toEqual(mockFiles); + expect(responseData.nextPageToken).toBe('next-token'); + }); + + it('should construct query if no field specifier is present', async () => { + const mockFiles = [ + { id: 'file1', name: 'Document.pdf', modifiedTime: '2024-01-01T00:00:00Z' }, + { id: 'file2', name: 'Spreadsheet.xlsx', modifiedTime: '2024-01-02T00:00:00Z' }, + ]; + + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: mockFiles, + nextPageToken: 'next-token', + }, + }); + + const result = await driveService.search({ + query: "My Document", + pageSize: 20, + }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: "fullText contains 'My Document'", + pageSize: 20, + pageToken: undefined, + corpus: undefined, + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.files).toEqual(mockFiles); + expect(responseData.nextPageToken).toBe('next-token'); + }); + + it('should escape special characters in search query', async () => { + const mockFiles = [ + { id: 'file1', name: "John's Report.pdf", modifiedTime: '2024-01-01T00:00:00Z' }, + ]; + + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: mockFiles, + }, + }); + + const result = await driveService.search({ + query: "John's \\Report", + pageSize: 10, + }); + + // Verify that single quotes and backslashes are properly escaped + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: "fullText contains 'John\\'s \\\\Report'", + pageSize: 10, + pageToken: undefined, + corpus: undefined, + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.files).toEqual(mockFiles); + }); + + it('should search by title when query starts with title:', async () => { + const mockFiles = [ + { id: 'file1', name: 'My Document.pdf', modifiedTime: '2024-01-01T00:00:00Z' }, + ]; + + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: mockFiles, + }, + }); + + const result = await driveService.search({ + query: 'title:My Document', + pageSize: 10, + }); + + // Should only search in name field when title: prefix is used + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: "name contains 'My Document'", + pageSize: 10, + pageToken: undefined, + corpus: undefined, + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.files).toEqual(mockFiles); + }); + + it('should handle quoted title searches', async () => { + const mockFiles = [ + { id: 'file1', name: 'Test Document', modifiedTime: '2024-01-01T00:00:00Z' }, + ]; + + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: mockFiles, + }, + }); + + const result = await driveService.search({ + query: 'title:"Test Document"', + pageSize: 10, + }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: "name contains 'Test Document'", + pageSize: 10, + pageToken: undefined, + corpus: undefined, + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.files).toEqual(mockFiles); + }); + + it('should handle sharedWithMe filter', async () => { + const mockFiles = [ + { id: 'shared1', name: 'SharedDoc.pdf', modifiedTime: '2024-01-01T00:00:00Z' }, + ]; + + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: mockFiles, + }, + }); + + const result = await driveService.search({ + sharedWithMe: true, + }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: 'sharedWithMe', + pageSize: 10, + pageToken: undefined, + corpus: undefined, + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.files).toEqual(mockFiles); + }); + + it('should filter unread files when unreadOnly is true', async () => { + const mockFiles = [ + { id: 'file1', name: 'ReadDoc.pdf', viewedByMeTime: '2024-01-01T00:00:00Z' }, + { id: 'file2', name: 'UnreadDoc.pdf', viewedByMeTime: null }, + { id: 'file3', name: 'UnreadSpreadsheet.xlsx' }, // No viewedByMeTime property + ]; + + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: mockFiles, + }, + }); + + const result = await driveService.search({ + query: 'type = "document"', + unreadOnly: true, + }); + + const responseData = JSON.parse(result.content[0].text); + // Should only include files without viewedByMeTime + expect(responseData.files).toHaveLength(2); + expect(responseData.files[0].id).toBe('file2'); + expect(responseData.files[1].id).toBe('file3'); + }); + + it('should use pagination token', async () => { + const mockFiles = [ + { id: 'file3', name: 'Page2Doc.pdf' }, + ]; + + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: mockFiles, + }, + }); + + await driveService.search({ + query: 'type = "document"', + pageToken: 'previous-token', + }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: 'type = "document"', + pageSize: 10, + pageToken: 'previous-token', + corpus: undefined, + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + }); + + it('should handle corpus parameter', async () => { + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: [], + }, + }); + + await driveService.search({ + query: 'type = "document"', + corpus: 'domain', + }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: 'type = "document"', + pageSize: 10, + pageToken: undefined, + corpus: 'domain', + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + }); + + it('should handle API errors gracefully', async () => { + const apiError = new Error('Search API failed'); + mockDriveAPI.files.list.mockRejectedValue(apiError); + + const result = await driveService.search({ + query: 'type = "document"', + }); + + expect(JSON.parse(result.content[0].text)).toEqual({ error: 'Search API failed' }); + }); + + it('should use default values when parameters are not provided', async () => { + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: [], + }, + }); + + await driveService.search({}); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: undefined, + pageSize: 10, + pageToken: undefined, + corpus: undefined, + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + }); + + it('should handle Google Drive folder URLs', async () => { + const mockFiles = [ + { id: 'folder123', name: 'My Folder', mimeType: 'application/vnd.google-apps.folder' }, + ]; + + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: mockFiles, + }, + }); + + const result = await driveService.search({ + query: 'https://drive.google.com/drive/folders/folder123', + pageSize: 10, + }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: "'folder123' in parents", + pageSize: 10, + pageToken: undefined, + corpus: undefined, + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.files).toEqual(mockFiles); + }); + + it('should handle corporate Google Drive folder URLs', async () => { + const mockFiles = [ + { id: 'file1', name: 'Document.pdf', mimeType: 'application/pdf' }, + { id: 'file2', name: 'Image.png', mimeType: 'image/png' }, + ]; + + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: mockFiles, + }, + }); + + const result = await driveService.search({ + query: 'https://drive.google.com/corp/drive/u/0/folders/1Ahs8C3GFWBZnrzQ44z0OR07hNQTWlE7u', + pageSize: 10, + }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: "'1Ahs8C3GFWBZnrzQ44z0OR07hNQTWlE7u' in parents", + pageSize: 10, + pageToken: undefined, + corpus: undefined, + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.files).toEqual(mockFiles); + }); + + it('should handle Google Drive file URLs', async () => { + const mockFile = { id: 'file456', name: 'My Document.pdf', mimeType: 'application/pdf' }; + + mockDriveAPI.files.get.mockResolvedValue({ + data: mockFile, + }); + + const result = await driveService.search({ + query: 'https://drive.google.com/file/d/file456/view', + pageSize: 10, + }); + + expect(mockDriveAPI.files.get).toHaveBeenCalledWith({ + fileId: 'file456', + fields: 'id, name, modifiedTime, viewedByMeTime, mimeType, parents', + }); + expect(mockDriveAPI.files.list).not.toHaveBeenCalled(); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.files).toEqual([mockFile]); + expect(responseData.nextPageToken).toBeNull(); + }); + + it('should handle Google Docs URLs', async () => { + const mockFile = { id: 'doc789', name: 'My Document', mimeType: 'application/vnd.google-apps.document' }; + + mockDriveAPI.files.get.mockResolvedValue({ + data: mockFile, + }); + + const result = await driveService.search({ + query: 'https://docs.google.com/document/d/doc789/edit', + pageSize: 10, + }); + + expect(mockDriveAPI.files.get).toHaveBeenCalledWith({ + fileId: 'doc789', + fields: 'id, name, modifiedTime, viewedByMeTime, mimeType, parents', + }); + expect(mockDriveAPI.files.list).not.toHaveBeenCalled(); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.files).toEqual([mockFile]); + expect(responseData.nextPageToken).toBeNull(); + }); + + it('should handle invalid Google Drive URLs', async () => { + const result = await driveService.search({ + query: 'https://drive.google.com/invalid/url', + pageSize: 10, + }); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.error).toBe('Invalid Drive URL. Please provide a valid Google Drive URL or a search query.'); + expect(responseData.details).toBe('Could not extract file or folder ID from the provided URL.'); + + // Should not call the API for invalid URLs + expect(mockDriveAPI.files.list).not.toHaveBeenCalled(); + }); + + it('should handle folder URLs with id parameter', async () => { + const mockFolder = { id: 'folder789', name: 'My Folder', mimeType: 'application/vnd.google-apps.folder' }; + const mockFiles = [ + { id: 'file1', name: 'Document.pdf', mimeType: 'application/pdf' }, + ]; + + mockDriveAPI.files.get.mockResolvedValue({ + data: mockFolder, + }); + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: mockFiles, + }, + }); + + const result = await driveService.search({ + query: 'https://drive.google.com/drive?id=folder789', + pageSize: 10, + }); + + expect(mockDriveAPI.files.get).toHaveBeenCalledWith({ + fileId: 'folder789', + fields: 'mimeType', + }); + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: "'folder789' in parents", + pageSize: 10, + pageToken: undefined, + corpus: undefined, + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.files).toEqual(mockFiles); + }); + + it('should handle file URLs with id parameter', async () => { + const mockFile = { id: 'file123', name: 'My File.pdf', mimeType: 'application/pdf' }; + + mockDriveAPI.files.get.mockResolvedValueOnce({ + data: { mimeType: 'application/pdf' }, + }).mockResolvedValueOnce({ + data: mockFile, + }); + + const result = await driveService.search({ + query: 'https://drive.google.com/drive?id=file123', + pageSize: 10, + }); + + expect(mockDriveAPI.files.get).toHaveBeenCalledWith({ + fileId: 'file123', + fields: 'mimeType', + }); + expect(mockDriveAPI.files.get).toHaveBeenCalledWith({ + fileId: 'file123', + fields: 'id, name, modifiedTime, viewedByMeTime, mimeType, parents', + }); + expect(mockDriveAPI.files.list).not.toHaveBeenCalled(); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.files).toEqual([mockFile]); + }); + + it('should handle raw Drive IDs as folder queries', async () => { + const mockFiles = [ + { id: 'file1', name: 'Document.pdf', mimeType: 'application/pdf' }, + { id: 'file2', name: 'Spreadsheet.xlsx', mimeType: 'application/vnd.google-apps.spreadsheet' }, + ]; + + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: mockFiles, + }, + }); + + const result = await driveService.search({ + query: '1Ahs8C3GFWBZnrzQ44z0OR07hNQTWlE7u', + pageSize: 10, + }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: "'1Ahs8C3GFWBZnrzQ44z0OR07hNQTWlE7u' in parents", + pageSize: 10, + pageToken: undefined, + corpus: undefined, + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.files).toEqual(mockFiles); + }); + + it('should not wrap a valid query in full-text search', async () => { + const mockFiles = [ + { id: 'file1', name: 'My File.pdf', mimeType: 'application/pdf' }, + ]; + + mockDriveAPI.files.list.mockResolvedValue({ + data: { + files: mockFiles, + }, + }); + + const result = await driveService.search({ + query: "'me' in owners", + pageSize: 10, + }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + q: "'me' in owners", + pageSize: 10, + pageToken: undefined, + corpus: undefined, + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + + const responseData = JSON.parse(result.content[0].text); + expect(responseData.files).toEqual(mockFiles); + }); + }); +}); \ No newline at end of file diff --git a/workspace-mcp-server/src/__tests__/services/GmailService.test.ts b/workspace-mcp-server/src/__tests__/services/GmailService.test.ts new file mode 100644 index 00000000..d1ce83b3 --- /dev/null +++ b/workspace-mcp-server/src/__tests__/services/GmailService.test.ts @@ -0,0 +1,753 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +import { GmailService } from '../../services/GmailService'; +import { AuthManager } from '../../auth/AuthManager'; +import { MimeHelper } from '../../utils/MimeHelper'; +import { google } from 'googleapis'; + +// Mock the modules +jest.mock('googleapis'); +jest.mock('../../utils/logger'); +jest.mock('../../utils/MimeHelper'); + +describe('GmailService', () => { + let gmailService: GmailService; + let mockAuthManager: jest.Mocked; + let mockGmailAPI: any; + + beforeEach(() => { + // Clear all mocks before each test + jest.clearAllMocks(); + + // Create mock AuthManager + mockAuthManager = { + getAuthenticatedClient: jest.fn(), + loadSavedCredentialsIfExist: jest.fn(), + saveCredentials: jest.fn(), + authorize: jest.fn(), + } as any; + + // Create mock Gmail API + mockGmailAPI = { + users: { + messages: { + list: jest.fn(), + get: jest.fn(), + send: jest.fn(), + trash: jest.fn(), + untrash: jest.fn(), + delete: jest.fn(), + modify: jest.fn(), + }, + drafts: { + create: jest.fn(), + send: jest.fn(), + list: jest.fn(), + get: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }, + labels: { + list: jest.fn(), + get: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }, + threads: { + list: jest.fn(), + get: jest.fn(), + }, + }, + }; + + // Mock the google.gmail constructor + (google.gmail as jest.Mock) = jest.fn().mockReturnValue(mockGmailAPI); + + // Create GmailService instance + gmailService = new GmailService(mockAuthManager); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('initialize', () => { + it('should initialize the Gmail API client', async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + + await gmailService.initialize(); + + expect(mockAuthManager.getAuthenticatedClient).toHaveBeenCalledTimes(1); + expect(google.gmail).toHaveBeenCalledWith( + expect.objectContaining({ + version: 'v1', + auth: mockAuthClient, + }) + ); + }); + }); + + describe('search', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await gmailService.initialize(); + }); + + it('should search for emails with query', async () => { + const mockMessages = [ + { id: 'msg1', threadId: 'thread1' }, + { id: 'msg2', threadId: 'thread2' }, + ]; + + mockGmailAPI.users.messages.list.mockResolvedValue({ + data: { + messages: mockMessages, + nextPageToken: 'next-token', + resultSizeEstimate: 100, + }, + }); + + const result = await gmailService.search({ + query: 'from:example@gmail.com', + maxResults: 10, + }); + + expect(mockGmailAPI.users.messages.list).toHaveBeenCalledWith({ + userId: 'me', + q: 'from:example@gmail.com', + maxResults: 10, + pageToken: undefined, + labelIds: undefined, + includeSpamTrash: false, + }); + + const response = JSON.parse(result.content[0].text); + expect(response.messages).toEqual(mockMessages); + expect(response.nextPageToken).toBe('next-token'); + expect(response.resultSizeEstimate).toBe(100); + }); + + it('should handle pagination with pageToken', async () => { + mockGmailAPI.users.messages.list.mockResolvedValue({ + data: { + messages: [], + nextPageToken: null, + }, + }); + + await gmailService.search({ + query: 'subject:Test', + pageToken: 'page-2', + }); + + expect(mockGmailAPI.users.messages.list).toHaveBeenCalledWith( + expect.objectContaining({ + pageToken: 'page-2', + }) + ); + }); + + it('should filter by labels', async () => { + mockGmailAPI.users.messages.list.mockResolvedValue({ + data: { + messages: [], + }, + }); + + await gmailService.search({ + labelIds: ['INBOX', 'UNREAD'], + }); + + expect(mockGmailAPI.users.messages.list).toHaveBeenCalledWith( + expect.objectContaining({ + labelIds: ['INBOX', 'UNREAD'], + }) + ); + }); + + it('should include spam and trash when specified', async () => { + mockGmailAPI.users.messages.list.mockResolvedValue({ + data: { + messages: [], + }, + }); + + await gmailService.search({ + includeSpamTrash: true, + }); + + expect(mockGmailAPI.users.messages.list).toHaveBeenCalledWith( + expect.objectContaining({ + includeSpamTrash: true, + }) + ); + }); + + it('should handle empty search results', async () => { + mockGmailAPI.users.messages.list.mockResolvedValue({ + data: { + messages: null, + resultSizeEstimate: 0, + }, + }); + + const result = await gmailService.search({}); + + const response = JSON.parse(result.content[0].text); + expect(response.messages).toEqual([]); + expect(response.resultSizeEstimate).toBe(0); + }); + + it('should handle API errors gracefully', async () => { + const apiError = new Error('Gmail API error'); + mockGmailAPI.users.messages.list.mockRejectedValue(apiError); + + const result = await gmailService.search({ query: 'test' }); + + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('Gmail API error'); + }); + }); + + describe('get', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await gmailService.initialize(); + }); + + it('should get a message by ID with full format', async () => { + const mockMessage = { + id: 'msg1', + threadId: 'thread1', + payload: { + headers: [ + { name: 'From', value: 'sender@example.com' }, + { name: 'To', value: 'recipient@example.com' }, + { name: 'Subject', value: 'Test Email' }, + ], + body: { + data: 'SGVsbG8gV29ybGQh', // Base64 for "Hello World!" + }, + }, + }; + + mockGmailAPI.users.messages.get.mockResolvedValue({ + data: mockMessage, + }); + + const result = await gmailService.get({ + messageId: 'msg1', + format: 'full', + }); + + expect(mockGmailAPI.users.messages.get).toHaveBeenCalledWith({ + userId: 'me', + id: 'msg1', + format: 'full', + }); + + const response = JSON.parse(result.content[0].text); + expect(response.id).toBe('msg1'); + expect(response.subject).toBe('Test Email'); + expect(response.from).toBe('sender@example.com'); + expect(response.to).toBe('recipient@example.com'); + }); + + it('should handle minimal format', async () => { + const mockMessage = { + id: 'msg1', + threadId: 'thread1', + snippet: 'This is a preview of the email...', + }; + + mockGmailAPI.users.messages.get.mockResolvedValue({ + data: mockMessage, + }); + + await gmailService.get({ + messageId: 'msg1', + format: 'minimal', + }); + + expect(mockGmailAPI.users.messages.get).toHaveBeenCalledWith({ + userId: 'me', + id: 'msg1', + format: 'minimal', + }); + }); + + it('should handle metadata format', async () => { + mockGmailAPI.users.messages.get.mockResolvedValue({ + data: { + id: 'msg1', + payload: { + headers: [ + { name: 'Subject', value: 'Test' }, + ], + }, + }, + }); + + await gmailService.get({ + messageId: 'msg1', + format: 'metadata', + }); + + expect(mockGmailAPI.users.messages.get).toHaveBeenCalledWith({ + userId: 'me', + id: 'msg1', + format: 'metadata', + }); + }); + + it('should handle API errors', async () => { + const apiError = new Error('Message not found'); + mockGmailAPI.users.messages.get.mockRejectedValue(apiError); + + const result = await gmailService.get({ messageId: 'invalid-id' }); + + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('Message not found'); + }); + }); + + describe('modify', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await gmailService.initialize(); + }); + + it('should add a label to a message', async () => { + mockGmailAPI.users.messages.modify.mockResolvedValue({ + data: { + id: 'msg1', + labelIds: ['Label_1'], + }, + }); + + const result = await gmailService.modify({ + messageId: 'msg1', + addLabelIds: ['Label_1'], + }); + + expect(mockGmailAPI.users.messages.modify).toHaveBeenCalledWith({ + userId: 'me', + id: 'msg1', + requestBody: { + addLabelIds: ['Label_1'], + removeLabelIds: [], + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response).toStrictEqual({ + id: 'msg1', + labelIds: ['Label_1'], + }); + }); + + it('should add multiple labels to a message', async () => { + mockGmailAPI.users.messages.modify.mockResolvedValue({ + data: { + id: 'msg1', + labelIds: ['Label_1', 'Label_2'], + }, + }); + + const result = await gmailService.modify({ + messageId: 'msg1', + addLabelIds: ['Label_1', 'Label_2'], + }); + + expect(mockGmailAPI.users.messages.modify).toHaveBeenCalledWith({ + userId: 'me', + id: 'msg1', + requestBody: { + addLabelIds: ['Label_1', 'Label_2'], + removeLabelIds: [], + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response).toStrictEqual({ + id: 'msg1', + labelIds: ['Label_1', 'Label_2'], + }); + }); + + it('should remove a label from a message', async () => { + mockGmailAPI.users.messages.modify.mockResolvedValue({ + data: { + id: 'msg1', + labelIds: ['Label_2'], + }, + }); + + const result = await gmailService.modify({ + messageId: 'msg1', + removeLabelIds: ['Label_1'], + }); + + expect(mockGmailAPI.users.messages.modify).toHaveBeenCalledWith({ + userId: 'me', + id: 'msg1', + requestBody: { + addLabelIds: [], + removeLabelIds: ['Label_1'], + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response).toStrictEqual({ + id: 'msg1', + labelIds: ['Label_2'], + }); + }); + + it('should remove multiple labels from a message', async () => { + mockGmailAPI.users.messages.modify.mockResolvedValue({ + data: { + id: 'msg1', + labelIds: [], + }, + }); + + const result = await gmailService.modify({ + messageId: 'msg1', + removeLabelIds: ['Label_1', 'Label_2'], + }); + + expect(mockGmailAPI.users.messages.modify).toHaveBeenCalledWith({ + userId: 'me', + id: 'msg1', + requestBody: { + addLabelIds: [], + removeLabelIds: ['Label_1', 'Label_2'], + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response).toStrictEqual({ + id: 'msg1', + labelIds: [], + }); + }); + + it('should add and remove labels on a message', async () => { + mockGmailAPI.users.messages.modify.mockResolvedValue({ + data: { + id: 'msg1', + labelIds: ['Label_1'], + }, + }); + + const result = await gmailService.modify({ + messageId: 'msg1', + addLabelIds: ['Label_1'], + removeLabelIds: ['Label_2'], + }); + + expect(mockGmailAPI.users.messages.modify).toHaveBeenCalledWith({ + userId: 'me', + id: 'msg1', + requestBody: { + addLabelIds: ['Label_1'], + removeLabelIds: ['Label_2'], + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response).toStrictEqual({ + id: 'msg1', + labelIds: ['Label_1'], + }); + }); + + it('should handle API errors', async () => { + const apiError = new Error('Message not found'); + mockGmailAPI.users.messages.modify.mockRejectedValue(apiError); + + const result = await gmailService.modify({ messageId: 'invalid-id' }); + + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('Message not found'); + }); + }); + + describe('send', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await gmailService.initialize(); + + // Mock MimeHelper + (MimeHelper.createMimeMessage as jest.Mock) = jest.fn().mockReturnValue('base64encodedmessage'); + }); + + it('should send an email with basic parameters', async () => { + const mockSentMessage = { + id: 'sent-msg-1', + threadId: 'thread1', + labelIds: ['SENT'], + }; + + mockGmailAPI.users.messages.send.mockResolvedValue({ + data: mockSentMessage, + }); + + const result = await gmailService.send({ + to: 'recipient@example.com', + subject: 'Test Subject', + body: 'Test Body', + }); + + expect(MimeHelper.createMimeMessage).toHaveBeenCalledWith({ + to: 'recipient@example.com', + subject: 'Test Subject', + body: 'Test Body', + from: undefined, + cc: undefined, + bcc: undefined, + replyTo: undefined, + isHtml: false, + }); + + expect(mockGmailAPI.users.messages.send).toHaveBeenCalledWith({ + userId: 'me', + requestBody: { + raw: 'base64encodedmessage', + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response.status).toBe('sent'); + expect(response.id).toBe('sent-msg-1'); + expect(response.threadId).toBe('thread1'); + expect(response.labelIds).toEqual(['SENT']); + }); + + it('should send email with multiple recipients', async () => { + mockGmailAPI.users.messages.send.mockResolvedValue({ + data: { id: 'sent-msg-2' }, + }); + + await gmailService.send({ + to: ['recipient1@example.com', 'recipient2@example.com'], + subject: 'Test', + body: 'Body', + cc: ['cc1@example.com', 'cc2@example.com'], + bcc: 'bcc@example.com', + }); + + expect(MimeHelper.createMimeMessage).toHaveBeenCalledWith({ + to: 'recipient1@example.com, recipient2@example.com', + subject: 'Test', + body: 'Body', + from: undefined, + cc: 'cc1@example.com, cc2@example.com', + bcc: 'bcc@example.com', + replyTo: undefined, + isHtml: false, + }); + }); + + it('should send HTML email', async () => { + mockGmailAPI.users.messages.send.mockResolvedValue({ + data: { id: 'sent-msg-3' }, + }); + + await gmailService.send({ + to: 'recipient@example.com', + subject: 'HTML Test', + body: '

Hello

', + isHtml: true, + }); + + expect(MimeHelper.createMimeMessage).toHaveBeenCalledWith( + expect.objectContaining({ + isHtml: true, + }) + ); + }); + + it('should handle send errors', async () => { + const apiError = new Error('Failed to send message'); + mockGmailAPI.users.messages.send.mockRejectedValue(apiError); + + const result = await gmailService.send({ + to: 'recipient@example.com', + subject: 'Test', + body: 'Body', + }); + + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('Failed to send message'); + }); + }); + + describe('createDraft', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await gmailService.initialize(); + + (MimeHelper.createMimeMessage as jest.Mock) = jest.fn().mockReturnValue('base64encodedmessage'); + }); + + it('should create a draft email', async () => { + const mockDraft = { + id: 'draft1', + message: { + id: 'msg1', + threadId: 'thread1', + }, + }; + + mockGmailAPI.users.drafts.create.mockResolvedValue({ + data: mockDraft, + }); + + const result = await gmailService.createDraft({ + to: 'recipient@example.com', + subject: 'Draft Subject', + body: 'Draft Body', + }); + + expect(mockGmailAPI.users.drafts.create).toHaveBeenCalledWith({ + userId: 'me', + requestBody: { + message: { + raw: 'base64encodedmessage', + }, + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response.status).toBe('draft_created'); + expect(response.id).toBe('draft1'); + expect(response.message.id).toBe('msg1'); + expect(response.message.threadId).toBe('thread1'); + }); + + it('should handle draft creation errors', async () => { + const apiError = new Error('Failed to create draft'); + mockGmailAPI.users.drafts.create.mockRejectedValue(apiError); + + const result = await gmailService.createDraft({ + to: 'recipient@example.com', + subject: 'Test', + body: 'Body', + }); + + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('Failed to create draft'); + }); + }); + + describe('sendDraft', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await gmailService.initialize(); + }); + + it('should send a draft', async () => { + const mockSentMessage = { + id: 'sent-msg-1', + threadId: 'thread1', + labelIds: ['SENT'], + }; + + mockGmailAPI.users.drafts.send.mockResolvedValue({ + data: mockSentMessage, + }); + + const result = await gmailService.sendDraft({ draftId: 'draft1' }); + + expect(mockGmailAPI.users.drafts.send).toHaveBeenCalledWith({ + userId: 'me', + requestBody: { + id: 'draft1', + }, + }); + + const response = JSON.parse(result.content[0].text); + expect(response.status).toBe('sent'); + expect(response.id).toBe('sent-msg-1'); + }); + + it('should handle send draft errors', async () => { + const apiError = new Error('Draft not found'); + mockGmailAPI.users.drafts.send.mockRejectedValue(apiError); + + const result = await gmailService.sendDraft({ draftId: 'invalid-draft' }); + + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('Draft not found'); + }); + }); + + describe('listLabels', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await gmailService.initialize(); + }); + + it('should list all labels', async () => { + const mockLabels = [ + { id: 'INBOX', name: 'INBOX', type: 'system' }, + { id: 'Label_1', name: 'Work', type: 'user' }, + { id: 'Label_2', name: 'Personal', type: 'user' }, + ]; + + mockGmailAPI.users.labels.list.mockResolvedValue({ + data: { + labels: mockLabels, + }, + }); + + const result = await gmailService.listLabels(); + + expect(mockGmailAPI.users.labels.list).toHaveBeenCalledWith({ + userId: 'me', + }); + + const response = JSON.parse(result.content[0].text); + expect(response.labels).toEqual(mockLabels); + }); + + it('should handle empty labels list', async () => { + mockGmailAPI.users.labels.list.mockResolvedValue({ + data: { + labels: null, + }, + }); + + const result = await gmailService.listLabels(); + + const response = JSON.parse(result.content[0].text); + expect(response.labels).toEqual([]); + }); + + it('should handle list labels errors', async () => { + const apiError = new Error('Failed to list labels'); + mockGmailAPI.users.labels.list.mockRejectedValue(apiError); + + const result = await gmailService.listLabels(); + + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('Failed to list labels'); + }); + }); +}); diff --git a/workspace-mcp-server/src/__tests__/services/PeopleService.test.ts b/workspace-mcp-server/src/__tests__/services/PeopleService.test.ts new file mode 100644 index 00000000..cd01ef78 --- /dev/null +++ b/workspace-mcp-server/src/__tests__/services/PeopleService.test.ts @@ -0,0 +1,104 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +import { PeopleService } from '../../services/PeopleService'; +import { AuthManager } from '../../auth/AuthManager'; +import { google } from 'googleapis'; + +// Mock the googleapis module +jest.mock('googleapis'); +jest.mock('../../utils/logger'); + +describe('PeopleService', () => { + let peopleService: PeopleService; + let mockAuthManager: jest.Mocked; + let mockPeopleAPI: any; + + beforeEach(() => { + // Clear all mocks before each test + jest.clearAllMocks(); + + // Create mock AuthManager + mockAuthManager = { + getAuthenticatedClient: jest.fn(), + } as any; + + // Create mock People API + mockPeopleAPI = { + people: { + get: jest.fn(), + }, + }; + + // Mock the google constructors + (google.people as jest.Mock) = jest.fn().mockReturnValue(mockPeopleAPI); + + // Create PeopleService instance + peopleService = new PeopleService(mockAuthManager); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('initialize', () => { + it('should initialize People API client', async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + + await peopleService.initialize(); + + expect(mockAuthManager.getAuthenticatedClient).toHaveBeenCalledTimes(1); + expect(google.people).toHaveBeenCalledWith( + expect.objectContaining({ + version: 'v1', + auth: mockAuthClient, + }) + ); + }); + }); + + describe('getUserProfile', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await peopleService.initialize(); + }); + + it('should return a user profile', async () => { + const mockUser = { + data: { + resourceName: 'people/110001608645105799644', + names: [{ + displayName: 'Test User', + }], + emailAddresses: [{ + value: 'test@example.com', + }], + }, + }; + mockPeopleAPI.people.get.mockResolvedValue(mockUser); + + const result = await peopleService.getUserProfile({ userId: '110001608645105799644' }); + + expect(mockPeopleAPI.people.get).toHaveBeenCalledWith({ + resourceName: 'people/110001608645105799644', + personFields: 'names,emailAddresses', + }); + expect(JSON.parse(result.content[0].text)).toEqual({ results: [{ person: mockUser.data }] }); + }); + + it('should handle errors during getUserProfile', async () => { + const apiError = new Error('API Error'); + mockPeopleAPI.people.get.mockRejectedValue(apiError); + + const result = await peopleService.getUserProfile({ userId: '110001608645105799644' }); + + expect(JSON.parse(result.content[0].text)).toEqual({ error: 'API Error' }); + }); + }); +}); diff --git a/workspace-mcp-server/src/__tests__/services/SheetsService.test.ts b/workspace-mcp-server/src/__tests__/services/SheetsService.test.ts new file mode 100644 index 00000000..e9333f0e --- /dev/null +++ b/workspace-mcp-server/src/__tests__/services/SheetsService.test.ts @@ -0,0 +1,458 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +import { SheetsService } from '../../services/SheetsService'; +import { AuthManager } from '../../auth/AuthManager'; +import { google } from 'googleapis'; + +// Mock the googleapis module +jest.mock('googleapis'); +jest.mock('../../utils/logger'); + +describe('SheetsService', () => { + let sheetsService: SheetsService; + let mockAuthManager: jest.Mocked; + let mockSheetsAPI: any; + let mockDriveAPI: any; + + beforeEach(() => { + // Clear all mocks before each test + jest.clearAllMocks(); + + // Create mock AuthManager + mockAuthManager = { + getAuthenticatedClient: jest.fn(), + } as any; + + + // Create mock Sheets API + mockSheetsAPI = { + spreadsheets: { + get: jest.fn(), + values: { + get: jest.fn(), + }, + }, + }; + + mockDriveAPI = { + files: { + list: jest.fn(), + }, + }; + + // Mock the google constructors + (google.sheets as jest.Mock) = jest.fn().mockReturnValue(mockSheetsAPI); + (google.drive as jest.Mock) = jest.fn().mockReturnValue(mockDriveAPI); + + // Create SheetsService instance + sheetsService = new SheetsService(mockAuthManager); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('initialize', () => { + it('should initialize Sheets and Drive API clients', async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + + await sheetsService.initialize(); + + expect(mockAuthManager.getAuthenticatedClient).toHaveBeenCalledTimes(1); + expect(google.sheets).toHaveBeenCalledWith( + expect.objectContaining({ + version: 'v4', + auth: mockAuthClient, + }) + ); + expect(google.drive).toHaveBeenCalledWith( + expect.objectContaining({ + version: 'v3', + auth: mockAuthClient, + }) + ); + }); + }); + + describe('getText', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await sheetsService.initialize(); + }); + + it('should extract text from a spreadsheet in default format', async () => { + const mockSpreadsheet = { + data: { + properties: { + title: 'Test Spreadsheet', + }, + sheets: [ + { properties: { title: 'Sheet1' } }, + { properties: { title: 'Sheet2' } }, + ], + }, + }; + + const mockSheet1Data = { + data: { + values: [ + ['Header1', 'Header2', 'Header3'], + ['Row1Col1', 'Row1Col2', 'Row1Col3'], + ['Row2Col1', 'Row2Col2', 'Row2Col3'], + ], + }, + }; + + const mockSheet2Data = { + data: { + values: [ + ['A', 'B'], + ['1', '2'], + ], + }, + }; + + mockSheetsAPI.spreadsheets.get.mockResolvedValue(mockSpreadsheet); + mockSheetsAPI.spreadsheets.values.get + .mockResolvedValueOnce(mockSheet1Data) + .mockResolvedValueOnce(mockSheet2Data); + + const result = await sheetsService.getText({ spreadsheetId: 'test-spreadsheet-id' }); + + expect(mockSheetsAPI.spreadsheets.get).toHaveBeenCalledWith({ + spreadsheetId: 'test-spreadsheet-id', + includeGridData: false, + }); + + expect(mockSheetsAPI.spreadsheets.values.get).toHaveBeenCalledTimes(2); + expect(mockSheetsAPI.spreadsheets.values.get).toHaveBeenNthCalledWith(1, { + spreadsheetId: 'test-spreadsheet-id', + range: "'Sheet1'", + }); + expect(mockSheetsAPI.spreadsheets.values.get).toHaveBeenNthCalledWith(2, { + spreadsheetId: 'test-spreadsheet-id', + range: "'Sheet2'", + }); + + expect(result.content[0].type).toBe('text'); + expect(result.content[0].text).toContain('Test Spreadsheet'); + expect(result.content[0].text).toContain('Sheet1'); + expect(result.content[0].text).toContain('Header1 | Header2 | Header3'); + expect(result.content[0].text).toContain('Sheet2'); + expect(result.content[0].text).toContain('A | B'); + }); + + it('should extract text in CSV format', async () => { + const mockSpreadsheet = { + data: { + properties: { + title: 'CSV Test', + }, + sheets: [ + { properties: { title: 'Sheet1' } }, + ], + }, + }; + + const mockSheetData = { + data: { + values: [ + ['Name', 'Age', 'City'], + ['John, Jr.', '25', 'New York'], + ['Jane', '30', 'San Francisco'], + ], + }, + }; + + mockSheetsAPI.spreadsheets.get.mockResolvedValue(mockSpreadsheet); + mockSheetsAPI.spreadsheets.values.get.mockResolvedValue(mockSheetData); + + const result = await sheetsService.getText({ + spreadsheetId: 'test-spreadsheet-id', + format: 'csv' + }); + + expect(result.content[0].type).toBe('text'); + expect(result.content[0].text).toContain('Name,Age,City'); + expect(result.content[0].text).toContain('"John, Jr.",25,New York'); + }); + + it('should extract text in JSON format', async () => { + const mockSpreadsheet = { + data: { + properties: { + title: 'JSON Test', + }, + sheets: [ + { properties: { title: 'Sheet1' } }, + ], + }, + }; + + const mockSheetData = { + data: { + values: [ + ['A', 'B'], + ['1', '2'], + ], + }, + }; + + mockSheetsAPI.spreadsheets.get.mockResolvedValue(mockSpreadsheet); + mockSheetsAPI.spreadsheets.values.get.mockResolvedValue(mockSheetData); + + const result = await sheetsService.getText({ + spreadsheetId: 'test-spreadsheet-id', + format: 'json' + }); + + expect(result.content[0].type).toBe('text'); + const jsonResult = JSON.parse(result.content[0].text); + expect(jsonResult.Sheet1).toEqual([['A', 'B'], ['1', '2']]); + }); + + it('should handle empty sheets', async () => { + const mockSpreadsheet = { + data: { + properties: { + title: 'Empty Test', + }, + sheets: [ + { properties: { title: 'EmptySheet' } }, + ], + }, + }; + + const mockSheetData = { + data: { + values: [], + }, + }; + + mockSheetsAPI.spreadsheets.get.mockResolvedValue(mockSpreadsheet); + mockSheetsAPI.spreadsheets.values.get.mockResolvedValue(mockSheetData); + + const result = await sheetsService.getText({ spreadsheetId: 'test-spreadsheet-id' }); + + expect(result.content[0].text).toContain('EmptySheet'); + expect(result.content[0].text).toContain('(Empty sheet)'); + }); + + it('should handle errors gracefully', async () => { + mockSheetsAPI.spreadsheets.get.mockRejectedValue(new Error('API Error')); + + const result = await sheetsService.getText({ spreadsheetId: 'error-spreadsheet-id' }); + + expect(result.content[0].type).toBe('text'); + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('API Error'); + }); + }); + + describe('getRange', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await sheetsService.initialize(); + }); + + it('should get values from a specific range', async () => { + const mockRangeData = { + data: { + range: 'Sheet1!A1:B3', + values: [ + ['A1', 'B1'], + ['A2', 'B2'], + ['A3', 'B3'], + ], + }, + }; + + mockSheetsAPI.spreadsheets.values.get.mockResolvedValue(mockRangeData); + + const result = await sheetsService.getRange({ + spreadsheetId: 'test-spreadsheet-id', + range: 'Sheet1!A1:B3' + }); + + expect(mockSheetsAPI.spreadsheets.values.get).toHaveBeenCalledWith({ + spreadsheetId: 'test-spreadsheet-id', + range: 'Sheet1!A1:B3', + }); + + const response = JSON.parse(result.content[0].text); + expect(response.range).toBe('Sheet1!A1:B3'); + expect(response.values).toHaveLength(3); + expect(response.values[0]).toEqual(['A1', 'B1']); + }); + + it('should handle empty ranges', async () => { + const mockRangeData = { + data: { + range: 'Sheet1!Z100:Z200', + values: [], + }, + }; + + mockSheetsAPI.spreadsheets.values.get.mockResolvedValue(mockRangeData); + + const result = await sheetsService.getRange({ + spreadsheetId: 'test-spreadsheet-id', + range: 'Sheet1!Z100:Z200' + }); + + const response = JSON.parse(result.content[0].text); + expect(response.values).toEqual([]); + }); + + it('should handle errors gracefully', async () => { + mockSheetsAPI.spreadsheets.values.get.mockRejectedValue(new Error('Range Error')); + + const result = await sheetsService.getRange({ + spreadsheetId: 'error-id', + range: 'InvalidRange' + }); + + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('Range Error'); + }); + }); + + describe('find', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await sheetsService.initialize(); + }); + + it('should find spreadsheets by query', async () => { + const mockResponse = { + data: { + files: [ + { id: 'sheet1', name: 'Spreadsheet 1' }, + { id: 'sheet2', name: 'Spreadsheet 2' }, + ], + nextPageToken: 'next-token', + }, + }; + + mockDriveAPI.files.list.mockResolvedValue(mockResponse); + + const result = await sheetsService.find({ query: 'budget' }); + const response = JSON.parse(result.content[0].text); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + pageSize: 10, + fields: 'nextPageToken, files(id, name)', + q: "mimeType='application/vnd.google-apps.spreadsheet' and fullText contains 'budget'", + pageToken: undefined, + }); + + expect(response.files).toHaveLength(2); + expect(response.files[0].name).toBe('Spreadsheet 1'); + expect(response.nextPageToken).toBe('next-token'); + }); + + it('should handle title-specific searches', async () => { + const mockResponse = { + data: { + files: [{ id: 'sheet1', name: 'Q4 Budget' }], + }, + }; + + mockDriveAPI.files.list.mockResolvedValue(mockResponse); + + const result = await sheetsService.find({ query: 'title:"Q4 Budget"' }); + const response = JSON.parse(result.content[0].text); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith( + expect.objectContaining({ + q: "mimeType='application/vnd.google-apps.spreadsheet' and name contains 'Q4 Budget'", + }) + ); + + expect(response.files).toHaveLength(1); + expect(response.files[0].name).toBe('Q4 Budget'); + }); + }); + + describe('getMetadata', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await sheetsService.initialize(); + }); + + it('should retrieve spreadsheet metadata', async () => { + const mockSpreadsheet = { + data: { + spreadsheetId: 'test-id', + properties: { + title: 'Test Spreadsheet', + locale: 'en_US', + timeZone: 'America/New_York', + }, + sheets: [ + { + properties: { + sheetId: 0, + title: 'Sheet1', + index: 0, + gridProperties: { + rowCount: 1000, + columnCount: 26, + }, + }, + }, + { + properties: { + sheetId: 1, + title: 'Sheet2', + index: 1, + gridProperties: { + rowCount: 500, + columnCount: 10, + }, + }, + }, + ], + }, + }; + + mockSheetsAPI.spreadsheets.get.mockResolvedValue(mockSpreadsheet); + + const result = await sheetsService.getMetadata({ spreadsheetId: 'test-id' }); + const metadata = JSON.parse(result.content[0].text); + + expect(mockSheetsAPI.spreadsheets.get).toHaveBeenCalledWith({ + spreadsheetId: 'test-id', + includeGridData: false, + }); + + expect(metadata.spreadsheetId).toBe('test-id'); + expect(metadata.title).toBe('Test Spreadsheet'); + expect(metadata.locale).toBe('en_US'); + expect(metadata.timeZone).toBe('America/New_York'); + expect(metadata.sheets).toHaveLength(2); + expect(metadata.sheets[0].title).toBe('Sheet1'); + expect(metadata.sheets[0].rowCount).toBe(1000); + expect(metadata.sheets[0].columnCount).toBe(26); + }); + + it('should handle errors gracefully', async () => { + mockSheetsAPI.spreadsheets.get.mockRejectedValue(new Error('Metadata Error')); + + const result = await sheetsService.getMetadata({ spreadsheetId: 'error-id' }); + const response = JSON.parse(result.content[0].text); + + expect(response.error).toBe('Metadata Error'); + }); + }); +}); diff --git a/workspace-mcp-server/src/__tests__/services/SlidesService.test.ts b/workspace-mcp-server/src/__tests__/services/SlidesService.test.ts new file mode 100644 index 00000000..afcbfd0e --- /dev/null +++ b/workspace-mcp-server/src/__tests__/services/SlidesService.test.ts @@ -0,0 +1,287 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +import { SlidesService } from '../../services/SlidesService'; +import { AuthManager } from '../../auth/AuthManager'; +import { google } from 'googleapis'; + +// Mock the googleapis module +jest.mock('googleapis'); +jest.mock('../../utils/logger'); + +describe('SlidesService', () => { + let slidesService: SlidesService; + let mockAuthManager: jest.Mocked; + let mockSlidesAPI: any; + let mockDriveAPI: any; + + beforeEach(() => { + // Clear all mocks before each test + jest.clearAllMocks(); + + // Create mock AuthManager + mockAuthManager = { + getAuthenticatedClient: jest.fn(), + } as any; + + + // Create mock Slides API + mockSlidesAPI = { + presentations: { + get: jest.fn(), + }, + }; + + mockDriveAPI = { + files: { + list: jest.fn(), + }, + }; + + // Mock the google constructors + (google.slides as jest.Mock) = jest.fn().mockReturnValue(mockSlidesAPI); + (google.drive as jest.Mock) = jest.fn().mockReturnValue(mockDriveAPI); + + // Create SlidesService instance + slidesService = new SlidesService(mockAuthManager); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('initialize', () => { + it('should initialize Slides and Drive API clients', async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + + await slidesService.initialize(); + + expect(mockAuthManager.getAuthenticatedClient).toHaveBeenCalledTimes(1); + expect(google.slides).toHaveBeenCalledWith( + expect.objectContaining({ + version: 'v1', + auth: mockAuthClient, + }) + ); + expect(google.drive).toHaveBeenCalledWith( + expect.objectContaining({ + version: 'v3', + auth: mockAuthClient, + }) + ); + }); + }); + + describe('getText', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await slidesService.initialize(); + }); + + it('should extract text from a presentation', async () => { + const mockPresentation = { + data: { + title: 'Test Presentation', + slides: [ + { + pageElements: [ + { + shape: { + text: { + textElements: [ + { textRun: { content: 'Slide 1 Title' } }, + { paragraphMarker: {} }, + { textRun: { content: 'Slide 1 Content' } }, + ], + }, + }, + }, + ], + }, + { + pageElements: [ + { + table: { + tableRows: [ + { + tableCells: [ + { + text: { + textElements: [ + { textRun: { content: 'Cell 1' } }, + ], + }, + }, + { + text: { + textElements: [ + { textRun: { content: 'Cell 2' } }, + ], + }, + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + }; + + mockSlidesAPI.presentations.get.mockResolvedValue(mockPresentation); + + const result = await slidesService.getText({ presentationId: 'test-presentation-id' }); + + expect(mockSlidesAPI.presentations.get).toHaveBeenCalledWith({ + presentationId: 'test-presentation-id', + fields: 'title,slides(pageElements(shape(text,shapeProperties),table(tableRows(tableCells(text)))))', + }); + + expect(result.content[0].type).toBe('text'); + expect(result.content[0].text).toContain('Test Presentation'); + expect(result.content[0].text).toContain('Slide 1 Title'); + expect(result.content[0].text).toContain('Slide 1 Content'); + expect(result.content[0].text).toContain('Cell 1 | Cell 2'); + }); + + it('should handle presentations with no slides', async () => { + const mockPresentation = { + data: { + title: 'Empty Presentation', + slides: [], + }, + }; + + mockSlidesAPI.presentations.get.mockResolvedValue(mockPresentation); + + const result = await slidesService.getText({ presentationId: 'empty-presentation-id' }); + + expect(result.content[0].type).toBe('text'); + expect(result.content[0].text).toContain('Empty Presentation'); + }); + + it('should handle errors gracefully', async () => { + mockSlidesAPI.presentations.get.mockRejectedValue(new Error('API Error')); + + const result = await slidesService.getText({ presentationId: 'error-presentation-id' }); + + expect(result.content[0].type).toBe('text'); + const response = JSON.parse(result.content[0].text); + expect(response.error).toBe('API Error'); + }); + }); + + describe('find', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await slidesService.initialize(); + }); + + it('should find presentations by query', async () => { + const mockResponse = { + data: { + files: [ + { id: 'pres1', name: 'Presentation 1' }, + { id: 'pres2', name: 'Presentation 2' }, + ], + nextPageToken: 'next-token', + }, + }; + + mockDriveAPI.files.list.mockResolvedValue(mockResponse); + + const result = await slidesService.find({ query: 'test query' }); + const response = JSON.parse(result.content[0].text); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ + pageSize: 10, + fields: 'nextPageToken, files(id, name)', + q: "mimeType='application/vnd.google-apps.presentation' and fullText contains 'test query'", + pageToken: undefined, + }); + + expect(response.files).toHaveLength(2); + expect(response.files[0].name).toBe('Presentation 1'); + expect(response.nextPageToken).toBe('next-token'); + }); + + it('should handle title-specific searches', async () => { + const mockResponse = { + data: { + files: [{ id: 'pres1', name: 'Specific Title' }], + }, + }; + + mockDriveAPI.files.list.mockResolvedValue(mockResponse); + + const result = await slidesService.find({ query: 'title:"Specific Title"' }); + const response = JSON.parse(result.content[0].text); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith( + expect.objectContaining({ + q: "mimeType='application/vnd.google-apps.presentation' and name contains 'Specific Title'", + }) + ); + + expect(response.files).toHaveLength(1); + expect(response.files[0].name).toBe('Specific Title'); + }); + }); + + describe('getMetadata', () => { + beforeEach(async () => { + const mockAuthClient = { access_token: 'test-token' }; + mockAuthManager.getAuthenticatedClient.mockResolvedValue(mockAuthClient as any); + await slidesService.initialize(); + }); + + it('should retrieve presentation metadata', async () => { + const mockPresentation = { + data: { + presentationId: 'test-id', + title: 'Test Presentation', + slides: [{ objectId: 'slide1' }, { objectId: 'slide2' }], + pageSize: { width: { magnitude: 10 }, height: { magnitude: 7.5 } }, + masters: [{ objectId: 'master1' }], + layouts: [{ objectId: 'layout1' }], + notesMaster: { objectId: 'notesMaster1' }, + }, + }; + + mockSlidesAPI.presentations.get.mockResolvedValue(mockPresentation); + + const result = await slidesService.getMetadata({ presentationId: 'test-id' }); + const metadata = JSON.parse(result.content[0].text); + + expect(mockSlidesAPI.presentations.get).toHaveBeenCalledWith({ + presentationId: 'test-id', + fields: 'presentationId,title,slides(objectId),pageSize,notesMaster,masters,layouts', + }); + + expect(metadata.presentationId).toBe('test-id'); + expect(metadata.title).toBe('Test Presentation'); + expect(metadata.slideCount).toBe(2); + expect(metadata.hasMasters).toBe(true); + expect(metadata.hasLayouts).toBe(true); + expect(metadata.hasNotesMaster).toBe(true); + }); + + it('should handle errors gracefully', async () => { + mockSlidesAPI.presentations.get.mockRejectedValue(new Error('Metadata Error')); + + const result = await slidesService.getMetadata({ presentationId: 'error-id' }); + const response = JSON.parse(result.content[0].text); + + expect(response.error).toBe('Metadata Error'); + }); + }); +}); diff --git a/workspace-mcp-server/src/__tests__/services/TimeService.test.ts b/workspace-mcp-server/src/__tests__/services/TimeService.test.ts new file mode 100644 index 00000000..2b3a8452 --- /dev/null +++ b/workspace-mcp-server/src/__tests__/services/TimeService.test.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TimeService } from '../../services/TimeService'; + +describe('TimeService', () => { + let timeService: TimeService; + const mockDate = new Date('2025-08-19T12:34:56Z'); + + beforeEach(() => { + timeService = new TimeService(); + jest.useFakeTimers(); + jest.setSystemTime(mockDate); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('getCurrentDate', () => { + it('should return the current date in YYYY-MM-DD format', async () => { + const result = await timeService.getCurrentDate(); + expect(result.content[0].text).toEqual(JSON.stringify({ date: '2025-08-19' })); + }); + }); + + describe('getCurrentTime', () => { + it('should return the current time in HH:MM:SS format', async () => { + const result = await timeService.getCurrentTime(); + expect(result.content[0].text).toEqual(JSON.stringify({ time: '12:34:56' })); + }); + }); + + describe('getTimeZone', () => { + it('should return the local timezone', async () => { + const result = await timeService.getTimeZone(); + const expectedTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + expect(result.content[0].text).toEqual(JSON.stringify({ timeZone: expectedTimeZone })); + }); + }); +}); diff --git a/workspace-mcp-server/src/__tests__/setup.ts b/workspace-mcp-server/src/__tests__/setup.ts new file mode 100644 index 00000000..7274d642 --- /dev/null +++ b/workspace-mcp-server/src/__tests__/setup.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Test setup file for Jest +// This file runs before all tests +import { jest } from '@jest/globals'; + +// Mock console methods to reduce noise in test output +global.console = { + ...console, + // Keep errors and warnings + error: jest.fn(console.error), + warn: jest.fn(console.warn), + // Silence other logs during tests unless explicitly needed + log: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +}; + +// Set test environment variables +process.env.NODE_ENV = 'test'; + +// Increase timeout for integration tests if needed +jest.setTimeout(10000); + +// Clean up after all tests +afterAll(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); +}); \ No newline at end of file diff --git a/workspace-mcp-server/src/__tests__/utils/DriveQueryBuilder.test.ts b/workspace-mcp-server/src/__tests__/utils/DriveQueryBuilder.test.ts new file mode 100644 index 00000000..ff4a78d7 --- /dev/null +++ b/workspace-mcp-server/src/__tests__/utils/DriveQueryBuilder.test.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from '@jest/globals'; +import { buildDriveSearchQuery, MIME_TYPES } from '../../utils/DriveQueryBuilder'; + +describe('DriveQueryBuilder', () => { + describe('buildDriveSearchQuery', () => { + it('should build fullText query for regular search', () => { + const query = buildDriveSearchQuery(MIME_TYPES.DOCUMENT, 'test query'); + expect(query).toBe("mimeType='application/vnd.google-apps.document' and fullText contains 'test query'"); + }); + + it('should build name query for title-prefixed search', () => { + const query = buildDriveSearchQuery(MIME_TYPES.PRESENTATION, 'title:My Presentation'); + expect(query).toBe("mimeType='application/vnd.google-apps.presentation' and name contains 'My Presentation'"); + }); + + it('should handle quoted title searches', () => { + const query = buildDriveSearchQuery(MIME_TYPES.SPREADSHEET, 'title:"Budget 2024"'); + expect(query).toBe("mimeType='application/vnd.google-apps.spreadsheet' and name contains 'Budget 2024'"); + }); + + it('should handle single-quoted title searches', () => { + const query = buildDriveSearchQuery(MIME_TYPES.DOCUMENT, "title:'Q4 Report'"); + expect(query).toBe("mimeType='application/vnd.google-apps.document' and name contains 'Q4 Report'"); + }); + + it('should escape special characters in query', () => { + const query = buildDriveSearchQuery(MIME_TYPES.DOCUMENT, "test's query\\path"); + expect(query).toBe("mimeType='application/vnd.google-apps.document' and fullText contains 'test\\'s query\\\\path'"); + }); + + it('should escape special characters in title search', () => { + const query = buildDriveSearchQuery(MIME_TYPES.PRESENTATION, "title:John's Presentation\\2024"); + expect(query).toBe("mimeType='application/vnd.google-apps.presentation' and name contains 'John\\'s Presentation\\\\2024'"); + }); + + it('should handle empty strings', () => { + const query = buildDriveSearchQuery(MIME_TYPES.SPREADSHEET, ''); + expect(query).toBe("mimeType='application/vnd.google-apps.spreadsheet' and fullText contains ''"); + }); + + it('should handle whitespace-only queries', () => { + const query = buildDriveSearchQuery(MIME_TYPES.DOCUMENT, ' '); + expect(query).toBe("mimeType='application/vnd.google-apps.document' and fullText contains ' '"); + }); + + it('should handle title prefix with whitespace', () => { + const query = buildDriveSearchQuery(MIME_TYPES.PRESENTATION, ' title: "My Doc" '); + expect(query).toBe("mimeType='application/vnd.google-apps.presentation' and name contains 'My Doc'"); + }); + + it('should work with all MIME types', () => { + expect(buildDriveSearchQuery(MIME_TYPES.DOCUMENT, 'test')) + .toContain('application/vnd.google-apps.document'); + expect(buildDriveSearchQuery(MIME_TYPES.PRESENTATION, 'test')) + .toContain('application/vnd.google-apps.presentation'); + expect(buildDriveSearchQuery(MIME_TYPES.SPREADSHEET, 'test')) + .toContain('application/vnd.google-apps.spreadsheet'); + expect(buildDriveSearchQuery(MIME_TYPES.FOLDER, 'test')) + .toContain('application/vnd.google-apps.folder'); + }); + }); + + describe('MIME_TYPES constants', () => { + it('should have correct MIME type values', () => { + expect(MIME_TYPES.DOCUMENT).toBe('application/vnd.google-apps.document'); + expect(MIME_TYPES.PRESENTATION).toBe('application/vnd.google-apps.presentation'); + expect(MIME_TYPES.SPREADSHEET).toBe('application/vnd.google-apps.spreadsheet'); + expect(MIME_TYPES.FOLDER).toBe('application/vnd.google-apps.folder'); + }); + }); +}); diff --git a/workspace-mcp-server/src/__tests__/utils/IdUtils.test.ts b/workspace-mcp-server/src/__tests__/utils/IdUtils.test.ts new file mode 100644 index 00000000..9e52d48e --- /dev/null +++ b/workspace-mcp-server/src/__tests__/utils/IdUtils.test.ts @@ -0,0 +1,116 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from '@jest/globals'; +import { extractDocId } from '../../utils/IdUtils'; + +describe('IdUtils', () => { + describe('extractDocId', () => { + it('should extract document ID from a full Google Docs URL', () => { + const url = 'https://docs.google.com/document/d/1a2b3c4d5e6f7g8h9i0j/edit'; + const result = extractDocId(url); + expect(result).toBe('1a2b3c4d5e6f7g8h9i0j'); + }); + + it('should extract document ID from URL with additional parameters', () => { + const url = 'https://docs.google.com/document/d/abc123-XYZ_789/edit?usp=sharing'; + const result = extractDocId(url); + expect(result).toBe('abc123-XYZ_789'); + }); + + it('should extract document ID from URL with preview path', () => { + const url = 'https://docs.google.com/document/d/test-doc-id-123/preview'; + const result = extractDocId(url); + expect(result).toBe('test-doc-id-123'); + }); + + it('should extract document ID from URL without protocol', () => { + const url = 'docs.google.com/document/d/my_document_id/view'; + const result = extractDocId(url); + expect(result).toBe('my_document_id'); + }); + + it('should return undefined when raw document ID is passed directly', () => { + const docId = '1a2b3c4d5e6f7g8h9i0j'; + const result = extractDocId(docId); + expect(result).toBeUndefined(); + }); + + it('should return undefined for document ID with underscores and hyphens', () => { + const docId = 'doc_id-with-special_chars_123'; + const result = extractDocId(docId); + expect(result).toBeUndefined(); + }); + + it('should return undefined if no pattern matches', () => { + const randomString = 'not a doc id or url'; + const result = extractDocId(randomString); + expect(result).toBeUndefined(); + }); + + it('should return undefined for empty string', () => { + const result = extractDocId(''); + expect(result).toBeUndefined(); + }); + + it('should extract from partial URL path', () => { + const partialPath = '/document/d/abc123xyz/'; + const result = extractDocId(partialPath); + expect(result).toBe('abc123xyz'); + }); + + it('should handle URL with multiple document paths (edge case)', () => { + // Should extract the first match + const url = '/document/d/first123/document/d/second456/'; + const result = extractDocId(url); + expect(result).toBe('first123'); + }); + + it('should handle very long document IDs', () => { + const longId = 'a'.repeat(100) + '_' + 'b'.repeat(50); + const url = `https://docs.google.com/document/d/${longId}/edit`; + const result = extractDocId(url); + expect(result).toBe(longId); + }); + + it('should handle document ID with only numbers', () => { + const url = 'https://docs.google.com/document/d/1234567890/edit'; + const result = extractDocId(url); + expect(result).toBe('1234567890'); + }); + + it('should handle document ID with only letters', () => { + const url = 'https://docs.google.com/document/d/abcdefghij/edit'; + const result = extractDocId(url); + expect(result).toBe('abcdefghij'); + }); + + it('should handle malformed URLs gracefully', () => { + const malformedUrl = 'https://docs.google.com/document/edit'; + const result = extractDocId(malformedUrl); + // Should return the input as-is when pattern doesn't match + expect(result).toBeUndefined(); + }); + + it('should be case sensitive for document IDs', () => { + const url = 'https://docs.google.com/document/d/AbCdEfGhIj/edit'; + const result = extractDocId(url); + expect(result).toBe('AbCdEfGhIj'); + }); + + it('should extract document ID from a complex URL with resourcekey', () => { + const url = 'https://docs.google.com/document/d/1MGqTbt5joTs40QS-YZTP9QH1-TxQ5tij7RgXPFWMPiI/edit?resourcekey=0-X_p2TPxpk0visLTHHMF7Yg&tab=t.0'; + const result = extractDocId(url); + expect(result).toBe('1MGqTbt5joTs40QS-YZTP9QH1-TxQ5tij7RgXPFWMPiI'); + }); + + it('should extract document ID from a URL without a trailing slash', () => { + const url = 'https://docs.google.com/document/d/1MGqTbt5joTs40QS-YZTP9QH1-TxQ5tij7RgXPFWMPiI'; + const result = extractDocId(url); + expect(result).toBe('1MGqTbt5joTs40QS-YZTP9QH1-TxQ5tij7RgXPFWMPiI'); + }); + }); +}); diff --git a/workspace-mcp-server/src/__tests__/utils/MimeHelper.test.ts b/workspace-mcp-server/src/__tests__/utils/MimeHelper.test.ts new file mode 100644 index 00000000..c5106b8d --- /dev/null +++ b/workspace-mcp-server/src/__tests__/utils/MimeHelper.test.ts @@ -0,0 +1,382 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from '@jest/globals'; +import { MimeHelper } from '../../utils/MimeHelper'; + +describe('MimeHelper', () => { + describe('createMimeMessage', () => { + it('should create a basic plain text email', () => { + const encoded = MimeHelper.createMimeMessage({ + to: 'recipient@example.com', + subject: 'Test Subject', + body: 'This is a test email body.', + }); + + // Decode the message to verify its structure + const decoded = MimeHelper.decodeBase64Url(encoded); + + expect(decoded).toContain('To: recipient@example.com'); + expect(decoded).toContain('Subject: =?utf-8?B?VGVzdCBTdWJqZWN0?='); + expect(decoded).toContain('Content-Type: text/plain; charset=utf-8'); + expect(decoded).toContain('This is a test email body.'); + }); + + it('should create an HTML email', () => { + const encoded = MimeHelper.createMimeMessage({ + to: 'recipient@example.com', + subject: 'HTML Email', + body: '

Hello World

This is HTML content.

', + isHtml: true, + }); + + const decoded = MimeHelper.decodeBase64Url(encoded); + + expect(decoded).toContain('Content-Type: text/html; charset=utf-8'); + expect(decoded).toContain('

Hello World

'); + }); + + it('should include optional headers when provided', () => { + const encoded = MimeHelper.createMimeMessage({ + to: 'recipient@example.com', + subject: 'Full Headers Test', + body: 'Test body', + from: 'sender@example.com', + cc: 'cc@example.com', + bcc: 'bcc@example.com', + replyTo: 'reply@example.com', + }); + + const decoded = MimeHelper.decodeBase64Url(encoded); + + expect(decoded).toContain('From: sender@example.com'); + expect(decoded).toContain('To: recipient@example.com'); + expect(decoded).toContain('Cc: cc@example.com'); + expect(decoded).toContain('Bcc: bcc@example.com'); + expect(decoded).toContain('Reply-To: reply@example.com'); + }); + + it('should handle UTF-8 subjects correctly', () => { + const encoded = MimeHelper.createMimeMessage({ + to: 'recipient@example.com', + subject: 'Test with emoji 🎉 and special chars é ñ', + body: 'Test body', + }); + + const decoded = MimeHelper.decodeBase64Url(encoded); + + // The subject should be base64 encoded + expect(decoded).toContain('Subject: =?utf-8?B?'); + + // Decode the subject to verify it's correct + const subjectMatch = decoded.match(/Subject: =\?utf-8\?B\?([^?]+)\?=/); + if (subjectMatch) { + const decodedSubject = Buffer.from(subjectMatch[1], 'base64').toString('utf-8'); + expect(decodedSubject).toBe('Test with emoji 🎉 and special chars é ñ'); + } + }); + + it('should properly format the MIME message with CRLF line endings', () => { + const encoded = MimeHelper.createMimeMessage({ + to: 'recipient@example.com', + subject: 'CRLF Test', + body: 'Line 1\nLine 2', + }); + + const decoded = MimeHelper.decodeBase64Url(encoded); + + // Should use CRLF (\r\n) as line separators + expect(decoded).toContain('\r\n'); + expect(decoded.split('\r\n').length).toBeGreaterThan(3); + }); + + it('should handle multiple recipients in to field', () => { + const encoded = MimeHelper.createMimeMessage({ + to: 'recipient1@example.com, recipient2@example.com', + subject: 'Multiple Recipients', + body: 'Test body', + }); + + const decoded = MimeHelper.decodeBase64Url(encoded); + + expect(decoded).toContain('To: recipient1@example.com, recipient2@example.com'); + }); + + it('should encode to base64url format (no padding, URL-safe characters)', () => { + const encoded = MimeHelper.createMimeMessage({ + to: 'recipient@example.com', + subject: 'Base64URL Test', + body: 'Test content that should be encoded', + }); + + // Check that it doesn't contain standard base64 characters + expect(encoded).not.toContain('+'); + expect(encoded).not.toContain('/'); + expect(encoded).not.toContain('='); + + // Should only contain base64url characters + expect(encoded).toMatch(/^[A-Za-z0-9\-_]+$/); + }); + }); + + describe('createMimeMessageWithAttachments', () => { + it('should create a message without attachments when none provided', () => { + const encoded = MimeHelper.createMimeMessageWithAttachments({ + to: 'recipient@example.com', + subject: 'No Attachments', + body: 'Simple message', + }); + + const decoded = MimeHelper.decodeBase64Url(encoded); + + // Should not contain multipart boundary + expect(decoded).not.toContain('Content-Type: multipart/mixed'); + expect(decoded).toContain('Content-Type: text/plain; charset=utf-8'); + }); + + it('should create a multipart message with attachments', () => { + const attachments = [ + { + filename: 'test.txt', + content: Buffer.from('Hello, World!'), + contentType: 'text/plain', + }, + ]; + + const encoded = MimeHelper.createMimeMessageWithAttachments({ + to: 'recipient@example.com', + subject: 'With Attachment', + body: 'Message with attachment', + attachments, + }); + + const decoded = MimeHelper.decodeBase64Url(encoded); + + expect(decoded).toContain('Content-Type: multipart/mixed; boundary='); + expect(decoded).toContain('Content-Disposition: attachment; filename="test.txt"'); + expect(decoded).toContain('Content-Type: text/plain'); + expect(decoded).toContain('Content-Transfer-Encoding: base64'); + }); + + it('should handle multiple attachments', () => { + const attachments = [ + { + filename: 'file1.txt', + content: 'First file content', + contentType: 'text/plain', + }, + { + filename: 'file2.pdf', + content: Buffer.from('PDF content'), + contentType: 'application/pdf', + }, + ]; + + const encoded = MimeHelper.createMimeMessageWithAttachments({ + to: 'recipient@example.com', + subject: 'Multiple Attachments', + body: 'Message with multiple attachments', + attachments, + }); + + const decoded = MimeHelper.decodeBase64Url(encoded); + + expect(decoded).toContain('filename="file1.txt"'); + expect(decoded).toContain('filename="file2.pdf"'); + expect(decoded).toContain('Content-Type: text/plain'); + expect(decoded).toContain('Content-Type: application/pdf'); + }); + + it('should use default content type for attachments without specified type', () => { + const attachments = [ + { + filename: 'unknown.bin', + content: Buffer.from('Binary content'), + }, + ]; + + const encoded = MimeHelper.createMimeMessageWithAttachments({ + to: 'recipient@example.com', + subject: 'Default Content Type', + body: 'Message', + attachments, + }); + + const decoded = MimeHelper.decodeBase64Url(encoded); + + expect(decoded).toContain('Content-Type: application/octet-stream'); + }); + + it('should properly format attachment content in 76-character lines', () => { + const longContent = 'a'.repeat(200); // Long content that needs to be wrapped + const attachments = [ + { + filename: 'long.txt', + content: Buffer.from(longContent), + }, + ]; + + const encoded = MimeHelper.createMimeMessageWithAttachments({ + to: 'recipient@example.com', + subject: 'Long Attachment', + body: 'Message', + attachments, + }); + + const decoded = MimeHelper.decodeBase64Url(encoded); + + // Find the base64 encoded attachment content + const lines = decoded.split('\r\n'); + const attachmentStart = lines.findIndex(line => + line.includes('Content-Transfer-Encoding: base64') + ); + + if (attachmentStart !== -1) { + // Check lines after the attachment header + for (let i = attachmentStart + 2; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith('--')) break; // Reached boundary + if (line.length > 0) { + expect(line.length).toBeLessThanOrEqual(76); + } + } + } + }); + + it('should handle HTML body with attachments', () => { + const attachments = [ + { + filename: 'doc.html', + content: 'HTML Doc', + contentType: 'text/html', + }, + ]; + + const encoded = MimeHelper.createMimeMessageWithAttachments({ + to: 'recipient@example.com', + subject: 'HTML with Attachment', + body: '

HTML Message Body

', + isHtml: true, + attachments, + }); + + const decoded = MimeHelper.decodeBase64Url(encoded); + + // Body should be HTML + expect(decoded).toMatch(/Content-Type: text\/html; charset=utf-8\r\n\r\n

HTML Message Body<\/p>/); + // Attachment should also be present + expect(decoded).toContain('filename="doc.html"'); + }); + + it('should include all optional headers with attachments', () => { + const attachments = [ + { + filename: 'test.txt', + content: 'Test', + }, + ]; + + const encoded = MimeHelper.createMimeMessageWithAttachments({ + to: 'recipient@example.com', + subject: 'Full Headers with Attachments', + body: 'Test body', + from: 'sender@example.com', + cc: 'cc@example.com', + bcc: 'bcc@example.com', + attachments, + }); + + const decoded = MimeHelper.decodeBase64Url(encoded); + + expect(decoded).toContain('From: sender@example.com'); + expect(decoded).toContain('Cc: cc@example.com'); + expect(decoded).toContain('Bcc: bcc@example.com'); + expect(decoded).toContain('MIME-Version: 1.0'); + }); + + it('should create unique boundary for each message', () => { + const attachments = [ + { + filename: 'test.txt', + content: 'Test', + }, + ]; + + const encoded1 = MimeHelper.createMimeMessageWithAttachments({ + to: 'recipient@example.com', + subject: 'Message 1', + body: 'Body 1', + attachments, + }); + + const encoded2 = MimeHelper.createMimeMessageWithAttachments({ + to: 'recipient@example.com', + subject: 'Message 2', + body: 'Body 2', + attachments, + }); + + const decoded1 = MimeHelper.decodeBase64Url(encoded1); + const decoded2 = MimeHelper.decodeBase64Url(encoded2); + + const boundary1Match = decoded1.match(/boundary="([^"]+)"/); + const boundary2Match = decoded2.match(/boundary="([^"]+)"/); + + expect(boundary1Match).toBeTruthy(); + expect(boundary2Match).toBeTruthy(); + expect(boundary1Match![1]).not.toBe(boundary2Match![1]); + }); + }); + + describe('decodeBase64Url', () => { + it('should decode base64url encoded strings', () => { + const original = 'Hello, World! This is a test.'; + const base64url = Buffer.from(original) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + + const decoded = MimeHelper.decodeBase64Url(base64url); + + expect(decoded).toBe(original); + }); + + it('should handle strings without padding', () => { + const base64url = 'SGVsbG8'; // "Hello" without padding + const decoded = MimeHelper.decodeBase64Url(base64url); + + expect(decoded).toBe('Hello'); + }); + + it('should convert URL-safe characters back to standard base64', () => { + const base64url = 'SGVsbG8-V29ybGRfIQ'; // Contains - and _ + const decoded = MimeHelper.decodeBase64Url(base64url); + + expect(decoded).toBeTruthy(); + expect(typeof decoded).toBe('string'); + }); + + it('should handle empty strings', () => { + const decoded = MimeHelper.decodeBase64Url(''); + + expect(decoded).toBe(''); + }); + + it('should properly decode a complete MIME message', () => { + const mimeMessage = MimeHelper.createMimeMessage({ + to: 'test@example.com', + subject: 'Test', + body: 'Test body', + }); + + const decoded = MimeHelper.decodeBase64Url(mimeMessage); + + expect(decoded).toContain('To: test@example.com'); + expect(decoded).toContain('Test body'); + }); + }); +}); \ No newline at end of file diff --git a/workspace-mcp-server/src/__tests__/utils/logger.test.ts b/workspace-mcp-server/src/__tests__/utils/logger.test.ts new file mode 100644 index 00000000..6d00ac1e --- /dev/null +++ b/workspace-mcp-server/src/__tests__/utils/logger.test.ts @@ -0,0 +1,235 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +import * as path from 'node:path'; + +// Mock fs/promises module BEFORE any imports that use it +jest.mock('fs/promises'); + +describe('logger', () => { + let consoleErrorSpy: any; + let logToFile: (message: string) => void; + let setLoggingEnabled: (enabled: boolean) => void; + let fs: any; + + async function setupLogger(appendFileMock?: any) { + jest.resetModules(); + jest.doMock('fs/promises', () => ({ + mkdir: jest.fn(() => Promise.resolve()), + appendFile: appendFileMock || jest.fn(() => Promise.resolve()), + })); + + fs = await import('node:fs/promises'); + const loggerModule = await import('../../utils/logger'); + logToFile = loggerModule.logToFile; + setLoggingEnabled = loggerModule.setLoggingEnabled; + setLoggingEnabled(true); + jest.clearAllMocks(); + } + + beforeEach(() => { + // Clear all mocks + jest.clearAllMocks(); + + // Clear module cache to ensure fresh imports + jest.resetModules(); + + // Spy on console.error + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('module initialization', () => { + it('should create log directory on module load', async () => { + // Set up mocks + jest.doMock('fs/promises', () => ({ + mkdir: jest.fn(() => Promise.resolve()), + appendFile: jest.fn(() => Promise.resolve()), + })); + + // Import the module (this triggers initialization) + await import('../../utils/logger'); + + // Get the mocked fs module + fs = await import('node:fs/promises'); + + // Wait for async initialization + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(fs.mkdir).toHaveBeenCalledWith( + expect.stringContaining('logs'), + { recursive: true } + ); + }); + + it('should handle directory creation errors gracefully', async () => { + const mkdirError = new Error('Permission denied'); + + // Set up mocks + jest.doMock('fs/promises', () => ({ + mkdir: jest.fn(() => Promise.reject(mkdirError)), + appendFile: jest.fn(() => Promise.resolve()), + })); + + // Import the module + await import('../../utils/logger'); + + // Wait for async initialization + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Could not create log directory:', + mkdirError + ); + }); + }); + + describe('logToFile', () => { + beforeEach(async () => { + await setupLogger(); + }); + + it('should append message with timestamp to log file', async () => { + const testMessage = 'Test log message'; + const mockDate = new Date('2024-01-01T12:00:00.000Z'); + jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any); + + logToFile(testMessage); + + // Wait for async operation + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(fs.appendFile).toHaveBeenCalledWith( + expect.stringContaining('server.log'), + '2024-01-01T12:00:00.000Z - Test log message\n' + ); + }); + + it('should handle multiple log messages', async () => { + logToFile('First message'); + logToFile('Second message'); + logToFile('Third message'); + + // Wait for async operations + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(fs.appendFile).toHaveBeenCalledTimes(3); + expect(fs.appendFile).toHaveBeenNthCalledWith( + 1, + expect.stringContaining('server.log'), + expect.stringContaining('First message') + ); + expect(fs.appendFile).toHaveBeenNthCalledWith( + 2, + expect.stringContaining('server.log'), + expect.stringContaining('Second message') + ); + expect(fs.appendFile).toHaveBeenNthCalledWith( + 3, + expect.stringContaining('server.log'), + expect.stringContaining('Third message') + ); + }); + + it('should log to console.error when file write fails', async () => { + const writeError = new Error('Disk full'); + await setupLogger(jest.fn(() => Promise.reject(writeError))); + + logToFile('Failed write test'); + + // Wait for async operation + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Failed to write to log file:', + writeError + ); + }); + + it('should format log message correctly', async () => { + const mockDate = new Date('2024-12-25T18:30:45.123Z'); + jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any); + + logToFile('Holiday log entry'); + + // Wait for async operation + await new Promise(resolve => setTimeout(resolve, 10)); + + const expectedMessage = '2024-12-25T18:30:45.123Z - Holiday log entry\n'; + expect(fs.appendFile).toHaveBeenCalledWith( + expect.any(String), + expectedMessage + ); + }); + + it('should handle empty messages', async () => { + const mockDate = new Date('2024-01-01T12:00:00.000Z'); + jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any); + logToFile(''); + + // Wait for async operation + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(fs.appendFile).toHaveBeenCalledWith( + expect.stringContaining('server.log'), + '2024-01-01T12:00:00.000Z - \n' + ); + }); + + it('should handle special characters in messages', async () => { + const specialMessage = 'Message with \n newline, \t tab, and "quotes"'; + + logToFile(specialMessage); + + // Wait for async operation + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(fs.appendFile).toHaveBeenCalledWith( + expect.stringContaining('server.log'), + expect.stringContaining(specialMessage) + ); + }); + + it('should use correct log file path', async () => { + logToFile('Path test'); + + // Wait for async operation + await new Promise(resolve => setTimeout(resolve, 10)); + + const callArgs = (fs.appendFile as jest.Mock).mock.calls[0]; + const logPath = callArgs[0] as string; + + expect(logPath).toContain('logs'); + expect(logPath).toContain('server.log'); + expect(path.isAbsolute(logPath)).toBe(true); + }); + + it('should not throw when appendFile fails', async () => { + await setupLogger(jest.fn(() => Promise.reject(new Error('Write failed')))); + + // Should not throw + expect(() => logToFile('Test message')).not.toThrow(); + + // Wait for async operation + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(consoleErrorSpy).toHaveBeenCalled(); + }); + + it('should not log when logging is disabled', () => { + setLoggingEnabled(false); + const testMessage = 'Test log message'; + + logToFile(testMessage); + + expect(fs.appendFile).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/workspace-mcp-server/src/__tests__/utils/markdownToDocsRequests.test.ts b/workspace-mcp-server/src/__tests__/utils/markdownToDocsRequests.test.ts new file mode 100644 index 00000000..4301be45 --- /dev/null +++ b/workspace-mcp-server/src/__tests__/utils/markdownToDocsRequests.test.ts @@ -0,0 +1,247 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from '@jest/globals'; +import { parseMarkdownToDocsRequests, processMarkdownLineBreaks } from '../../utils/markdownToDocsRequests'; + +describe('markdownToDocsRequests', () => { + describe('parseMarkdownToDocsRequests', () => { + // Skip tests that rely on marked working if it's not functioning in test environment + // We'll check markedWorks inside each test instead of using a variable + + it('should handle bold text', () => { + const markdown = 'This is **bold** text'; + const startIndex = 10; + const result = parseMarkdownToDocsRequests(markdown, startIndex); + + expect(result.plainText).toBe('This is bold text'); + expect(result.formattingRequests).toHaveLength(1); + expect(result.formattingRequests[0]).toEqual({ + updateTextStyle: { + range: { + startIndex: 18, // 10 + 8 (position of "bold") + endIndex: 22, // 10 + 12 (end of "bold") + }, + textStyle: { + bold: true, + }, + fields: 'bold' + } + }); + }); + + it('should handle italic text with asterisks', () => { + const markdown = 'This is *italic* text'; + const startIndex = 0; + const result = parseMarkdownToDocsRequests(markdown, startIndex); + + expect(result.plainText).toBe('This is italic text'); + expect(result.formattingRequests).toHaveLength(1); + expect(result.formattingRequests[0]).toEqual({ + updateTextStyle: { + range: { + startIndex: 8, + endIndex: 14, + }, + textStyle: { + italic: true, + }, + fields: 'italic' + } + }); + }); + + it('should handle italic text with underscores', () => { + const markdown = 'This is _italic_ text'; + const startIndex = 0; + const result = parseMarkdownToDocsRequests(markdown, startIndex); + + expect(result.plainText).toBe('This is italic text'); + expect(result.formattingRequests).toHaveLength(1); + expect(result.formattingRequests[0].updateTextStyle?.textStyle?.italic).toBe(true); + }); + + it('should handle inline code', () => { + const markdown = 'This is `code` text'; + const startIndex = 0; + const result = parseMarkdownToDocsRequests(markdown, startIndex); + + expect(result.plainText).toBe('This is code text'); + expect(result.formattingRequests).toHaveLength(1); + expect(result.formattingRequests[0]).toEqual({ + updateTextStyle: { + range: { + startIndex: 8, + endIndex: 12, + }, + textStyle: { + weightedFontFamily: { + fontFamily: 'Courier New', + weight: 400 + }, + backgroundColor: { + color: { + rgbColor: { + red: 0.95, + green: 0.95, + blue: 0.95 + } + } + } + }, + fields: 'weightedFontFamily,backgroundColor' + } + }); + }); + + it('should handle multiple formatting in one text', () => { + const markdown = 'Text with **bold**, *italic*, and `code` formatting'; + const startIndex = 0; + const result = parseMarkdownToDocsRequests(markdown, startIndex); + + expect(result.plainText).toBe('Text with bold, italic, and code formatting'); + expect(result.formattingRequests).toHaveLength(3); + }); + + it('should handle text with no formatting', () => { + const markdown = 'Plain text without any formatting'; + const startIndex = 0; + const result = parseMarkdownToDocsRequests(markdown, startIndex); + + expect(result.plainText).toBe('Plain text without any formatting'); + expect(result.formattingRequests).toHaveLength(0); + }); + + it('should handle overlapping formatting (keeps first)', () => { + const markdown = '**bold and text**'; + const startIndex = 0; + const result = parseMarkdownToDocsRequests(markdown, startIndex); + + // The bold formatting should be applied + expect(result.plainText).toBe('bold and text'); + expect(result.formattingRequests).toHaveLength(1); + expect(result.formattingRequests[0].updateTextStyle?.textStyle?.bold).toBe(true); + }); + + it('should respect the startIndex parameter', () => { + const markdown = '**bold**'; + const startIndex = 100; + const result = parseMarkdownToDocsRequests(markdown, startIndex); + + expect(result.plainText).toBe('bold'); + expect(result.formattingRequests[0]).toEqual({ + updateTextStyle: { + range: { + startIndex: 100, + endIndex: 104, + }, + textStyle: { + bold: true, + }, + fields: 'bold' + } + }); + }); + + it('should handle heading 1', () => { + const markdown = '# Main Title'; + const startIndex = 0; + const result = parseMarkdownToDocsRequests(markdown, startIndex); + + expect(result.plainText).toBe('Main Title'); + expect(result.formattingRequests).toHaveLength(1); + expect(result.formattingRequests[0].updateParagraphStyle?.paragraphStyle?.namedStyleType).toBe('HEADING_1'); + expect(result.formattingRequests[0].updateParagraphStyle?.range?.startIndex).toBe(0); + expect(result.formattingRequests[0].updateParagraphStyle?.range?.endIndex).toBe(10); + }); + + it('should handle heading 2', () => { + const markdown = '## Section Title'; + const startIndex = 0; + const result = parseMarkdownToDocsRequests(markdown, startIndex); + + expect(result.plainText).toBe('Section Title'); + expect(result.formattingRequests).toHaveLength(1); + expect(result.formattingRequests[0].updateParagraphStyle?.paragraphStyle?.namedStyleType).toBe('HEADING_2'); + expect(result.formattingRequests[0].updateParagraphStyle?.range?.startIndex).toBe(0); + expect(result.formattingRequests[0].updateParagraphStyle?.range?.endIndex).toBe(13); + }); + + it('should handle heading 3', () => { + const markdown = '### Subsection'; + const startIndex = 0; + const result = parseMarkdownToDocsRequests(markdown, startIndex); + + expect(result.plainText).toBe('Subsection'); + expect(result.formattingRequests).toHaveLength(1); + expect(result.formattingRequests[0].updateParagraphStyle?.paragraphStyle?.namedStyleType).toBe('HEADING_3'); + expect(result.formattingRequests[0].updateParagraphStyle?.range?.startIndex).toBe(0); + expect(result.formattingRequests[0].updateParagraphStyle?.range?.endIndex).toBe(10); + }); + + it('should handle mixed headings and text', () => { + const markdown = '# Title\n\nSome text\n\n## Section\n\nMore text'; + const startIndex = 0; + const result = parseMarkdownToDocsRequests(markdown, startIndex); + + expect(result.plainText).toContain('Title'); + expect(result.plainText).toContain('Some text'); + expect(result.plainText).toContain('Section'); + expect(result.plainText).toContain('More text'); + + // Should have formatting for both headings + const headingFormats = result.formattingRequests.filter(req => + req.updateParagraphStyle?.paragraphStyle?.namedStyleType !== undefined + ); + expect(headingFormats).toHaveLength(2); + }); + + it('should handle inline formatting within headings', () => { + const markdown = '# Main **bold** Title'; + const startIndex = 0; + const result = parseMarkdownToDocsRequests(markdown, startIndex); + + expect(result.plainText).toBe('Main bold Title'); + + // Should have both heading and bold formatting + const headingFormat = result.formattingRequests.find(req => + req.updateParagraphStyle?.paragraphStyle?.namedStyleType !== undefined + ); + const boldFormat = result.formattingRequests.find(req => + req.updateTextStyle?.textStyle?.bold === true + ); + + expect(headingFormat).toBeDefined(); + expect(boldFormat).toBeDefined(); + }); + }); + + describe('processMarkdownLineBreaks', () => { + it('should preserve single line breaks', () => { + const text = 'Line 1\nLine 2'; + const result = processMarkdownLineBreaks(text); + expect(result).toBe('Line 1\nLine 2'); + }); + + it('should convert double line breaks to double', () => { + const text = 'Paragraph 1\n\nParagraph 2'; + const result = processMarkdownLineBreaks(text); + expect(result).toBe('Paragraph 1\n\nParagraph 2'); + }); + + it('should convert multiple line breaks to double', () => { + const text = 'Paragraph 1\n\n\n\nParagraph 2'; + const result = processMarkdownLineBreaks(text); + expect(result).toBe('Paragraph 1\n\nParagraph 2'); + }); + + it('should handle text without line breaks', () => { + const text = 'Single line of text'; + const result = processMarkdownLineBreaks(text); + expect(result).toBe('Single line of text'); + }); + }); +}); \ No newline at end of file diff --git a/workspace-mcp-server/src/__tests__/utils/secure-browser-launcher.test.ts b/workspace-mcp-server/src/__tests__/utils/secure-browser-launcher.test.ts new file mode 100644 index 00000000..141e6214 --- /dev/null +++ b/workspace-mcp-server/src/__tests__/utils/secure-browser-launcher.test.ts @@ -0,0 +1,312 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + describe, + it, + expect, + beforeEach, + jest, + afterEach, +} from '@jest/globals'; +import { openBrowserSecurely } from '../../utils/secure-browser-launcher'; +import { platform } from 'node:os'; +import { EventEmitter } from 'node:events'; +import { ChildProcess } from 'node:child_process'; + +jest.mock('node:os'); + +const mockPlatform = platform as jest.Mock; + +describe('secure-browser-launcher', () => { + let mockChild: EventEmitter; + let mockExecFile: jest.Mock; + + beforeEach(() => { + mockChild = new EventEmitter(); + mockExecFile = jest.fn().mockReturnValue(mockChild as ChildProcess); + mockPlatform.mockReturnValue('darwin'); // Default to macOS + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + function simulateSuccess() { + process.nextTick(() => { + mockChild.emit('exit', 0); + }); + } + + function simulateFailure(error = new Error('Command failed')) { + process.nextTick(() => { + mockChild.emit('error', error); + }); + } + + describe('URL validation', () => { + it('should allow valid HTTP URLs', async () => { + const openPromise = openBrowserSecurely( + 'http://example.com', + mockExecFile as any + ); + simulateSuccess(); + await expect(openPromise).resolves.toBeUndefined(); + expect(mockExecFile).toHaveBeenCalledWith( + 'open', + ['http://example.com'], + expect.any(Object), + expect.any(Function) + ); + }); + + it('should allow valid HTTPS URLs', async () => { + const openPromise = openBrowserSecurely( + 'https://example.com', + mockExecFile as any + ); + simulateSuccess(); + await expect(openPromise).resolves.toBeUndefined(); + expect(mockExecFile).toHaveBeenCalledWith( + 'open', + ['https://example.com'], + expect.any(Object), + expect.any(Function) + ); + }); + + it('should reject non-HTTP(S) protocols', async () => { + await expect( + openBrowserSecurely('file:///etc/passwd', mockExecFile as any) + ).rejects.toThrow('Unsafe protocol'); + await expect( + openBrowserSecurely('javascript:alert(1)', mockExecFile as any) + ).rejects.toThrow('Unsafe protocol'); + await expect( + openBrowserSecurely('ftp://example.com', mockExecFile as any) + ).rejects.toThrow('Unsafe protocol'); + }); + + it('should reject invalid URLs', async () => { + await expect( + openBrowserSecurely('not-a-url', mockExecFile as any) + ).rejects.toThrow('Invalid URL'); + await expect( + openBrowserSecurely('', mockExecFile as any) + ).rejects.toThrow('Invalid URL'); + }); + + it('should reject URLs with control characters', async () => { + await expect( + openBrowserSecurely( + 'http://example.com\nmalicious-command', + mockExecFile as any + ) + ).rejects.toThrow('invalid characters'); + await expect( + openBrowserSecurely( + 'http://example.com\rmalicious-command', + mockExecFile as any + ) + ).rejects.toThrow('invalid characters'); + await expect( + openBrowserSecurely('http://example.com\x00', mockExecFile as any) + ).rejects.toThrow('invalid characters'); + }); + }); + + describe('Command injection prevention', () => { + it('should prevent PowerShell command injection on Windows', async () => { + mockPlatform.mockReturnValue('win32'); + const maliciousUrl = + "http://127.0.0.1:8080/?param=example#$(Invoke-Expression([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('Y2FsYy5leGU='))))"; + + const openPromise = openBrowserSecurely(maliciousUrl, mockExecFile as any); + simulateSuccess(); + await expect(openPromise).resolves.toBeUndefined(); + + expect(mockExecFile).toHaveBeenCalledWith( + 'powershell.exe', + [ + '-NoProfile', + '-NonInteractive', + '-WindowStyle', + 'Hidden', + '-Command', + `Start-Process '${maliciousUrl.replace(/'/g, "''")}'`, + ], + expect.any(Object), + expect.any(Function) + ); + }); + + it('should handle URLs with special shell characters safely', async () => { + const urlsWithSpecialChars = [ + 'http://example.com/path?param=value&other=$value', + 'http://example.com/path#fragment;command', + 'http://example.com/$(whoami)', + 'http://example.com/`command`', + 'http://example.com/|pipe', + 'http://example.com/>redirect', + ]; + + for (const url of urlsWithSpecialChars) { + const openPromise = openBrowserSecurely(url, mockExecFile as any); + simulateSuccess(); + await expect(openPromise).resolves.toBeUndefined(); + expect(mockExecFile).toHaveBeenCalledWith( + 'open', + [url], + expect.any(Object), + expect.any(Function) + ); + } + }); + + it('should properly escape single quotes in URLs on Windows', async () => { + mockPlatform.mockReturnValue('win32'); + const urlWithSingleQuotes = + "http://example.com/path?name=O'Brien&test='value'"; + + const openPromise = openBrowserSecurely( + urlWithSingleQuotes, + mockExecFile as any + ); + simulateSuccess(); + await expect(openPromise).resolves.toBeUndefined(); + + expect(mockExecFile).toHaveBeenCalledWith( + 'powershell.exe', + [ + '-NoProfile', + '-NonInteractive', + '-WindowStyle', + 'Hidden', + '-Command', + `Start-Process 'http://example.com/path?name=O''Brien&test=''value'''`, + ], + expect.any(Object), + expect.any(Function) + ); + }); + }); + + describe('Platform-specific behavior', () => { + it('should use correct command on macOS', async () => { + const openPromise = openBrowserSecurely( + 'https://example.com', + mockExecFile as any + ); + simulateSuccess(); + await expect(openPromise).resolves.toBeUndefined(); + expect(mockExecFile).toHaveBeenCalledWith( + 'open', + ['https://example.com'], + expect.any(Object), + expect.any(Function) + ); + }); + + it('should use PowerShell on Windows', async () => { + mockPlatform.mockReturnValue('win32'); + const openPromise = openBrowserSecurely( + 'https://example.com', + mockExecFile as any + ); + simulateSuccess(); + await expect(openPromise).resolves.toBeUndefined(); + expect(mockExecFile).toHaveBeenCalledWith( + 'powershell.exe', + [ + '-NoProfile', + '-NonInteractive', + '-WindowStyle', + 'Hidden', + '-Command', + `Start-Process 'https://example.com'`, + ], + expect.any(Object), + expect.any(Function) + ); + }); + + it('should use xdg-open on Linux', async () => { + mockPlatform.mockReturnValue('linux'); + const openPromise = openBrowserSecurely( + 'https://example.com', + mockExecFile as any + ); + simulateSuccess(); + await expect(openPromise).resolves.toBeUndefined(); + expect(mockExecFile).toHaveBeenCalledWith( + 'xdg-open', + ['https://example.com'], + expect.any(Object), + expect.any(Function) + ); + }); + + it('should throw on unsupported platforms', async () => { + mockPlatform.mockReturnValue('aix'); + await expect( + openBrowserSecurely('https://example.com', mockExecFile as any) + ).rejects.toThrow('Unsupported platform'); + }); + }); + + describe('Error handling', () => { + it('should handle browser launch failures gracefully', async () => { + const openPromise = openBrowserSecurely( + 'https://example.com', + mockExecFile as any + ); + simulateFailure(); + await expect(openPromise).rejects.toThrow('Failed to open browser'); + }); + + it('should try fallback browsers on Linux', async () => { + mockPlatform.mockReturnValue('linux'); + + const mockChild2 = new EventEmitter(); + mockExecFile.mockImplementationOnce(() => { + // Defer the emit call to allow the 'on' handlers to be set up. + process.nextTick(() => { + mockChild.emit('error', new Error('xdg-open not found')); + }); + return mockChild as ChildProcess; + }); + mockExecFile.mockImplementationOnce(() => { + process.nextTick(() => { + mockChild2.emit('exit', 0); + }); + return mockChild2 as ChildProcess; + }); + + const openPromise = openBrowserSecurely( + 'https://example.com', + mockExecFile as any + ); + + await expect(openPromise).resolves.toBeUndefined(); + + expect(mockExecFile).toHaveBeenCalledTimes(2); + expect(mockExecFile).toHaveBeenNthCalledWith( + 1, + 'xdg-open', + ['https://example.com'], + expect.any(Object), + expect.any(Function) + ); + expect(mockExecFile).toHaveBeenNthCalledWith( + 2, + 'gnome-open', + ['https://example.com'], + expect.any(Object), + expect.any(Function) + ); + }); + }); +}); \ No newline at end of file diff --git a/workspace-mcp-server/src/__tests__/utils/validation.test.ts b/workspace-mcp-server/src/__tests__/utils/validation.test.ts new file mode 100644 index 00000000..10fb1671 --- /dev/null +++ b/workspace-mcp-server/src/__tests__/utils/validation.test.ts @@ -0,0 +1,148 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from '@jest/globals'; +import { + validateEmail, + validateDateTime, + validateDocumentId, + extractDocumentId, + emailSchema, + emailArraySchema, + searchQuerySchema, + ValidationError +} from '../../utils/validation'; + +describe('Validation Utilities', () => { + describe('Email Validation', () => { + it('should validate correct email addresses', () => { + expect(validateEmail('user@example.com')).toEqual({ success: true }); + expect(validateEmail('john.doe+tag@company.co.uk')).toEqual({ success: true }); + }); + + it('should reject invalid email addresses', () => { + expect(validateEmail('invalid')).toMatchObject({ success: false }); + expect(validateEmail('@example.com')).toMatchObject({ success: false }); + expect(validateEmail('user@')).toMatchObject({ success: false }); + expect(validateEmail('user @example.com')).toMatchObject({ success: false }); + }); + + it('should handle email arrays', () => { + const result1 = emailSchema.safeParse('user@example.com'); + expect(result1.success).toBe(true); + + const result2 = emailSchema.safeParse(['user1@example.com', 'user2@example.com']); + expect(result2.success).toBe(false); // Single schema doesn't accept arrays + }); + + it('should validate emailArraySchema with single email', () => { + const result = emailArraySchema.safeParse('user@example.com'); + expect(result.success).toBe(true); + }); + + it('should validate emailArraySchema with array of emails', () => { + const result = emailArraySchema.safeParse(['user1@example.com', 'user2@example.com']); + expect(result.success).toBe(true); + }); + + it('should reject emailArraySchema with invalid emails in array', () => { + const result = emailArraySchema.safeParse(['valid@example.com', 'invalid-email']); + expect(result.success).toBe(false); + }); + }); + + describe('DateTime Validation', () => { + it('should validate correct ISO 8601 datetime formats', () => { + expect(validateDateTime('2024-01-15T10:30:00Z')).toEqual({ success: true }); + expect(validateDateTime('2024-01-15T10:30:00.000Z')).toEqual({ success: true }); + expect(validateDateTime('2024-01-15T10:30:00-05:00')).toEqual({ success: true }); + expect(validateDateTime('2024-01-15T10:30:00+09:30')).toEqual({ success: true }); + }); + + it('should reject invalid datetime formats', () => { + expect(validateDateTime('2024-01-15')).toMatchObject({ success: false }); + expect(validateDateTime('10:30:00')).toMatchObject({ success: false }); + expect(validateDateTime('2024-01-15 10:30:00')).toMatchObject({ success: false }); + expect(validateDateTime('not a date')).toMatchObject({ success: false }); + }); + + it('should reject invalid dates', () => { + expect(validateDateTime('2024-13-01T10:30:00Z')).toMatchObject({ success: false }); // Invalid month + // Note: JavaScript Date constructor accepts Feb 30 and converts it to March 1st or 2nd + // So this test would pass as valid. We'd need more complex validation for this. + expect(validateDateTime('2024-00-01T10:30:00Z')).toMatchObject({ success: false }); // Invalid month (0) + }); + }); + + describe('Document ID Validation', () => { + it('should validate correct document IDs', () => { + expect(validateDocumentId('1a2b3c4d5e6f7g8h9i0j')).toEqual({ success: true }); + expect(validateDocumentId('abc-123_XYZ')).toEqual({ success: true }); + expect(validateDocumentId('Document_ID-123')).toEqual({ success: true }); + }); + + it('should reject invalid document IDs', () => { + expect(validateDocumentId('doc id with spaces')).toMatchObject({ success: false }); + expect(validateDocumentId('doc#id')).toMatchObject({ success: false }); + expect(validateDocumentId('doc/id')).toMatchObject({ success: false }); + expect(validateDocumentId('')).toMatchObject({ success: false }); + }); + }); + + describe('Document ID Extraction', () => { + it('should extract ID from Google Docs URLs', () => { + const url = 'https://docs.google.com/document/d/1a2b3c4d5e6f/edit'; + expect(extractDocumentId(url)).toBe('1a2b3c4d5e6f'); + }); + + it('should extract ID from Google Drive URLs', () => { + const url = 'https://drive.google.com/file/d/abc123XYZ/view'; + expect(extractDocumentId(url)).toBe('abc123XYZ'); + }); + + it('should extract ID from Google Sheets URLs', () => { + const url = 'https://sheets.google.com/spreadsheets/d/sheet_id_123/edit'; + expect(extractDocumentId(url)).toBe('sheet_id_123'); + }); + + it('should return ID if already valid', () => { + const id = 'valid_document_id_123'; + expect(extractDocumentId(id)).toBe(id); + }); + + it('should throw error for invalid input', () => { + expect(() => extractDocumentId('not a valid url or id')).toThrow(); + expect(() => extractDocumentId('https://example.com/doc')).toThrow(); + }); + }); + + describe('Search Query Sanitization', () => { + it('should escape potentially dangerous characters', () => { + const result = searchQuerySchema.parse("test' OR '1'='1"); + expect(result).toBe("test\\' OR \\'1\\'=\\'1"); // Quotes are escaped + }); + + it('should escape quotes while preserving search functionality', () => { + const result = searchQuerySchema.parse('search for "exact phrase"'); + expect(result).toBe('search for \\"exact phrase\\"'); + }); + + it('should preserve safe characters', () => { + const result = searchQuerySchema.parse('test query with spaces and-dashes'); + expect(result).toBe('test query with spaces and-dashes'); + }); + }); + + describe('ValidationError', () => { + it('should create proper error with field and value', () => { + const error = new ValidationError('Invalid email', 'email', 'bad@'); + expect(error.message).toBe('Invalid email'); + expect(error.field).toBe('email'); + expect(error.value).toBe('bad@'); + expect(error.name).toBe('ValidationError'); + }); + }); +}); \ No newline at end of file diff --git a/workspace-mcp-server/src/auth/AuthManager.ts b/workspace-mcp-server/src/auth/AuthManager.ts new file mode 100644 index 00000000..5cfaa65a --- /dev/null +++ b/workspace-mcp-server/src/auth/AuthManager.ts @@ -0,0 +1,259 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { google, Auth } from 'googleapis'; +import crypto from 'node:crypto'; +import * as http from 'node:http'; +import * as net from 'node:net'; +import * as url from 'node:url'; +import { logToFile } from '../utils/logger'; +import open from '../utils/open-wrapper'; +import { shouldLaunchBrowser } from '../utils/secure-browser-launcher'; +import { OAuthCredentialStorage } from './token-storage/oauth-credential-storage'; + +// The Client ID for the OAuth flow. +// The secret is handled by the cloud function, not in the client. +const CLIENT_ID = '338689075775-o75k922vn5fdl18qergr96rp8g63e4d7.apps.googleusercontent.com'; + +/** + * An Authentication URL for updating the credentials of a Oauth2Client + * as well as a promise that will resolve when the credentials have + * been refreshed (or which throws error when refreshing credentials failed). + */ +interface OauthWebLogin { + authUrl: string; + loginCompletePromise: Promise; +} + +export class AuthManager { + private client: Auth.OAuth2Client | null = null; + private scopes: string[]; + + constructor(scopes: string[]) { + this.scopes = scopes; + } + + private async loadCachedCredentials(client: Auth.OAuth2Client): Promise { + const credentials = await OAuthCredentialStorage.loadCredentials(); + + if (credentials) { + // Check if saved token has required scopes + const savedScopes = new Set(credentials.scope?.split(' ') ?? []); + logToFile(`Cached token has scopes: ${[...savedScopes].join(', ')}`); + logToFile(`Required scopes: ${this.scopes.join(', ')}`); + + const missingScopes = this.scopes.filter(scope => !savedScopes.has(scope)); + + if (missingScopes.length > 0) { + logToFile(`Token cache missing required scopes: ${missingScopes.join(', ')}`); + logToFile('Removing cached token to force re-authentication...'); + await OAuthCredentialStorage.clearCredentials(); + return false; + } else { + client.setCredentials(credentials); + return true; + } + } + + return false; + } + + public async getAuthenticatedClient(): Promise { + logToFile('getAuthenticatedClient called'); + + // Check if we have a cached client with valid credentials + if (this.client && this.client.credentials && this.client.credentials.refresh_token) { + logToFile('Returning existing cached client with valid credentials'); + return this.client; + } + + // Note: No clientSecret is provided here. The secret is only known by the cloud function. + const options: Auth.OAuth2ClientOptions = { + clientId: CLIENT_ID, + }; + const oAuth2Client = new google.auth.OAuth2(options); + + logToFile('No valid cached client, checking for saved credentials...'); + if (await this.loadCachedCredentials(oAuth2Client)) { + logToFile('Loaded saved credentials, caching and returning client'); + this.client = oAuth2Client; + return this.client; + } + + const webLogin = await this.authWithWeb(oAuth2Client); + await open(webLogin.authUrl); + console.log('Waiting for authentication...'); + + // Add timeout to prevent infinite waiting when browser tab gets stuck + const authTimeout = 5 * 60 * 1000; // 5 minutes timeout + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject( + new Error( + 'Authentication timed out after 5 minutes. The browser tab may have gotten stuck in a loading state. ' + + 'Please try again.', + ), + ); + }, authTimeout); + }); + await Promise.race([webLogin.loginCompletePromise, timeoutPromise]); + + await OAuthCredentialStorage.saveCredentials(oAuth2Client.credentials); + this.client = oAuth2Client; + return this.client; + } + + private async getAvailablePort(): Promise { + return new Promise((resolve, reject) => { + let port = 0; + try { + const portStr = process.env['OAUTH_CALLBACK_PORT']; + if (portStr) { + port = parseInt(portStr, 10); + if (isNaN(port) || port <= 0 || port > 65535) { + return reject( + new Error(`Invalid value for OAUTH_CALLBACK_PORT: "${portStr}"`), + ); + } + return resolve(port); + } + const server = net.createServer(); + server.listen(0, () => { + const address = server.address()! as net.AddressInfo; + port = address.port; + }); + server.on('listening', () => { + server.close(); + server.unref(); + }); + server.on('error', (e) => reject(e)); + server.on('close', () => resolve(port)); + } catch (e) { + reject(e); + } + }); + } + + private async authWithWeb(client: Auth.OAuth2Client): Promise { + logToFile(`Requesting authentication with scopes: ${this.scopes.join(', ')}`); + + const port = await this.getAvailablePort(); + const host = process.env['OAUTH_CALLBACK_HOST'] || 'localhost'; + + const localRedirectUri = `http://${host}:${port}/oauth2callback`; + + const isGuiAvailable = shouldLaunchBrowser(); + + // SECURITY: Generate a random token for CSRF protection. + const csrfToken = crypto.randomBytes(32).toString('hex'); + + // The state now contains a JSON payload indicating the flow mode and CSRF token. + const statePayload = { + uri: isGuiAvailable ? localRedirectUri : undefined, + manual: !isGuiAvailable, + csrf: csrfToken, + }; + const state = Buffer.from(JSON.stringify(statePayload)).toString('base64'); + + // The redirect URI for Google's auth server is the cloud function + const cloudFunctionRedirectUri = 'https://google-workspace-extension.geminicli.com'; + + const authUrl = client.generateAuthUrl({ + redirect_uri: cloudFunctionRedirectUri, // Tell Google to go to the cloud function + access_type: 'offline', + scope: this.scopes, + state: state, // Pass our JSON payload in the state + prompt: 'consent', // Make sure we get a refresh token + }); + + const loginCompletePromise = new Promise((resolve, reject) => { + const server = http.createServer(async (req, res) => { + try { + // Use startsWith for more robust path checking. + if (!req.url || !req.url.startsWith('/oauth2callback')) { + res.end(); + reject( + new Error( + 'OAuth callback not received. Unexpected request: ' + req.url, + ), + ); + return; + } + + const qs = new url.URL(req.url, `http://${host}:${port}`) + .searchParams; + + // SECURITY: Validate the state parameter to prevent CSRF attacks. + const returnedState = qs.get('state'); + if (returnedState !== csrfToken) { + res.end('State mismatch. Possible CSRF attack.'); + reject(new Error('OAuth state mismatch. Possible CSRF attack.')); + return; + } + + if (qs.get('error')) { + const errorCode = qs.get('error'); + const errorDescription = + qs.get('error_description') || 'No additional details provided'; + res.end(); + reject( + new Error( + `Google OAuth error: ${errorCode}. ${errorDescription}`, + ), + ); + return; + } + + const access_token = qs.get('access_token'); + const refresh_token = qs.get('refresh_token'); + const scope = qs.get('scope'); + const token_type = qs.get('token_type'); + const expiry_date_str = qs.get('expiry_date'); + + if (access_token && expiry_date_str) { + const tokens: Auth.Credentials = { + access_token: access_token, + refresh_token: refresh_token || null, + scope: scope || undefined, + token_type: (token_type as 'Bearer') || undefined, + expiry_date: parseInt(expiry_date_str, 10), + }; + client.setCredentials(tokens); + res.end('Authentication successful! Please return to the console.'); + resolve(); + } else { + reject( + new Error( + 'Authentication failed: Did not receive tokens from callback.', + ), + ); + } + } catch (e) { + reject(e); + } finally { + server.close(); + } + }) + + server.listen(port, host, () => { + // Server started successfully + }); + + server.on('error', (err) => { + reject( + new Error( + `OAuth callback server error: ${err}`, + ), + ); + }); + }); + + return { + authUrl, + loginCompletePromise, + }; + } +} diff --git a/workspace-mcp-server/src/auth/token-storage/base-token-storage.ts b/workspace-mcp-server/src/auth/token-storage/base-token-storage.ts new file mode 100644 index 00000000..8a9c44f5 --- /dev/null +++ b/workspace-mcp-server/src/auth/token-storage/base-token-storage.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + OAuthCredentials, + TokenStorage +} from './types'; + +export abstract class BaseTokenStorage implements TokenStorage { + protected readonly serviceName: string; + + constructor(serviceName: string) { + this.serviceName = serviceName; + } + + abstract getCredentials(serverName: string): Promise; + abstract setCredentials(credentials: OAuthCredentials): Promise; + abstract deleteCredentials(serverName: string): Promise; + abstract listServers(): Promise; + abstract getAllCredentials(): Promise>; + abstract clearAll(): Promise; + + protected validateCredentials(credentials: OAuthCredentials): void { + if (!credentials.serverName) { + throw new Error('Server name is required'); + } + if (!credentials.token) { + throw new Error('Token is required'); + } + if (!credentials.token.accessToken && !credentials.token.refreshToken) { + throw new Error('Access token or refresh token is required'); + } + if (!credentials.token.tokenType) { + throw new Error('Token type is required'); + } + } + + + + protected sanitizeServerName(serverName: string): string { + return serverName.replace(/[^a-zA-Z0-9-_.]/g, '_'); + } +} \ No newline at end of file diff --git a/workspace-mcp-server/src/auth/token-storage/file-token-storage.ts b/workspace-mcp-server/src/auth/token-storage/file-token-storage.ts new file mode 100644 index 00000000..d0a590dc --- /dev/null +++ b/workspace-mcp-server/src/auth/token-storage/file-token-storage.ts @@ -0,0 +1,215 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { promises as fs } from 'node:fs'; +import * as path from 'node:path'; +import * as os from 'node:os'; +import * as crypto from 'node:crypto'; +import { BaseTokenStorage } from './base-token-storage'; +import type { OAuthCredentials } from './types'; +import { logToFile } from '../../utils/logger'; +import { + ENCRYPTED_TOKEN_PATH, + ENCRYPTION_MASTER_KEY_PATH, +} from '../../utils/paths'; + +export class FileTokenStorage extends BaseTokenStorage { + private readonly tokenFilePath: string; + private readonly encryptionKey: Buffer; + private readonly masterKey: Buffer; + + private constructor(serviceName: string, masterKey: Buffer) { + super(serviceName); + this.tokenFilePath = ENCRYPTED_TOKEN_PATH; + this.masterKey = masterKey; + this.encryptionKey = this.deriveEncryptionKey(); + } + + static async create(serviceName: string): Promise { + const masterKey = await this.loadMasterKey(); + return new FileTokenStorage(serviceName, masterKey); + } + + private static async loadMasterKey(): Promise { + try { + const masterKey = await fs.readFile(ENCRYPTION_MASTER_KEY_PATH); + return masterKey; + } catch (error) { + const err = error as NodeJS.ErrnoException; + if (err.code === 'ENOENT') { + const newKey = crypto.randomBytes(32); + await fs.writeFile(ENCRYPTION_MASTER_KEY_PATH, newKey, { mode: 0o600 }); + return newKey; + } + throw error; + } + } + + private deriveEncryptionKey(): Buffer { + const salt = `${os.hostname()}-${ + os.userInfo().username + }-gemini-cli-workspace`; + return crypto.scryptSync(this.masterKey, salt, 32); + } + + private encrypt(text: string): string { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv('aes-256-gcm', this.encryptionKey, iv); + + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + const authTag = cipher.getAuthTag(); + + return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted; + } + + private decrypt(encryptedData: string): string { + const parts = encryptedData.split(':'); + if (parts.length !== 3) { + throw new Error('Invalid encrypted data format'); + } + + const iv = Buffer.from(parts[0], 'hex'); + const authTag = Buffer.from(parts[1], 'hex'); + const encrypted = parts[2]; + + const decipher = crypto.createDecipheriv( + 'aes-256-gcm', + this.encryptionKey, + iv, + ); + decipher.setAuthTag(authTag); + + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; + } + + private async ensureDirectoryExists(): Promise { + const dir = path.dirname(this.tokenFilePath); + await fs.mkdir(dir, { recursive: true, mode: 0o700 }); + } + + private async loadTokens(): Promise> { + try { + const data = await fs.readFile(this.tokenFilePath, 'utf-8'); + const decrypted = this.decrypt(data); + const tokens = JSON.parse(decrypted) as Record; + return new Map(Object.entries(tokens)); + } catch (error: unknown) { + const err = error as NodeJS.ErrnoException & { message?: string }; + if (err.code === 'ENOENT') { + logToFile('Token file does not exist'); + return new Map(); + } + if ( + err.message?.includes('Invalid encrypted data format') || + err.message?.includes( + 'Unsupported state or unable to authenticate data', + ) + ) { + logToFile('Token file corrupted'); + return new Map(); + } + throw error; + } + } + + private async saveTokens( + tokens: Map, + ): Promise { + await this.ensureDirectoryExists(); + + const data = Object.fromEntries(tokens); + const json = JSON.stringify(data, null, 2); + const encrypted = this.encrypt(json); + + await fs.writeFile(this.tokenFilePath, encrypted, { mode: 0o600 }); + } + + async getCredentials(serverName: string): Promise { + const tokens = await this.loadTokens(); + const credentials = tokens.get(serverName); + + if (!credentials) { + return null; + } + + + + return credentials; + } + + async setCredentials(credentials: OAuthCredentials): Promise { + this.validateCredentials(credentials); + + const tokens = await this.loadTokens(); + const updatedCredentials: OAuthCredentials = { + ...credentials, + updatedAt: Date.now(), + }; + + tokens.set(credentials.serverName, updatedCredentials); + await this.saveTokens(tokens); + } + + async deleteCredentials(serverName: string): Promise { + const tokens = await this.loadTokens(); + + if (!tokens.has(serverName)) { + throw new Error(`No credentials found for ${serverName}`); + } + + tokens.delete(serverName); + + if (tokens.size === 0) { + try { + await fs.unlink(this.tokenFilePath); + } catch (error: unknown) { + const err = error as NodeJS.ErrnoException; + if (err.code !== 'ENOENT') { + throw error; + } + } + } else { + await this.saveTokens(tokens); + } + } + + async listServers(): Promise { + const tokens = await this.loadTokens(); + return Array.from(tokens.keys()); + } + + async getAllCredentials(): Promise> { + const tokens = await this.loadTokens(); + const result = new Map(); + + for (const [serverName, credentials] of tokens) { + try { + this.validateCredentials(credentials); + result.set(serverName, credentials); + } catch (error) { + console.error(`Skipping invalid credentials for ${serverName}:`, error); + } + } + + return result; + } + + async clearAll(): Promise { + try { + await fs.unlink(this.tokenFilePath); + } catch (error: unknown) { + const err = error as NodeJS.ErrnoException; + if (err.code !== 'ENOENT') { + throw error; + } + } + } +} \ No newline at end of file diff --git a/workspace-mcp-server/src/auth/token-storage/hybrid-token-storage.ts b/workspace-mcp-server/src/auth/token-storage/hybrid-token-storage.ts new file mode 100644 index 00000000..39f3962b --- /dev/null +++ b/workspace-mcp-server/src/auth/token-storage/hybrid-token-storage.ts @@ -0,0 +1,98 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BaseTokenStorage } from './base-token-storage'; +import { FileTokenStorage } from './file-token-storage'; +import type { TokenStorage, OAuthCredentials } from './types'; +import { TokenStorageType } from './types'; + +const FORCE_FILE_STORAGE_ENV_VAR = 'GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE'; + +export class HybridTokenStorage extends BaseTokenStorage { + private storage: TokenStorage | null = null; + private storageType: TokenStorageType | null = null; + private storageInitPromise: Promise | null = null; + + constructor(serviceName: string) { + super(serviceName); + } + + private async initializeStorage(): Promise { + const forceFileStorage = process.env[FORCE_FILE_STORAGE_ENV_VAR] === 'true'; + + if (!forceFileStorage) { + try { + const { KeychainTokenStorage } = await import( + './keychain-token-storage' + ); + const keychainStorage = new KeychainTokenStorage(this.serviceName); + + const isAvailable = await keychainStorage.isAvailable(); + if (isAvailable) { + this.storage = keychainStorage; + this.storageType = TokenStorageType.KEYCHAIN; + return this.storage; + } + } catch (e) { + // Fallback to file storage if keychain fails to initialize. + console.warn('Keychain initialization failed, falling back to file storage:', e); + } + } + + this.storage = await FileTokenStorage.create(this.serviceName); + this.storageType = TokenStorageType.ENCRYPTED_FILE; + return this.storage; + } + + private async getStorage(): Promise { + if (this.storage !== null) { + return this.storage; + } + + // Use a single initialization promise to avoid race conditions + if (!this.storageInitPromise) { + this.storageInitPromise = this.initializeStorage(); + } + + // Wait for initialization to complete + return await this.storageInitPromise; + } + + async getCredentials(serverName: string): Promise { + const storage = await this.getStorage(); + return storage.getCredentials(serverName); + } + + async setCredentials(credentials: OAuthCredentials): Promise { + const storage = await this.getStorage(); + await storage.setCredentials(credentials); + } + + async deleteCredentials(serverName: string): Promise { + const storage = await this.getStorage(); + await storage.deleteCredentials(serverName); + } + + async listServers(): Promise { + const storage = await this.getStorage(); + return storage.listServers(); + } + + async getAllCredentials(): Promise> { + const storage = await this.getStorage(); + return storage.getAllCredentials(); + } + + async clearAll(): Promise { + const storage = await this.getStorage(); + await storage.clearAll(); + } + + async getStorageType(): Promise { + await this.getStorage(); + return this.storageType!; + } +} \ No newline at end of file diff --git a/workspace-mcp-server/src/auth/token-storage/index.ts b/workspace-mcp-server/src/auth/token-storage/index.ts new file mode 100644 index 00000000..6bc6ced3 --- /dev/null +++ b/workspace-mcp-server/src/auth/token-storage/index.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './types'; +export * from './base-token-storage'; +export * from './file-token-storage'; +export * from './hybrid-token-storage'; diff --git a/workspace-mcp-server/src/auth/token-storage/keychain-token-storage.ts b/workspace-mcp-server/src/auth/token-storage/keychain-token-storage.ts new file mode 100644 index 00000000..c12bca3d --- /dev/null +++ b/workspace-mcp-server/src/auth/token-storage/keychain-token-storage.ts @@ -0,0 +1,249 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as crypto from 'node:crypto'; +import { BaseTokenStorage } from './base-token-storage'; +import type { OAuthCredentials } from './types'; + +interface Keytar { + getPassword(service: string, account: string): Promise; + setPassword( + service: string, + account: string, + password: string, + ): Promise; + deletePassword(service: string, account: string): Promise; + findCredentials( + service: string, + ): Promise>; +} + +const KEYCHAIN_TEST_PREFIX = '__keychain_test__'; + +export class KeychainTokenStorage extends BaseTokenStorage { + private keychainAvailable: boolean | null = null; + private keytarModule: Keytar | null = null; + private keytarLoadAttempted = false; + + async getKeytar(): Promise { + // If we've already tried loading (successfully or not), return the result + if (this.keytarLoadAttempted) { + return this.keytarModule; + } + + this.keytarLoadAttempted = true; + + try { + // Try to import keytar without any timeout - let the OS handle it + const moduleName = 'keytar'; + const module = await import(moduleName); + this.keytarModule = module.default || module; + } catch (error) { + + console.error(error); + } + return this.keytarModule; + } + + async getCredentials(serverName: string): Promise { + if (!(await this.checkKeychainAvailability())) { + throw new Error('Keychain is not available'); + } + + const keytar = await this.getKeytar(); + if (!keytar) { + throw new Error('Keytar module not available'); + } + + try { + const sanitizedName = this.sanitizeServerName(serverName); + const data = await keytar.getPassword(this.serviceName, sanitizedName); + + if (!data) { + return null; + } + + const credentials = JSON.parse(data) as OAuthCredentials; + + + + return credentials; + } catch (error) { + if (error instanceof SyntaxError) { + throw new Error(`Failed to parse stored credentials for ${serverName}`); + } + throw error; + } + } + + async setCredentials(credentials: OAuthCredentials): Promise { + if (!(await this.checkKeychainAvailability())) { + throw new Error('Keychain is not available'); + } + + const keytar = await this.getKeytar(); + if (!keytar) { + throw new Error('Keytar module not available'); + } + + this.validateCredentials(credentials); + + const sanitizedName = this.sanitizeServerName(credentials.serverName); + const updatedCredentials: OAuthCredentials = { + ...credentials, + updatedAt: Date.now(), + }; + + const data = JSON.stringify(updatedCredentials); + await keytar.setPassword(this.serviceName, sanitizedName, data); + } + + async deleteCredentials(serverName: string): Promise { + if (!(await this.checkKeychainAvailability())) { + throw new Error('Keychain is not available'); + } + + const keytar = await this.getKeytar(); + if (!keytar) { + throw new Error('Keytar module not available'); + } + + const sanitizedName = this.sanitizeServerName(serverName); + const deleted = await keytar.deletePassword( + this.serviceName, + sanitizedName, + ); + + if (!deleted) { + throw new Error(`No credentials found for ${serverName}`); + } + } + + async listServers(): Promise { + if (!(await this.checkKeychainAvailability())) { + throw new Error('Keychain is not available'); + } + + const keytar = await this.getKeytar(); + if (!keytar) { + throw new Error('Keytar module not available'); + } + + try { + const credentials = await keytar.findCredentials(this.serviceName); + return credentials + .filter((cred) => !cred.account.startsWith(KEYCHAIN_TEST_PREFIX)) + .map((cred: { account: string }) => cred.account); + } catch (error) { + console.error('Failed to list servers from keychain:', error); + return []; + } + } + + async getAllCredentials(): Promise> { + if (!(await this.checkKeychainAvailability())) { + throw new Error('Keychain is not available'); + } + + const keytar = await this.getKeytar(); + if (!keytar) { + throw new Error('Keytar module not available'); + } + + const result = new Map(); + try { + const credentials = ( + await keytar.findCredentials(this.serviceName) + ).filter((c) => !c.account.startsWith(KEYCHAIN_TEST_PREFIX)); + + for (const cred of credentials) { + try { + const data = JSON.parse(cred.password) as OAuthCredentials; + this.validateCredentials(data); + result.set(cred.account, data); + } catch (error) { + console.error( + `Failed to parse credentials for ${cred.account}:`, + error, + ); + } + } + } catch (error) { + console.error('Failed to get all credentials from keychain:', error); + } + + return result; + } + + async clearAll(): Promise { + if (!(await this.checkKeychainAvailability())) { + throw new Error('Keychain is not available'); + } + + const servers = this.keytarModule + ? await this.keytarModule + .findCredentials(this.serviceName) + .then((creds) => creds.map((c) => c.account)) + .catch((error: Error) => { + throw new Error( + `Failed to list servers for clearing: ${error.message}`, + ); + }) + : []; + const errors: Error[] = []; + + for (const server of servers) { + try { + await this.deleteCredentials(server); + } catch (error) { + errors.push(error as Error); + } + } + + if (errors.length > 0) { + throw new Error( + `Failed to clear some credentials: ${errors.map((e) => e.message).join(', ')}`, + ); + } + } + + // Checks whether or not a set-get-delete cycle with the keychain works. + // Returns false if any operation fails. + async checkKeychainAvailability(): Promise { + if (this.keychainAvailable !== null) { + return this.keychainAvailable; + } + + try { + const keytar = await this.getKeytar(); + if (!keytar) { + this.keychainAvailable = false; + return false; + } + + const testAccount = `${KEYCHAIN_TEST_PREFIX}${crypto.randomBytes(8).toString('hex')}`; + const testPassword = 'test'; + + await keytar.setPassword(this.serviceName, testAccount, testPassword); + const retrieved = await keytar.getPassword(this.serviceName, testAccount); + const deleted = await keytar.deletePassword( + this.serviceName, + testAccount, + ); + + const success = deleted && retrieved === testPassword; + this.keychainAvailable = success; + return success; + } catch (_error) { + this.keychainAvailable = false; + return false; + } + } + + async isAvailable(): Promise { + return this.checkKeychainAvailability(); + } +} \ No newline at end of file diff --git a/workspace-mcp-server/src/auth/token-storage/oauth-credential-storage.ts b/workspace-mcp-server/src/auth/token-storage/oauth-credential-storage.ts new file mode 100644 index 00000000..b314ea6b --- /dev/null +++ b/workspace-mcp-server/src/auth/token-storage/oauth-credential-storage.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { type Credentials } from 'google-auth-library'; +import { HybridTokenStorage } from './hybrid-token-storage'; +import type { OAuthCredentials } from './types'; + +const KEYCHAIN_SERVICE_NAME = 'gemini-cli-workspace-oauth'; +const MAIN_ACCOUNT_KEY = 'main-account'; + +export class OAuthCredentialStorage { + private static storage: HybridTokenStorage = new HybridTokenStorage( + KEYCHAIN_SERVICE_NAME, + ); + + /** + * Load cached OAuth credentials + */ + static async loadCredentials(): Promise { + try { + const credentials = await this.storage.getCredentials(MAIN_ACCOUNT_KEY); + + if (credentials?.token) { + const { accessToken, refreshToken, expiresAt, tokenType, scope } = + credentials.token; + // Convert from OAuthCredentials format to Google Credentials format + const googleCreds: Credentials = { + access_token: accessToken, + refresh_token: refreshToken || undefined, + token_type: tokenType || undefined, + scope: scope || undefined, + }; + + if (expiresAt) { + googleCreds.expiry_date = expiresAt; + } + + return googleCreds; + } + + return null; + } catch (error: unknown) { + throw error; + } + } + + /** + * Save OAuth credentials + */ + static async saveCredentials(credentials: Credentials): Promise { + // Convert Google Credentials to OAuthCredentials format + const mcpCredentials: OAuthCredentials = { + serverName: MAIN_ACCOUNT_KEY, + token: { + accessToken: credentials.access_token || undefined, + refreshToken: credentials.refresh_token || undefined, + tokenType: credentials.token_type || 'Bearer', + scope: credentials.scope || undefined, + expiresAt: credentials.expiry_date || undefined, + }, + updatedAt: Date.now(), + }; + + await this.storage.setCredentials(mcpCredentials); + } + + /** + * Clear cached OAuth credentials + */ + static async clearCredentials(): Promise { + try { + await this.storage.deleteCredentials(MAIN_ACCOUNT_KEY); + } catch (error: unknown) { + throw error; + } + } +} \ No newline at end of file diff --git a/workspace-mcp-server/src/auth/token-storage/types.ts b/workspace-mcp-server/src/auth/token-storage/types.ts new file mode 100644 index 00000000..83f46669 --- /dev/null +++ b/workspace-mcp-server/src/auth/token-storage/types.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Interface for OAuth tokens. + */ +export interface OAuthToken { + accessToken?: string; + refreshToken?: string; + expiresAt?: number; + tokenType: string; + scope?: string; +} + +/** + * Interface for stored OAuth credentials. + */ +export interface OAuthCredentials { + serverName: string; + token: OAuthToken; + clientId?: string; + tokenUrl?: string; + mcpServerUrl?: string; + updatedAt: number; +} + +export enum TokenStorageType { + KEYCHAIN = 'keychain', + ENCRYPTED_FILE = 'encrypted_file', +} + +export interface TokenStorage { + getCredentials(serverName: string): Promise; + setCredentials(credentials: OAuthCredentials): Promise; + deleteCredentials(serverName: string): Promise; + listServers(): Promise; + getAllCredentials(): Promise>; + clearAll(): Promise; +} \ No newline at end of file diff --git a/workspace-mcp-server/src/index.ts b/workspace-mcp-server/src/index.ts new file mode 100644 index 00000000..2141fc51 --- /dev/null +++ b/workspace-mcp-server/src/index.ts @@ -0,0 +1,647 @@ +#!/usr/bin/env node + +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from 'zod'; +import { AuthManager } from './auth/AuthManager'; +import { DocsService } from './services/DocsService'; +import { DriveService } from "./services/DriveService"; +import { CalendarService } from "./services/CalendarService"; +import { ChatService } from "./services/ChatService"; +import { GmailService } from "./services/GmailService"; +import { TimeService } from "./services/TimeService"; +import { PeopleService } from "./services/PeopleService"; +import { SlidesService } from "./services/SlidesService"; +import { SheetsService } from "./services/SheetsService"; +import { GMAIL_SEARCH_MAX_RESULTS } from "./utils/constants"; +import { extractDocId } from "./utils/IdUtils"; + +import { setLoggingEnabled } from "./utils/logger"; + +// Shared schemas for Gmail tools +const emailComposeSchema = { + to: z.union([z.string(), z.array(z.string())]).describe('Recipient email address(es).'), + subject: z.string().describe('Email subject.'), + body: z.string().describe('Email body content.'), + cc: z.union([z.string(), z.array(z.string())]).optional().describe('CC recipient email address(es).'), + bcc: z.union([z.string(), z.array(z.string())]).optional().describe('BCC recipient email address(es).'), + isHtml: z.boolean().optional().describe('Whether the body is HTML (default: false).'), +}; + +const SCOPES = [ + 'https://www.googleapis.com/auth/documents', + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/calendar', + 'https://www.googleapis.com/auth/chat.spaces', + 'https://www.googleapis.com/auth/chat.messages', + 'https://www.googleapis.com/auth/chat.memberships', + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/gmail.modify', + 'https://www.googleapis.com/auth/directory.readonly', + 'https://www.googleapis.com/auth/presentations.readonly', + 'https://www.googleapis.com/auth/spreadsheets.readonly', +]; + +async function main() { + // 1. Initialize services + if (process.argv.includes('--debug')) { + setLoggingEnabled(true); + } + + const authManager = new AuthManager(SCOPES); + const driveService = new DriveService(authManager); + await driveService.initialize(); + const docsService = new DocsService(authManager, driveService); + await docsService.initialize(); + const peopleService = new PeopleService(authManager); + await peopleService.initialize(); + const calendarService = new CalendarService(authManager); + await calendarService.initialize(); + const chatService = new ChatService(authManager); + await chatService.initialize(); + const gmailService = new GmailService(authManager); + await gmailService.initialize(); + const timeService = new TimeService(); + const slidesService = new SlidesService(authManager); + await slidesService.initialize(); + const sheetsService = new SheetsService(authManager); + await sheetsService.initialize(); + + // 2. Create the server instance + const server = new McpServer({ + name: "google-workspace-server", + version: "1.0.0", + }); + + // 3. Register tools directly on the server + server.registerTool( + "docs.create", + { + description: 'Creates a new Google Doc. Can be blank or with Markdown content.', + inputSchema: { + title: z.string().describe('The title for the new Google Doc.'), + folderName: z.string().optional().describe('The name of the folder to create the document in.'), + markdown: z.string().optional().describe('The Markdown content to create the document from.'), + } + }, + docsService.create + ); + + server.registerTool( + "docs.insertText", + { + description: 'Inserts text at the beginning of a Google Doc.', + inputSchema: { + documentId: z.string().describe('The ID of the document to modify.'), + text: z.string().describe('The text to insert at the beginning of the document.'), + } + }, + docsService.insertText + ); + + server.registerTool( + "docs.find", + { + description: 'Finds Google Docs by searching for a query in their title. Supports pagination.', + inputSchema: { + query: z.string().describe('The text to search for in the document titles.'), + pageToken: z.string().optional().describe('The token for the next page of results.'), + pageSize: z.number().optional().describe('The maximum number of results to return.'), + } + }, + docsService.find + ); + + server.registerTool( + "drive.findFolder", + { + description: 'Finds a folder by name in Google Drive.', + inputSchema: { + folderName: z.string().describe('The name of the folder to find.'), + } + }, + driveService.findFolder + ); + + server.registerTool( + "docs.move", + { + description: 'Moves a document to a specified folder.', + inputSchema: { + documentId: z.string().describe('The ID of the document to move.'), + folderName: z.string().describe('The name of the destination folder.'), + } + }, + docsService.move + ); + + server.registerTool( + "docs.getText", + { + description: 'Retrieves the text content of a Google Doc.', + inputSchema: { + documentId: z.string().describe('The ID of the document to read.'), + } + }, + docsService.getText + ); + + server.registerTool( + "docs.appendText", + { + description: 'Appends text to the end of a Google Doc.', + inputSchema: { + documentId: z.string().describe('The ID of the document to modify.'), + text: z.string().describe('The text to append to the document.'), + } + }, + docsService.appendText + ); + + server.registerTool( + "docs.replaceText", + { + description: 'Replaces all occurrences of a given text with new text in a Google Doc.', + inputSchema: { + documentId: z.string().describe('The ID of the document to modify.'), + findText: z.string().describe('The text to find in the document.'), + replaceText: z.string().describe('The text to replace the found text with.'), + } + }, + docsService.replaceText + ); + + server.registerTool( + "docs.extractIdFromUrl", + { + description: 'Extracts the document ID from a Google Workspace URL.', + inputSchema: { + url: z.string().describe('The URL of the Google Workspace document.'), + } + }, + async (input: { url: string }) => { + const result = extractDocId(input.url); + return { + content: [{ + type: "text" as const, + text: result || '' + }] + }; + } + ); + + // Slides tools + server.registerTool( + "slides.getText", + { + description: 'Retrieves the text content of a Google Slides presentation.', + inputSchema: { + presentationId: z.string().describe('The ID or URL of the presentation to read.'), + } + }, + slidesService.getText + ); + + server.registerTool( + "slides.find", + { + description: 'Finds Google Slides presentations by searching for a query. Supports pagination.', + inputSchema: { + query: z.string().describe('The text to search for in presentations.'), + pageToken: z.string().optional().describe('The token for the next page of results.'), + pageSize: z.number().optional().describe('The maximum number of results to return.'), + } + }, + slidesService.find + ); + + server.registerTool( + "slides.getMetadata", + { + description: 'Gets metadata about a Google Slides presentation.', + inputSchema: { + presentationId: z.string().describe('The ID or URL of the presentation.'), + } + }, + slidesService.getMetadata + ); + + // Sheets tools + server.registerTool( + "sheets.getText", + { + description: 'Retrieves the content of a Google Sheets spreadsheet.', + inputSchema: { + spreadsheetId: z.string().describe('The ID or URL of the spreadsheet to read.'), + format: z.enum(['text', 'csv', 'json']).optional().describe('Output format (default: text).'), + } + }, + sheetsService.getText + ); + + server.registerTool( + "sheets.getRange", + { + description: 'Gets values from a specific range in a Google Sheets spreadsheet.', + inputSchema: { + spreadsheetId: z.string().describe('The ID or URL of the spreadsheet.'), + range: z.string().describe('The A1 notation range to get (e.g., "Sheet1!A1:B10").'), + } + }, + sheetsService.getRange + ); + + server.registerTool( + "sheets.find", + { + description: 'Finds Google Sheets spreadsheets by searching for a query. Supports pagination.', + inputSchema: { + query: z.string().describe('The text to search for in spreadsheets.'), + pageToken: z.string().optional().describe('The token for the next page of results.'), + pageSize: z.number().optional().describe('The maximum number of results to return.'), + } + }, + sheetsService.find + ); + + server.registerTool( + "sheets.getMetadata", + { + description: 'Gets metadata about a Google Sheets spreadsheet.', + inputSchema: { + spreadsheetId: z.string().describe('The ID or URL of the spreadsheet.'), + } + }, + sheetsService.getMetadata + ); + + server.registerTool( + "drive.search", + { + description: 'Searches for files and folders in Google Drive. The query can be a simple search term, a Google Drive URL, or a full query string. For more information on query strings see: https://developers.google.com/drive/api/guides/search-files', + inputSchema: { + query: z.string().optional().describe('A simple search term (e.g., "Budget Q3"), a Google Drive URL, or a full query string (e.g., "name contains \'Budget\' and owners in \'user@example.com\'").'), + pageSize: z.number().optional().describe('The maximum number of results to return.'), + pageToken: z.string().optional().describe('The token for the next page of results.'), + corpus: z.string().optional().describe('The corpus of files to search (e.g., "user", "domain").'), + unreadOnly: z.boolean().optional().describe('Whether to filter for unread files only.'), + sharedWithMe: z.boolean().optional().describe('Whether to search for files shared with the user.'), + } + }, + driveService.search + ); + + server.registerTool( + "calendar.list", + { + description: 'Lists all of the user\'s calendars.', + inputSchema: {} + }, + calendarService.listCalendars + ); + + server.registerTool( + "calendar.createEvent", + { + description: 'Creates a new event in a calendar.', + inputSchema: { + calendarId: z.string().describe('The ID of the calendar to create the event in.'), + summary: z.string().describe('The summary or title of the event.'), + start: z.object({ + dateTime: z.string().describe('The start time in strict ISO 8601 format with seconds and timezone (e.g., 2024-01-15T10:30:00Z or 2024-01-15T10:30:00-05:00).'), + }), + end: z.object({ + dateTime: z.string().describe('The end time in strict ISO 8601 format with seconds and timezone (e.g., 2024-01-15T11:30:00Z or 2024-01-15T11:30:00-05:00).'), + }), + attendees: z.array(z.string()).optional().describe('The email addresses of the attendees.'), + } + }, + calendarService.createEvent + ); + + server.registerTool( + "calendar.listEvents", + { + description: 'Lists events from a calendar. Defaults to upcoming events.', + inputSchema: { + calendarId: z.string().describe('The ID of the calendar to list events from.'), + timeMin: z.string().optional().describe('The start time for the event search. Defaults to the current time.'), + timeMax: z.string().optional().describe('The end time for the event search.'), + attendeeResponseStatus: z.array(z.string()).optional().describe('The response status of the attendee.'), + } + }, + calendarService.listEvents + ); + + server.registerTool( + "calendar.getEvent", + { + description: 'Gets the details of a specific calendar event.', + inputSchema: { + eventId: z.string().describe('The ID of the event to retrieve.'), + calendarId: z.string().optional().describe('The ID of the calendar the event belongs to. Defaults to the primary calendar.'), + } + }, + calendarService.getEvent + ); + + server.registerTool( + "calendar.findFreeTime", + { + description: 'Finds a free time slot for multiple people to meet.', + inputSchema: { + attendees: z.array(z.string()).describe('The email addresses of the attendees.'), + timeMin: z.string().describe('The start time for the search in strict ISO 8601 format with seconds and timezone (e.g., 2024-01-15T09:00:00Z or 2024-01-15T09:00:00-05:00).'), + timeMax: z.string().describe('The end time for the search in strict ISO 8601 format with seconds and timezone (e.g., 2024-01-15T18:00:00Z or 2024-01-15T18:00:00-05:00).'), + duration: z.number().describe('The duration of the meeting in minutes.'), + } + }, + calendarService.findFreeTime + ); + + server.registerTool( + "calendar.updateEvent", + { + description: 'Updates an existing event in a calendar.', + inputSchema: { + eventId: z.string().describe('The ID of the event to update.'), + calendarId: z.string().optional().describe('The ID of the calendar to update the event in.'), + summary: z.string().optional().describe('The new summary or title of the event.'), + start: z.object({ + dateTime: z.string().describe('The new start time in strict ISO 8601 format with seconds and timezone (e.g., 2024-01-15T10:30:00Z or 2024-01-15T10:30:00-05:00).'), + }).optional(), + end: z.object({ + dateTime: z.string().describe('The new end time in strict ISO 8601 format with seconds and timezone (e.g., 2024-01-15T11:30:00Z or 2024-01-15T11:30:00-05:00).'), + }).optional(), + attendees: z.array(z.string()).optional().describe('The new list of attendees for the event.'), + } + }, + calendarService.updateEvent + ); + + server.registerTool( + "calendar.respondToEvent", + { + description: 'Responds to a meeting invitation (accept, decline, or tentative).', + inputSchema: { + eventId: z.string().describe('The ID of the event to respond to.'), + calendarId: z.string().optional().describe('The ID of the calendar containing the event.'), + responseStatus: z.enum(['accepted', 'declined', 'tentative']).describe('Your response to the invitation.'), + sendNotification: z.boolean().optional().describe('Whether to send a notification to the organizer (default: true).'), + responseMessage: z.string().optional().describe('Optional message to include with your response.'), + } + }, + calendarService.respondToEvent + ); + + server.registerTool( + "chat.listSpaces", + { + description: 'Lists the spaces the user is a member of.', + inputSchema: {} + }, + chatService.listSpaces + ); + + server.registerTool( + "chat.findSpaceByName", + { + description: 'Finds a Google Chat space by its display name.', + inputSchema: { + displayName: z.string().describe('The display name of the space to find.'), + } + }, + chatService.findSpaceByName + ); + + server.registerTool( + "chat.sendMessage", + { + description: 'Sends a message to a Google Chat space.', + inputSchema: { + spaceName: z.string().describe('The name of the space to send the message to (e.g., spaces/AAAAN2J52O8).'), + message: z.string().describe('The message to send.'), + } + }, + chatService.sendMessage + ); + + server.registerTool( + "chat.getMessages", + { + description: 'Gets messages from a Google Chat space.', + inputSchema: { + spaceName: z.string().describe('The name of the space to get messages from (e.g., spaces/AAAAN2J52O8).'), + unreadOnly: z.boolean().optional().describe('Whether to return only unread messages.'), + pageSize: z.number().optional().describe('The maximum number of messages to return.'), + pageToken: z.string().optional().describe('The token for the next page of results.'), + } + }, + chatService.getMessages + ); + + server.registerTool( + "chat.sendDm", + { + description: 'Sends a direct message to a user.', + inputSchema: { + email: z.string().email().describe('The email address of the user to send the message to.'), + message: z.string().describe('The message to send.'), + } + }, + chatService.sendDm + ); + + server.registerTool( + "chat.findDmByEmail", + { + description: 'Finds a Google Chat DM space by a user\'s email address.', + inputSchema: { + email: z.string().email().describe('The email address of the user to find the DM space with.'), + } + }, + chatService.findDmByEmail + ); + + server.registerTool( + "chat.listThreads", + { + description: 'Lists threads from a Google Chat space in reverse chronological order.', + inputSchema: { + spaceName: z.string().describe('The name of the space to get threads from (e.g., spaces/AAAAN2J52O8).'), + pageSize: z.number().optional().describe('The maximum number of threads to return.'), + pageToken: z.string().optional().describe('The token for the next page of results.'), + } + }, + chatService.listThreads + ); + + server.registerTool( + 'chat.setUpSpace', + { + description: 'Sets up a new Google Chat space with a display name and a list of members.', + inputSchema: { + displayName: z.string().describe('The display name of the space.'), + userNames: z.array(z.string()).describe('The user names of the members to add to the space (e.g. users/12345678)'), + } + }, + chatService.setUpSpace + ); + + + // Gmail tools + server.registerTool( + "gmail.search", + { + description: 'Search for emails in Gmail using query parameters.', + inputSchema: { + query: z.string().optional().describe('Search query (same syntax as Gmail search box, e.g., "from:someone@example.com is:unread").'), + maxResults: z.number().optional().describe(`Maximum number of results to return (default: ${GMAIL_SEARCH_MAX_RESULTS}).`), + pageToken: z.string().optional().describe('Token for the next page of results.'), + labelIds: z.array(z.string()).optional().describe('Filter by label IDs (e.g., ["INBOX", "UNREAD"]).'), + includeSpamTrash: z.boolean().optional().describe('Include messages from SPAM and TRASH (default: false).'), + } + }, + gmailService.search + ); + + server.registerTool( + "gmail.get", + { + description: 'Get the full content of a specific email message.', + inputSchema: { + messageId: z.string().describe('The ID of the message to retrieve.'), + format: z.enum(['minimal', 'full', 'raw', 'metadata']).optional().describe('Format of the message (default: full).'), + } + }, + gmailService.get + ); + + server.registerTool( + "gmail.modify", + { + description: `Modify a Gmail message. Supported modifications include: + - Add labels to a message. + - Remove labels from a message. +There are a list of system labels that can be modified on a message: + - INBOX: removing INBOX label removes the message from inbox and archives the message. + - SPAM: adding SPAM label marks a message as spam. + - TRASH: adding TRASH label moves a message to trash. + - UNREAD: removing UNREAD label marks a message as read. + - STARRED: adding STARRED label marks a message as starred. + - IMPORTANT: adding IMPORTANT label marks a message as important.`, + inputSchema: { + messageId: z.string().describe('The ID of the message to add labels to and/or remove labels from.'), + addLabelIds: z.array(z.string()).max(100).optional().describe('A list of label IDs to add to the message. Limit to 100 labels.'), + removeLabelIds: z.array(z.string()).max(100).optional().describe('A list of label IDs to remove from the message. Limit to 100 labels.'), + } + }, + gmailService.modify + ); + + server.registerTool( + "gmail.send", + { + description: 'Send an email message.', + inputSchema: emailComposeSchema + }, + gmailService.send + ); + + server.registerTool( + "gmail.createDraft", + { + description: 'Create a draft email message.', + inputSchema: emailComposeSchema + }, + gmailService.createDraft + ); + + server.registerTool( + "gmail.sendDraft", + { + description: 'Send a previously created draft email.', + inputSchema: { + draftId: z.string().describe('The ID of the draft to send.'), + } + }, + gmailService.sendDraft + ); + + server.registerTool( + "gmail.listLabels", + { + description: 'List all Gmail labels in the user\'s mailbox.', + inputSchema: {} + }, + gmailService.listLabels + ); + + // Time tools + server.registerTool( + "time.getCurrentDate", + { + description: 'Gets the current date.', + inputSchema: {} + }, + timeService.getCurrentDate + ); + + server.registerTool( + "time.getCurrentTime", + { + description: 'Gets the current time.', + inputSchema: {} + }, + timeService.getCurrentTime + ); + + server.registerTool( + "time.getTimeZone", + { + description: 'Gets the local timezone.', + inputSchema: {} + }, + timeService.getTimeZone + ); + + // People tools + server.registerTool( + "people.getUserProfile", + { + description: 'Gets a user\'s profile information.', + inputSchema: { + userId: z.string().optional().describe('The ID of the user to get profile information for.'), + email: z.string().optional().describe('The email address of the user to get profile information for.'), + name: z.string().optional().describe('The name of the user to get profile information for.'), + } + }, + peopleService.getUserProfile + ); + + server.registerTool( + "people.getMe", + { + description: 'Gets the profile information of the authenticated user.', + inputSchema: {} + }, + peopleService.getMe + ); + + // 4. Connect the transport layer and start listening + const transport = new StdioServerTransport(); + await server.connect(transport); + + console.error("Google Workspace MCP Server is running (registerTool). Listening for requests..."); +} + +main().catch(error => { + console.error('A critical error occurred:', error); + process.exit(1); +}); diff --git a/workspace-mcp-server/src/services/CalendarService.ts b/workspace-mcp-server/src/services/CalendarService.ts new file mode 100644 index 00000000..d3df2d44 --- /dev/null +++ b/workspace-mcp-server/src/services/CalendarService.ts @@ -0,0 +1,523 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { calendar_v3, google } from 'googleapis'; +import { logToFile } from '../utils/logger'; +import { gaxiosOptions } from '../utils/GaxiosConfig'; +import { iso8601DateTimeSchema, emailArraySchema } from '../utils/validation'; +import { z } from 'zod'; + +export class CalendarService { + private calendar: calendar_v3.Calendar | null = null; + private primaryCalendarId: string | null = null; + + constructor(private authManager: any) { + } + + private createValidationErrorResponse(error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Validation failed'; + let helpMessage = 'Please use strict ISO 8601 format with seconds and timezone. Examples: 2024-01-15T10:30:00Z (UTC) or 2024-01-15T10:30:00-05:00 (EST)'; + + if (error instanceof z.ZodError && error.issues.some(issue => issue.path.includes('attendees') || issue.message.includes('email'))) { + helpMessage = 'Please ensure all attendee emails are in a valid format.'; + } + + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + error: 'Invalid input format', + details: errorMessage, + help: helpMessage + }) + }] + }; + } + + async initialize() { + logToFile('Initializing CalendarService...'); + this.calendar = await this.getCalendar(); + logToFile('CalendarService initialized.'); + } + + private async getCalendar(): Promise { + if (this.calendar) { + return this.calendar; + } + logToFile('Getting authenticated client for calendar...'); + const auth = await this.authManager.getAuthenticatedClient(); + logToFile('Got auth client, creating calendar instance...'); + const options = { ...gaxiosOptions, auth }; + return google.calendar({ version: 'v3', ...options }); + } + + private async getPrimaryCalendarId(): Promise { + if (this.primaryCalendarId) { + return this.primaryCalendarId; + } + logToFile('Getting primary calendar ID...'); + const calendar = await this.getCalendar(); + const res = await calendar.calendarList.list(); + const primaryCalendar = res.data.items?.find(c => c.primary); + if (primaryCalendar && primaryCalendar.id) { + logToFile(`Found primary calendar: ${primaryCalendar.id}`); + this.primaryCalendarId = primaryCalendar.id; + return primaryCalendar.id; + } + logToFile('No primary calendar found, defaulting to "primary"'); + return 'primary'; + } + + listCalendars = async () => { + logToFile('listCalendars called'); + try { + logToFile('Getting calendar instance...'); + const calendar = await this.getCalendar(); + logToFile('Making API call to calendar.calendarList.list()...'); + const res = await calendar.calendarList.list(); + logToFile(`Found ${res.data.items?.length} calendars.`); + const calendars = res.data.items || []; + logToFile(`Returning calendar data: ${JSON.stringify(calendars.map(c => ({ id: c?.id, summary: c?.summary })))}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(calendars.map(c => ({ id: c?.id, summary: c?.summary }))) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during calendar.list: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + createEvent = async (input: { calendarId?: string, summary: string, start: { dateTime: string }, end: { dateTime: string }, attendees?: string[] }) => { + const { calendarId, summary, start, end, attendees } = input; + + // Validate datetime formats + try { + iso8601DateTimeSchema.parse(start.dateTime); + iso8601DateTimeSchema.parse(end.dateTime); + if (attendees) { + emailArraySchema.parse(attendees); + } + } catch (error) { + return this.createValidationErrorResponse(error); + } + + const finalCalendarId = calendarId || await this.getPrimaryCalendarId(); + logToFile(`Creating event in calendar: ${finalCalendarId}`); + logToFile(`Event summary: ${summary}`); + logToFile(`Event start: ${start.dateTime}`); + logToFile(`Event end: ${end.dateTime}`); + logToFile(`Event attendees: ${attendees?.join(', ')}`); + try { + const event = { + summary, + start, + end, + attendees: attendees?.map(email => ({ email })) + }; + const calendar = await this.getCalendar(); + const res = await calendar.events.insert({ + calendarId: finalCalendarId, + requestBody: event, + }); + logToFile(`Successfully created event: ${res.data.id}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(res.data) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during calendar.createEvent: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + listEvents = async (input: { calendarId?: string, timeMin?: string, timeMax?: string, attendeeResponseStatus?: string[] }) => { + const { calendarId, timeMin = (new Date()).toISOString(), attendeeResponseStatus = ['accepted', 'tentative', 'needsAction'] } = input; + + let timeMax = input.timeMax; + if (!timeMax) { + const thirtyDaysFromNow = new Date(); + thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30); + timeMax = thirtyDaysFromNow.toISOString(); + } + + const finalCalendarId = calendarId || await this.getPrimaryCalendarId(); + logToFile(`Listing events for calendar: ${finalCalendarId}`); + try { + const calendar = await this.getCalendar(); + const res = await calendar.events.list({ + calendarId: finalCalendarId, + timeMin, + timeMax, + singleEvents: true, + fields: 'items(id,summary,start,end,description,htmlLink,attendees,status)', + }); + + const events = res.data.items + ?.filter(event => event.status !== 'cancelled' && !!event.summary) + .filter(event => { + if (!event.attendees || event.attendees.length === 0) { + return true; // No attendees, so we can't filter, include it + } + if (event.attendees.length === 1 && event.attendees[0].self) { + return true; // I'm the only one, always include it + } + const self = event.attendees.find(a => a.self); + if (!self) { + return true; // We are not an attendee, include it + } + return attendeeResponseStatus.includes(self.responseStatus || 'needsAction'); + }); + + logToFile(`Found ${events?.length} events after filtering.`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(events) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during calendar.listEvents: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + getEvent = async (input: { eventId: string, calendarId?: string }) => { + const { eventId, calendarId } = input; + const finalCalendarId = calendarId || await this.getPrimaryCalendarId(); + logToFile(`Getting event ${eventId} from calendar: ${finalCalendarId}`); + try { + const calendar = await this.getCalendar(); + const res = await calendar.events.get({ + calendarId: finalCalendarId, + eventId, + }); + logToFile(`Successfully retrieved event: ${res.data.id}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(res.data) + }] + }; + } catch (error) { + const errorMessage = (error as any).response?.data?.error?.message || (error instanceof Error ? error.message : String(error)); + logToFile(`Error during calendar.getEvent: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + updateEvent = async (input: { eventId: string, calendarId?: string, summary?: string, start?: { dateTime: string }, end?: { dateTime: string }, attendees?: string[] }) => { + const { eventId, calendarId, summary, start, end, attendees } = input; + + // Validate datetime formats if provided + try { + if (start) { + iso8601DateTimeSchema.parse(start.dateTime); + } + if (end) { + iso8601DateTimeSchema.parse(end.dateTime); + } + if (attendees) { + emailArraySchema.parse(attendees); + } + } catch (error) { + return this.createValidationErrorResponse(error); + } + + const finalCalendarId = calendarId || await this.getPrimaryCalendarId(); + logToFile(`Updating event ${eventId} in calendar: ${finalCalendarId}`); + + try { + const calendar = await this.getCalendar(); + + // Build request body with only the fields to update (patch semantics) + const requestBody: calendar_v3.Schema$Event = {}; + if (summary !== undefined) requestBody.summary = summary; + if (start) requestBody.start = start; + if (end) requestBody.end = end; + if (attendees) requestBody.attendees = attendees.map(email => ({ email })); + + const res = await calendar.events.update({ + calendarId: finalCalendarId, + eventId, + requestBody, + }); + + logToFile(`Successfully updated event: ${res.data.id}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(res.data) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during calendar.updateEvent: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + respondToEvent = async (input: { eventId: string, calendarId?: string, responseStatus: 'accepted' | 'declined' | 'tentative', sendNotification?: boolean, responseMessage?: string }) => { + const { eventId, calendarId, responseStatus, sendNotification = true, responseMessage } = input; + const finalCalendarId = calendarId || await this.getPrimaryCalendarId(); + + logToFile(`Responding to event ${eventId} in calendar: ${finalCalendarId} with status: ${responseStatus}`); + if (responseMessage) { + logToFile(`Response message: ${responseMessage}`); + } + + try { + const calendar = await this.getCalendar(); + + // First, get the current event to find the attendee entry + const event = await calendar.events.get({ + calendarId: finalCalendarId, + eventId, + }); + + if (!event.data.attendees || event.data.attendees.length === 0) { + logToFile('Event has no attendees'); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: 'Event has no attendees' }) + }] + }; + } + + // Find the current user's attendee entry + const selfAttendee = event.data.attendees.find(a => a.self === true); + if (!selfAttendee) { + logToFile('User is not an attendee of this event'); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: 'You are not an attendee of this event' }) + }] + }; + } + + // Update the response status for the current user + selfAttendee.responseStatus = responseStatus; + if (responseMessage !== undefined) { + selfAttendee.comment = responseMessage; + } + + // Patch the event with the updated attendee list + const res = await calendar.events.patch({ + calendarId: finalCalendarId, + eventId, + sendNotifications: sendNotification, + requestBody: { + attendees: event.data.attendees + } + }); + + logToFile(`Successfully responded to event: ${res.data.id} with status: ${responseStatus}`); + + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + eventId: res.data.id, + summary: res.data.summary, + responseStatus, + message: `Successfully ${responseStatus} the meeting invitation${responseMessage ? ' with message' : ''}` + }) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during calendar.respondToEvent: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + findFreeTime = async (input: { attendees: string[], timeMin: string, timeMax: string, duration: number }) => { + const { attendees, timeMin, timeMax, duration } = input; + + // Validate datetime formats + try { + iso8601DateTimeSchema.parse(timeMin); + iso8601DateTimeSchema.parse(timeMax); + // Note: attendees can include 'me' as a special value, so we don't validate as emails + } catch (error) { + return this.createValidationErrorResponse(error); + } + + logToFile(`Finding free time for attendees: ${attendees.join(', ')}`); + logToFile(`Time range: ${timeMin} - ${timeMax}`); + logToFile(`Duration: ${duration} minutes`); + + try { + const calendar = await this.getCalendar(); + const items = await Promise.all(attendees.map(async (email) => { + if (email === 'me') { + const primaryId = await this.getPrimaryCalendarId(); + return { id: primaryId }; + } + return { id: email }; + })); + + const res = await calendar.freebusy.query({ + requestBody: { + items, + timeMin, + timeMax, + }, + }); + + const busyTimes = Object.values(res.data.calendars || {}).flatMap(cal => cal.busy || []); + if (busyTimes.length === 0) { + logToFile('No busy times found, returning the start of the time range.'); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ start: timeMin, end: new Date(new Date(timeMin).getTime() + duration * 60000).toISOString() }) + }] + }; + } + + // Sort and merge overlapping busy intervals for better performance + const sortedBusyTimes = busyTimes + .filter(busy => busy.start && busy.end) + .map(busy => ({ + start: new Date(busy.start!).getTime(), + end: new Date(busy.end!).getTime() + })) + .sort((a, b) => a.start - b.start); + + const mergedBusyTimes: { start: number; end: number }[] = []; + for (const busy of sortedBusyTimes) { + if (mergedBusyTimes.length === 0) { + mergedBusyTimes.push(busy); + } else { + const last = mergedBusyTimes[mergedBusyTimes.length - 1]; + if (busy.start <= last.end) { + // Overlapping or adjacent intervals - merge them + last.end = Math.max(last.end, busy.end); + } else { + mergedBusyTimes.push(busy); + } + } + } + + const startTime = new Date(timeMin).getTime(); + const endTime = new Date(timeMax).getTime(); + const durationMs = duration * 60000; + + // If no busy times, return the start of the range + if (mergedBusyTimes.length === 0) { + const slotEnd = new Date(startTime + durationMs); + logToFile(`No busy times, found free time: ${timeMin} - ${slotEnd.toISOString()}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ start: timeMin, end: slotEnd.toISOString() }) + }] + }; + } + + // Check if we can fit the meeting before the first busy slot + if (startTime + durationMs <= mergedBusyTimes[0].start) { + const slotEnd = new Date(startTime + durationMs); + logToFile(`Found free time: ${timeMin} - ${slotEnd.toISOString()}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ start: timeMin, end: slotEnd.toISOString() }) + }] + }; + } + + // Check gaps between busy slots + for (let i = 0; i < mergedBusyTimes.length - 1; i++) { + const gapStart = mergedBusyTimes[i].end; + const gapEnd = mergedBusyTimes[i + 1].start; + + if (gapEnd - gapStart >= durationMs) { + const slotStart = new Date(gapStart); + const slotEnd = new Date(gapStart + durationMs); + logToFile(`Found free time: ${slotStart.toISOString()} - ${slotEnd.toISOString()}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ start: slotStart.toISOString(), end: slotEnd.toISOString() }) + }] + }; + } + } + + // Check if we can fit after the last busy slot + const lastBusyEnd = mergedBusyTimes[mergedBusyTimes.length - 1].end; + if (lastBusyEnd + durationMs <= endTime) { + const slotStart = new Date(lastBusyEnd); + const slotEnd = new Date(lastBusyEnd + durationMs); + logToFile(`Found free time: ${slotStart.toISOString()} - ${slotEnd.toISOString()}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ start: slotStart.toISOString(), end: slotEnd.toISOString() }) + }] + }; + } + + logToFile('No available free time found'); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: 'No available free time found' }) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during calendar.findFreeTime: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } +} diff --git a/workspace-mcp-server/src/services/ChatService.ts b/workspace-mcp-server/src/services/ChatService.ts new file mode 100644 index 00000000..223ff6d9 --- /dev/null +++ b/workspace-mcp-server/src/services/ChatService.ts @@ -0,0 +1,433 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { google, chat_v1, people_v1, Auth } from 'googleapis'; +import { AuthManager } from '../auth/AuthManager'; +import { logToFile } from '../utils/logger'; +import { gaxiosOptions } from '../utils/GaxiosConfig'; + +export class ChatService { + private chat: chat_v1.Chat; + private people: people_v1.People; + private authClient: Auth.OAuth2Client; + + constructor(private authManager: AuthManager) { + this.chat = {} as chat_v1.Chat; + this.people = {} as people_v1.People; + this.authClient = {} as Auth.OAuth2Client; + } + + public async initialize(): Promise { + this.authClient = await this.authManager.getAuthenticatedClient(); + const options = { ...gaxiosOptions, auth: this.authClient }; + this.chat = google.chat({ + version: 'v1', + ...options, + }); + this.people = google.people({ + version: 'v1', + ...options, + }); + } + + private async _setupDmSpace(email: string): Promise { + const person = { + name: `users/${email}`, + type: 'HUMAN', + }; + + const setupResponse = await this.chat.spaces.setup({ + requestBody: { + space: { + spaceType: 'DIRECT_MESSAGE', + }, + memberships: [ + { + member: person, + }, + ], + }, + }); + + const space = setupResponse.data; + if (!space) { + throw new Error('Could not find or create a DM space.'); + } + return space; + } + + public listSpaces = async () => { + logToFile('Listing chat spaces'); + try { + const res = await this.chat.spaces.list({}); + const spaces = res.data.spaces || []; + logToFile(`Successfully listed ${spaces.length} chat spaces.`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(spaces) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during chat.listSpaces: ${errorMessage}`); + if (error instanceof Error && error.stack) { + logToFile(`Stack trace: ${error.stack}`); + } + logToFile(`Full error object: ${JSON.stringify(error, null, 2)}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + error: 'An error occurred while listing chat spaces.', + details: errorMessage + }) + }] + }; + } + } + + public sendMessage = async ({ spaceName, message }: { spaceName: string, message: string }) => { + logToFile(`Sending message to space: ${spaceName}`); + try { + const response = await this.chat.spaces.messages.create({ + parent: spaceName, + requestBody: { + text: message, + }, + }); + logToFile(`Successfully sent message to space: ${spaceName}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(response.data) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during chat.sendMessage: ${errorMessage}`); + if (error instanceof Error && error.stack) { + logToFile(`Stack trace: ${error.stack}`); + } + logToFile(`Full error object: ${JSON.stringify(error, null, 2)}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + error: 'An error occurred while sending the message.', + details: errorMessage + }) + }] + }; + } + } + + public findSpaceByName = async ({ displayName }: { displayName: string }) => { + logToFile(`Finding space with display name: ${displayName}`); + try { + // The Chat API's spaces.list method does not support filtering by + // displayName on the server. We must fetch all spaces and filter locally. + let pageToken: string | undefined = undefined; + let allSpaces: chat_v1.Schema$Space[] = []; + + do { + const res: any = await this.chat.spaces.list({ pageToken }); + const spaces = res.data.spaces || []; + allSpaces = allSpaces.concat(spaces); + pageToken = res.data.nextPageToken || undefined; + } while (pageToken); + + const foundSpaces = allSpaces.filter(space => space.displayName === displayName); + + if (foundSpaces.length > 0) { + logToFile(`Found ${foundSpaces.length} space(s) with display name: ${displayName}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(foundSpaces) + }] + }; + } else { + logToFile(`No space found with display name: ${displayName}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + error: `No space found with display name: ${displayName}` + }) + }] + }; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during chat.findSpaceByName: ${errorMessage}`); + if (error instanceof Error && error.stack) { + logToFile(`Stack trace: ${error.stack}`); + } + logToFile(`Full error object: ${JSON.stringify(error, null, 2)}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + error: 'An error occurred while finding the space.', + details: errorMessage + }) + }] + }; + } + } + + public getMessages = async ({ spaceName, unreadOnly, pageSize, pageToken }: { spaceName: string, unreadOnly?: boolean, pageSize?: number, pageToken?: string }) => { + logToFile(`Listing messages for space: ${spaceName}`); + try { + if (unreadOnly) { + const person = await this.people.people.get({ + resourceName: 'people/me', + personFields: 'metadata', + }); + + const userId = person.data.metadata?.sources?.find(s => s.type === 'PROFILE')?.id; + + if (!userId) { + throw new Error('Could not determine user ID.'); + } + const userMemberName = `users/${userId}`; + + const membersRes = await this.chat.spaces.members.list({ + parent: spaceName, + }); + // Type assertion needed due to incomplete type definitions + const memberships = (membersRes.data as any).memberships || []; + const currentUserMember = memberships.find((m: any) => m.member?.name === userMemberName); + + const lastReadTime = currentUserMember?.lastReadTime; + + if (!lastReadTime) { + logToFile(`No last read time found for user in space: ${spaceName}`); + // This can happen if the user has never read messages in the space. + // In this case, all messages are unread. + const res = await this.chat.spaces.messages.list({ parent: spaceName, pageSize, pageToken }); + const messages = res.data.messages || []; + logToFile(`Successfully listed ${messages.length} unread messages for space: ${spaceName}`); + return { content: [{ type: "text" as const, text: JSON.stringify({ messages, nextPageToken: res.data.nextPageToken }) }] }; + } + + const res = await this.chat.spaces.messages.list({ + parent: spaceName, + filter: `createTime > "${lastReadTime}"`, + pageSize, + pageToken, + }); + + const messages = res.data.messages || []; + logToFile(`Successfully listed ${messages.length} unread messages for space: ${spaceName}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ messages, nextPageToken: res.data.nextPageToken }) + }] + }; + + } else { + const res = await this.chat.spaces.messages.list({ + parent: spaceName, + pageSize, + pageToken, + }); + const messages = res.data.messages || []; + logToFile(`Successfully listed ${messages.length} messages for space: ${spaceName}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ messages, nextPageToken: res.data.nextPageToken }) + }] + }; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during chat.getMessages: ${errorMessage}`); + if (error instanceof Error && error.stack) { + logToFile(`Stack trace: ${error.stack}`); + } + logToFile(`Full error object: ${JSON.stringify(error, null, 2)}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + error: 'An error occurred while listing messages.', + details: errorMessage + }) + }] + }; + } + } + + public sendDm = async ({ email, message }: { email: string, message: string }) => { + logToFile(`chat.sendDm called with: email=${email}, message=${message}`); + try { + const space = await this._setupDmSpace(email); + const spaceName = space.name; + + if (!spaceName) { + throw new Error('Could not determine the space name for the DM.'); + } + + // Send the message to the DM space. + const messageResponse = await this.chat.spaces.messages.create({ + parent: spaceName, + requestBody: { + text: message, + }, + }); + + logToFile(`Successfully sent DM to: ${email}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(messageResponse.data) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during chat.sendDm: ${errorMessage}`); + if (error instanceof Error && error.stack) { + logToFile(`Stack trace: ${error.stack}`); + } + logToFile(`Full error object: ${JSON.stringify(error, null, 2)}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + error: 'An error occurred while sending the DM.', + details: errorMessage + }) + }] + }; + } + } + + public findDmByEmail = async ({ email }: { email: string }) => { + logToFile(`Finding DM space with user: ${email}`); + try { + const space = await this._setupDmSpace(email); + logToFile(`Found or created DM space: ${space.name}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(space) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during chat.findDmByEmail: ${errorMessage}`); + if (error instanceof Error && error.stack) { + logToFile(`Stack trace: ${error.stack}`); + } + logToFile(`Full error object: ${JSON.stringify(error, null, 2)}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + error: 'An error occurred while finding the DM space.', + details: errorMessage + }) + }] + }; + } + } + + public listThreads = async ({ spaceName, pageSize, pageToken }: { spaceName: string, pageSize?: number, pageToken?: string }) => { + logToFile(`Listing threads for space: ${spaceName}`); + try { + const res = await this.chat.spaces.messages.list({ + parent: spaceName, + pageSize, + pageToken, + orderBy: 'createTime desc', + }); + + const messages = res.data.messages || []; + const threads: chat_v1.Schema$Message[] = []; + const threadIds = new Set(); + + for (const message of messages) { + if (message.thread?.name && !threadIds.has(message.thread.name)) { + threads.push(message); + threadIds.add(message.thread.name); + } + } + + logToFile(`Successfully listed ${threads.length} threads for space: ${spaceName}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ threads, nextPageToken: res.data.nextPageToken }) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during chat.listThreads: ${errorMessage}`); + if (error instanceof Error && error.stack) { + logToFile(`Stack trace: ${error.stack}`); + } + logToFile(`Full error object: ${JSON.stringify(error, null, 2)}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + error: 'An error occurred while listing threads.', + details: errorMessage + }) + }] + }; + } + } + + public setUpSpace = async ({ displayName, userNames }: { displayName: string, userNames: string[] }) => { + logToFile(`Creating space with display name: ${displayName}`); + try { + const memberships = userNames.map(userName => ({ + member: { + name: userName, + type: 'HUMAN', + }, + })); + + const response = await this.chat.spaces.setup({ + requestBody: { + space: { + spaceType: 'SPACE', + displayName, + }, + memberships: memberships, + }, + }); + logToFile(`Successfully created space: ${response.data.name}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(response.data) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during chat.createSpace: ${errorMessage}`); + if (error instanceof Error && error.stack) { + logToFile(`Stack trace: ${error.stack}`); + } + logToFile(`Full error object: ${JSON.stringify(error, null, 2)}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + error: 'An error occurred while creating the space.', + details: errorMessage + }) + }] + }; + } + } +} diff --git a/workspace-mcp-server/src/services/DocsService.ts b/workspace-mcp-server/src/services/DocsService.ts new file mode 100644 index 00000000..3f2dcbcd --- /dev/null +++ b/workspace-mcp-server/src/services/DocsService.ts @@ -0,0 +1,493 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { google, docs_v1, drive_v3, Auth } from 'googleapis'; +import { AuthManager } from '../auth/AuthManager'; +import { DriveService } from './DriveService'; +import { logToFile } from '../utils/logger'; +import { extractDocId } from '../utils/IdUtils'; +import { marked } from 'marked'; +import { Readable } from 'node:stream'; +import createDOMPurify from 'dompurify'; +import { JSDOM } from 'jsdom'; +import { gaxiosOptions, mediaUploadOptions } from '../utils/GaxiosConfig'; +import { buildDriveSearchQuery, MIME_TYPES } from '../utils/DriveQueryBuilder'; +import { extractDocumentId as validateAndExtractDocId } from '../utils/validation'; +import { parseMarkdownToDocsRequests, processMarkdownLineBreaks } from '../utils/markdownToDocsRequests'; + +export class DocsService { + private docs: docs_v1.Docs; + private drive: drive_v3.Drive; + private purify: ReturnType; + + constructor(private authManager: AuthManager, private driveService: DriveService) { + this.docs = {} as docs_v1.Docs; + this.drive = {} as drive_v3.Drive; + const window = new JSDOM('').window; + this.purify = createDOMPurify(window as any); + } + + public async initialize(): Promise { + const auth: Auth.OAuth2Client = await this.authManager.getAuthenticatedClient(); + const options = { ...gaxiosOptions, auth }; + this.docs = google.docs({ version: 'v1', ...options }); + this.drive = google.drive({ version: 'v3', ...options }); + } + + public create = async ({ title, folderName, markdown }: { title: string, folderName?: string, markdown?: string }) => { + logToFile(`[DocsService] Starting create with title: ${title}, folderName: ${folderName}, markdown: ${markdown ? 'true' : 'false'}`); + try { + const docInfo = await (async (): Promise<{ documentId: string; title: string; }> => { + if (markdown) { + logToFile('[DocsService] Creating doc with markdown'); + const unsafeHtml = await marked.parse(markdown); + const html = this.purify.sanitize(unsafeHtml); + + const fileMetadata = { + name: title, + mimeType: 'application/vnd.google-apps.document', + }; + + const media = { + mimeType: 'text/html', + body: Readable.from(html), + }; + + logToFile('[DocsService] Calling drive.files.create'); + const file = await this.drive.files.create({ + requestBody: fileMetadata, + media: media, + fields: 'id, name', + }, mediaUploadOptions); + logToFile('[DocsService] drive.files.create finished'); + return { documentId: file.data.id!, title: file.data.name! }; + } else { + logToFile('[DocsService] Creating blank doc'); + logToFile('[DocsService] Calling docs.documents.create'); + const doc = await this.docs.documents.create({ + requestBody: { title }, + }); + logToFile('[DocsService] docs.documents.create finished'); + return { documentId: doc.data.documentId!, title: doc.data.title! }; + } + })(); + + if (folderName) { + logToFile(`[DocsService] Moving doc to folder: ${folderName}`); + await this._moveFileToFolder(docInfo.documentId, folderName); + logToFile(`[DocsService] Finished moving doc to folder: ${folderName}`); + } + + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + documentId: docInfo.documentId, + title: docInfo.title, + }) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during docs.create: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + public insertText = async ({ documentId, text }: { documentId: string, text: string }) => { + logToFile(`[DocsService] Starting insertText for document: ${documentId}`); + try { + const id = extractDocId(documentId) || documentId; + + // Parse markdown and generate formatting requests + const { plainText, formattingRequests } = parseMarkdownToDocsRequests(text, 1); + const processedText = processMarkdownLineBreaks(plainText); + + // Build batch update requests + const requests: docs_v1.Schema$Request[] = [ + { + insertText: { + location: { index: 1 }, + text: processedText, + }, + } + ]; + + // Add formatting requests if any + if (formattingRequests.length > 0) { + requests.push(...formattingRequests); + } + + const res = await this.docs.documents.batchUpdate({ + documentId: id, + requestBody: { + requests, + }, + }); + + logToFile(`[DocsService] Finished insertText for document: ${id}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + documentId: res.data.documentId!, + writeControl: res.data.writeControl!, + }) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[DocsService] Error during docs.insertText: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + public find = async ({ query, pageToken, pageSize = 10 }: { query: string, pageToken?: string, pageSize?: number }) => { + logToFile(`Searching for documents with query: ${query}`); + if (pageToken) { + logToFile(`Using pageToken: ${pageToken}`); + } + if (pageSize) { + logToFile(`Using pageSize: ${pageSize}`); + } + try { + const q = buildDriveSearchQuery(MIME_TYPES.DOCUMENT, query); + logToFile(`Executing Drive API query: ${q}`); + + const res = await this.drive.files.list({ + pageSize: pageSize, + fields: 'nextPageToken, files(id, name)', + q: q, + pageToken: pageToken, + }); + + const files = res.data.files || []; + const nextPageToken = res.data.nextPageToken; + + logToFile(`Found ${files.length} files.`); + if (nextPageToken) { + logToFile(`Next page token: ${nextPageToken}`); + } + logToFile(`API Response: ${JSON.stringify(res.data, null, 2)}`); + + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + files: files, + nextPageToken: nextPageToken + }) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during docs.find: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + public move = async ({ documentId, folderName }: { documentId: string, folderName: string }) => { + logToFile(`[DocsService] Starting move for document: ${documentId}`); + try { + const id = extractDocId(documentId) || documentId; + await this._moveFileToFolder(id, folderName); + logToFile(`[DocsService] Finished move for document: ${id}`); + return { + content: [{ + type: "text" as const, + text: `Moved document ${id} to folder ${folderName}` + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[DocsService] Error during docs.move: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + public getText = async ({ documentId }: { documentId: string }) => { + logToFile(`[DocsService] Starting getText for document: ${documentId}`); + try { + // Validate and extract document ID + const id = validateAndExtractDocId(documentId); + const res = await this.docs.documents.get({ + documentId: id, + fields: 'body', + }); + + const body = res.data.body; + let text = ''; + if (body && body.content) { + body.content.forEach(element => { + text += this._readStructuralElement(element); + }); + } + + logToFile(`[DocsService] Finished getText for document: ${id}`); + return { + content: [{ + type: "text" as const, + text: text + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[DocsService] Error during docs.getText: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + private _readStructuralElement(element: docs_v1.Schema$StructuralElement): string { + let text = ''; + if (element.paragraph) { + element.paragraph.elements?.forEach(pElement => { + if (pElement.textRun && pElement.textRun.content) { + text += pElement.textRun.content; + } + }); + } else if (element.table) { + element.table.tableRows?.forEach(row => { + row.tableCells?.forEach(cell => { + cell.content?.forEach(cellContent => { + text += this._readStructuralElement(cellContent); + }); + }); + }); + } + return text; + } + + public appendText = async ({ documentId, text }: { documentId: string, text: string }) => { + logToFile(`[DocsService] Starting appendText for document: ${documentId}`); + try { + const id = extractDocId(documentId) || documentId; + const res = await this.docs.documents.get({ + documentId: id, + fields: 'body', + }); + + const body = res.data.body; + const lastElement = body?.content?.[body.content.length - 1]; + const endIndex = lastElement?.endIndex || 1; + + const locationIndex = Math.max(1, endIndex - 1); + + // Parse markdown and generate formatting requests + const { plainText, formattingRequests } = parseMarkdownToDocsRequests(text, locationIndex); + const processedText = processMarkdownLineBreaks(plainText); + + // Build batch update requests + const requests: docs_v1.Schema$Request[] = [ + { + insertText: { + location: { index: locationIndex }, + text: processedText, + }, + } + ]; + + // Add formatting requests if any + if (formattingRequests.length > 0) { + requests.push(...formattingRequests); + } + + await this.docs.documents.batchUpdate({ + documentId: id, + requestBody: { + requests, + }, + }); + + logToFile(`[DocsService] Finished appendText for document: ${id}`); + return { + content: [{ + type: "text" as const, + text: `Successfully appended text to document ${id}` + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[DocsService] Error during docs.appendText: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + public replaceText = async ({ documentId, findText, replaceText }: { documentId: string, findText: string, replaceText: string }) => { + logToFile(`[DocsService] Starting replaceText for document: ${documentId}`); + try { + const id = extractDocId(documentId) || documentId; + + // Parse markdown to get plain text and formatting info + const { plainText, formattingRequests: originalFormattingRequests } = parseMarkdownToDocsRequests(replaceText, 0); + const processedText = processMarkdownLineBreaks(plainText); + + // First, get the document to find where the text will be replaced + const docBefore = await this.docs.documents.get({ + documentId: id, + fields: 'body', + }); + + // Find all occurrences of the text to be replaced + const documentText = this._getFullDocumentText(docBefore.data.body); + const occurrences: number[] = []; + let searchIndex = 0; + while ((searchIndex = documentText.indexOf(findText, searchIndex)) !== -1) { + occurrences.push(searchIndex + 1); // Google Docs uses 1-based indexing + searchIndex += findText.length; + } + + // Build batch update requests + const requests: docs_v1.Schema$Request[] = [ + { + replaceAllText: { + replaceText: processedText, + containsText: { + text: findText, + matchCase: true, + }, + }, + } + ]; + + // Calculate formatting positions for each replacement + // After replacement, we need to adjust indices based on text length difference + const lengthDiff = processedText.length - findText.length; + let cumulativeOffset = 0; + + for (let i = 0; i < occurrences.length; i++) { + const occurrence = occurrences[i]; + const adjustedPosition = occurrence + cumulativeOffset - 1; // Subtract 1 because parseMarkdownToDocsRequests expects 0-based + + // Adjust formatting requests for this occurrence + for (const formatRequest of originalFormattingRequests) { + if (formatRequest.updateTextStyle) { + const adjustedRequest: docs_v1.Schema$Request = { + updateTextStyle: { + ...formatRequest.updateTextStyle, + range: { + startIndex: (formatRequest.updateTextStyle.range?.startIndex || 0) + adjustedPosition, + endIndex: (formatRequest.updateTextStyle.range?.endIndex || 0) + adjustedPosition + } + } + }; + requests.push(adjustedRequest); + } + } + + cumulativeOffset += lengthDiff; + } + + await this.docs.documents.batchUpdate({ + documentId: id, + requestBody: { + requests, + }, + }); + + logToFile(`[DocsService] Finished replaceText for document: ${id}`); + return { + content: [{ + type: "text" as const, + text: `Successfully replaced text in document ${id}` + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[DocsService] Error during docs.replaceText: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + private _getFullDocumentText(body: docs_v1.Schema$Body | undefined): string { + let text = ''; + if (body && body.content) { + body.content.forEach(element => { + text += this._readStructuralElement(element); + }); + } + return text; + } + + + + private async _moveFileToFolder(documentId: string, folderName: string): Promise { + try { + const findFolderResponse = await this.driveService.findFolder({ folderName }); + const parsedResponse = JSON.parse(findFolderResponse.content[0].text); + + if (parsedResponse.error) { + throw new Error(parsedResponse.error); + } + + const folders = parsedResponse as { id: string, name: string }[]; + + if (folders.length === 0) { + throw new Error(`Folder not found: ${folderName}`); + } + + if (folders.length > 1) { + logToFile(`Warning: Found multiple folders with name "${folderName}". Using the first one found.`); + } + + const folderId = folders[0].id; + const file = await this.drive.files.get({ + fileId: documentId, + fields: 'parents', + }); + + const previousParents = file.data.parents?.join(','); + + await this.drive.files.update({ + fileId: documentId, + addParents: folderId, + removeParents: previousParents, + fields: 'id, parents', + }); + } catch (error) { + if (error instanceof Error) { + logToFile(`Error during _moveFileToFolder: ${error.message}`); + } else { + logToFile(`An unknown error occurred during _moveFileToFolder: ${JSON.stringify(error)}`); + } + throw error; + } + } +} diff --git a/workspace-mcp-server/src/services/DriveService.ts b/workspace-mcp-server/src/services/DriveService.ts new file mode 100644 index 00000000..e6fd2dcc --- /dev/null +++ b/workspace-mcp-server/src/services/DriveService.ts @@ -0,0 +1,234 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { google, drive_v3, Auth } from 'googleapis'; +import { AuthManager } from '../auth/AuthManager'; +import { logToFile } from '../utils/logger'; +import { gaxiosOptions } from '../utils/GaxiosConfig'; +import { escapeQueryString } from '../utils/DriveQueryBuilder'; + +const MIN_DRIVE_ID_LENGTH = 25; + +const URL_PATTERNS = [ + { pattern: /\/folders\/([a-zA-Z0-9-_]+)/, type: 'folder' as const }, + { pattern: /\/file\/d\/([a-zA-Z0-9-_]+)/, type: 'file' as const }, + { pattern: /\/document\/d\/([a-zA-Z0-9-_]+)/, type: 'file' as const }, + { pattern: /\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/, type: 'file' as const }, + { pattern: /\/presentation\/d\/([a-zA-Z0-9-_]+)/, type: 'file' as const }, + { pattern: /\/forms\/d\/([a-zA-Z0-9-_]+)/, type: 'file' as const }, + { pattern: /[?&]id=([a-zA-Z0-9-_]+)/, type: 'unknown' as const } +]; + +export class DriveService { + private drive: drive_v3.Drive; + + constructor(private authManager: AuthManager) { + this.drive = {} as drive_v3.Drive; + } + + public async initialize(): Promise { + const auth: Auth.OAuth2Client = await this.authManager.getAuthenticatedClient(); + const options = { ...gaxiosOptions, auth }; + this.drive = google.drive({ version: 'v3', ...options }); + } + + public findFolder = async ({ folderName }: { folderName: string }) => { + logToFile(`Searching for folder with name: ${folderName}`); + try { + const query = `mimeType='application/vnd.google-apps.folder' and name = '${folderName}'`; + logToFile(`Executing Drive API query: ${query}`); + const res = await this.drive.files.list({ + q: query, + fields: 'files(id, name)', + spaces: 'drive', + }); + + const folders = res.data.files || []; + logToFile(`Found ${folders.length} folders.`); + logToFile(`API Response: ${JSON.stringify(folders, null, 2)}`); + + return { + content: [{ + type: "text" as const, + text: JSON.stringify(folders) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during drive.findFolder: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + public search = async ({ query, pageSize = 10, pageToken, corpus, unreadOnly, sharedWithMe }: { query?: string, pageSize?: number, pageToken?: string, corpus?: string, unreadOnly?: boolean, sharedWithMe?: boolean }) => { + let q = query; + let isProcessed = false; + + // Check if query is a Google Drive URL + if (query && (query.includes('drive.google.com') || query.includes('docs.google.com'))) { + isProcessed = true; + logToFile(`Detected Google Drive URL in query: ${query}`); + + let fileId: string | null = null; + let urlType: 'file' | 'folder' | 'unknown' = 'unknown'; + + for (const urlPattern of URL_PATTERNS) { + const match = query.match(urlPattern.pattern); + if (match) { + fileId = match[1]; + urlType = urlPattern.type; + break; + } + } + + if (fileId) { + let isFolder = urlType === 'folder'; + + if (urlType === 'unknown') { + try { + const file = await this.drive.files.get({ fileId, fields: 'mimeType' }); + if (file.data.mimeType === 'application/vnd.google-apps.folder') { + isFolder = true; + } + } catch { + logToFile(`Could not determine type of ID from URL, treating as file: ${fileId}`); + } + } + + if (isFolder) { + q = `'${fileId}' in parents`; + logToFile(`Extracted Folder ID from URL: ${fileId}, using query: ${q}`); + } else { + logToFile(`Extracted File ID from URL: ${fileId}, using files.get`); + try { + const res = await this.drive.files.get({ + fileId: fileId, + fields: 'id, name, modifiedTime, viewedByMeTime, mimeType, parents', + }); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + files: [res.data], + nextPageToken: null + }) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during drive.files.get: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + } else { + logToFile(`Could not extract file/folder ID from URL: ${query}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + error: "Invalid Drive URL. Please provide a valid Google Drive URL or a search query.", + details: "Could not extract file or folder ID from the provided URL." + }) + }] + }; + } + } + + if (query && !isProcessed) { + const titlePrefix = 'title:'; + const trimmedQuery = query.trim(); + + if (trimmedQuery.startsWith(titlePrefix)) { + let searchTerm = trimmedQuery.substring(titlePrefix.length).trim(); + if ((searchTerm.startsWith("'") && searchTerm.endsWith("'")) || + (searchTerm.startsWith('"') && searchTerm.endsWith('"'))) { + searchTerm = searchTerm.substring(1, searchTerm.length - 1); + } + q = `name contains '${escapeQueryString(searchTerm)}'`; + } else { + const driveIdPattern = new RegExp(`^[a-zA-Z0-9-_]{${MIN_DRIVE_ID_LENGTH},}$`); + if (driveIdPattern.test(trimmedQuery) && !trimmedQuery.includes(" ")) { + q = `'${trimmedQuery}' in parents`; + logToFile(`Detected Drive ID: ${trimmedQuery}, listing contents`); + } else { + const looksLikeQuery = /( and | or | not | contains | in |=)/.test(trimmedQuery); + if (!looksLikeQuery) { + const escapedQuery = escapeQueryString(trimmedQuery); + q = `fullText contains '${escapedQuery}'`; + } + } + } + } + + if (sharedWithMe) { + logToFile('Searching for files shared with the user.'); + if (q) { + q += " and sharedWithMe"; + } else { + q = "sharedWithMe"; + } + } + + logToFile(`Executing Drive search with query: ${q}`); + if (corpus) { + logToFile(`Using corpus: ${corpus}`); + } + if (unreadOnly) { + logToFile('Filtering for unread files only.'); + } + + try { + const res = await this.drive.files.list({ + q: q, + pageSize: pageSize, + pageToken: pageToken, + corpus: corpus as 'user' | 'domain' | undefined, + fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + }); + + let files = res.data.files || []; + const nextPageToken = res.data.nextPageToken; + + if (unreadOnly) { + files = files.filter(file => !file.viewedByMeTime); + } + + logToFile(`Found ${files.length} files.`); + if (nextPageToken) { + logToFile(`Next page token: ${nextPageToken}`); + } + + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + files: files, + nextPageToken: nextPageToken + }) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during drive.search: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } +} \ No newline at end of file diff --git a/workspace-mcp-server/src/services/GmailService.ts b/workspace-mcp-server/src/services/GmailService.ts new file mode 100644 index 00000000..3dddb95f --- /dev/null +++ b/workspace-mcp-server/src/services/GmailService.ts @@ -0,0 +1,400 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { google, gmail_v1, Auth } from 'googleapis'; +import { AuthManager } from '../auth/AuthManager'; +import { logToFile } from '../utils/logger'; +import { MimeHelper } from '../utils/MimeHelper'; +import { GMAIL_SEARCH_MAX_RESULTS } from '../utils/constants'; +import { gaxiosOptions } from '../utils/GaxiosConfig'; +import { emailArraySchema } from '../utils/validation'; + +// Type definitions for email parameters +type SendEmailParams = { + to: string | string[]; + subject: string; + body: string; + cc?: string | string[]; + bcc?: string | string[]; + isHtml?: boolean; +}; + +export class GmailService { + private gmail: gmail_v1.Gmail; + + constructor(private authManager: AuthManager) { + this.gmail = {} as gmail_v1.Gmail; + } + + public async initialize(): Promise { + const auth: Auth.OAuth2Client = await this.authManager.getAuthenticatedClient(); + const options = { ...gaxiosOptions, auth }; + this.gmail = google.gmail({ version: 'v1', ...options }); + } + + /** + * Helper method to handle errors consistently across all methods + */ + private handleError(error: unknown, context: string) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error during ${context}: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + + public search = async ({ + query, + maxResults = GMAIL_SEARCH_MAX_RESULTS, + pageToken, + labelIds, + includeSpamTrash = false + }: { + query?: string, + maxResults?: number, + pageToken?: string, + labelIds?: string[], + includeSpamTrash?: boolean + }) => { + try { + logToFile(`Gmail search - query: ${query}, maxResults: ${maxResults}`); + + const response = await this.gmail.users.messages.list({ + userId: 'me', + q: query, + maxResults, + pageToken, + labelIds, + includeSpamTrash + }); + + const messages = response.data.messages || []; + const nextPageToken = response.data.nextPageToken; + const resultSizeEstimate = response.data.resultSizeEstimate; + + logToFile(`Found ${messages.length} messages, estimated total: ${resultSizeEstimate}`); + + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + messages: messages.map(msg => ({ + id: msg.id, + threadId: msg.threadId + })), + nextPageToken, + resultSizeEstimate + }, null, 2) + }] + }; + } catch (error) { + return this.handleError(error, 'gmail.search'); + } + } + + public get = async ({ + messageId, + format = 'full' + }: { + messageId: string, + format?: 'minimal' | 'full' | 'raw' | 'metadata' + }) => { + try { + logToFile(`Getting message ${messageId} with format: ${format}`); + + const response = await this.gmail.users.messages.get({ + userId: 'me', + id: messageId, + format + }); + + const message = response.data; + + // Extract useful information based on format + if (format === 'metadata' || format === 'full') { + const headers = message.payload?.headers || []; + const getHeader = (name: string) => headers.find(h => h.name === name)?.value; + + const subject = getHeader('Subject'); + const from = getHeader('From'); + const to = getHeader('To'); + const date = getHeader('Date'); + + // Extract body for full format + let body = ''; + if (format === 'full' && message.payload) { + body = this.extractBody(message.payload); + } + + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + id: message.id, + threadId: message.threadId, + labelIds: message.labelIds, + snippet: message.snippet, + subject, + from, + to, + date, + body: body || message.snippet + }, null, 2) + }] + }; + } + + return { + content: [{ + type: "text" as const, + text: JSON.stringify(message, null, 2) + }] + }; + } catch (error) { + return this.handleError(error, 'gmail.get'); + } + } + + public modify = async ({ + messageId, + addLabelIds = [], + removeLabelIds = [] + }: { + messageId: string, + addLabelIds?: string[], + removeLabelIds?: string[] + }) => { + try { + logToFile(`Modifying message ${messageId} with addLabelIds: ${addLabelIds}, removeLabelIds: ${removeLabelIds}`); + + const response = await this.gmail.users.messages.modify({ + userId: 'me', + id: messageId, + requestBody: { + addLabelIds, + removeLabelIds, + }, + }); + + const message = response.data; + return { + content: [{ + type: "text" as const, + text: JSON.stringify(message, null, 2) + }] + }; + } catch (error) { + return this.handleError(error, 'gmail.modify'); + } + } + + public send = async ({ + to, + subject, + body, + cc, + bcc, + isHtml = false + }: SendEmailParams) => { + try { + // Validate email addresses + try { + emailArraySchema.parse(to); + if (cc) emailArraySchema.parse(cc); + if (bcc) emailArraySchema.parse(bcc); + } catch (error) { + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + error: 'Invalid email address format', + details: error instanceof Error ? error.message : 'Validation failed' + }) + }] + }; + } + + logToFile(`Sending email to: ${to}, subject: ${subject}`); + + // Create MIME message + const mimeMessage = MimeHelper.createMimeMessage({ + to: Array.isArray(to) ? to.join(', ') : to, + subject, + body, + cc: cc ? (Array.isArray(cc) ? cc.join(', ') : cc) : undefined, + bcc: bcc ? (Array.isArray(bcc) ? bcc.join(', ') : bcc) : undefined, + isHtml + }); + + const response = await this.gmail.users.messages.send({ + userId: 'me', + requestBody: { + raw: mimeMessage + } + }); + + logToFile(`Email sent successfully: ${response.data.id}`); + + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + id: response.data.id, + threadId: response.data.threadId, + labelIds: response.data.labelIds, + status: 'sent' + }, null, 2) + }] + }; + } catch (error) { + return this.handleError(error, 'gmail.send'); + } + } + + public createDraft = async ({ + to, + subject, + body, + cc, + bcc, + isHtml = false + }: SendEmailParams) => { + try { + logToFile(`Creating draft - to: ${to}, subject: ${subject}`); + + // Create MIME message + const mimeMessage = MimeHelper.createMimeMessage({ + to: Array.isArray(to) ? to.join(', ') : to, + subject, + body, + cc: cc ? (Array.isArray(cc) ? cc.join(', ') : cc) : undefined, + bcc: bcc ? (Array.isArray(bcc) ? bcc.join(', ') : bcc) : undefined, + isHtml + }); + + const response = await this.gmail.users.drafts.create({ + userId: 'me', + requestBody: { + message: { + raw: mimeMessage + } + } + }); + + logToFile(`Draft created successfully: ${response.data.id}`); + + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + id: response.data.id, + message: { + id: response.data.message?.id, + threadId: response.data.message?.threadId, + labelIds: response.data.message?.labelIds + }, + status: 'draft_created' + }, null, 2) + }] + }; + } catch (error) { + return this.handleError(error, 'gmail.createDraft'); + } + } + + public sendDraft = async ({ draftId }: { draftId: string }) => { + try { + logToFile(`Sending draft: ${draftId}`); + + const response = await this.gmail.users.drafts.send({ + userId: 'me', + requestBody: { + id: draftId + } + }); + + logToFile(`Draft sent successfully: ${response.data.id}`); + + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + id: response.data.id, + threadId: response.data.threadId, + labelIds: response.data.labelIds, + status: 'sent' + }, null, 2) + }] + }; + } catch (error) { + return this.handleError(error, 'gmail.sendDraft'); + } + } + + public listLabels = async () => { + try { + logToFile(`Listing Gmail labels`); + + const response = await this.gmail.users.labels.list({ + userId: 'me' + }); + + const labels = response.data.labels || []; + + logToFile(`Found ${labels.length} labels`); + + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + labels: labels.map(label => ({ + id: label.id, + name: label.name, + type: label.type, + messageListVisibility: label.messageListVisibility, + labelListVisibility: label.labelListVisibility + })) + }, null, 2) + }] + }; + } catch (error) { + return this.handleError(error, 'gmail.listLabels'); + } + } + + private extractBody(payload: gmail_v1.Schema$MessagePart): string { + let body = ''; + + // Check for plain text or HTML in the main part + if (payload.body?.data) { + body = Buffer.from(payload.body.data, 'base64').toString('utf-8'); + } + + // Check parts for multipart messages + if (payload.parts) { + for (const part of payload.parts) { + if (part.mimeType === 'text/plain' && part.body?.data) { + body = Buffer.from(part.body.data, 'base64').toString('utf-8'); + break; // Prefer plain text + } else if (part.mimeType === 'text/html' && part.body?.data && !body) { + body = Buffer.from(part.body.data, 'base64').toString('utf-8'); + } else if (part.parts) { + // Recursive for nested parts + const nestedBody = this.extractBody(part); + if (nestedBody) { + body = nestedBody; + break; + } + } + } + } + + return body; + } +} diff --git a/workspace-mcp-server/src/services/PeopleService.ts b/workspace-mcp-server/src/services/PeopleService.ts new file mode 100644 index 00000000..deb95a1b --- /dev/null +++ b/workspace-mcp-server/src/services/PeopleService.ts @@ -0,0 +1,99 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { google, people_v1 } from 'googleapis'; +import { Auth } from 'googleapis'; +import { AuthManager } from '../auth/AuthManager'; +import { logToFile } from '../utils/logger'; +import { gaxiosOptions } from '../utils/GaxiosConfig'; + +export class PeopleService { + private people: people_v1.People; + + constructor(private authManager: AuthManager) { + this.people = {} as people_v1.People; + } + + public async initialize(): Promise { + const auth: Auth.OAuth2Client = await this.authManager.getAuthenticatedClient(); + const options = { ...gaxiosOptions, auth }; + this.people = google.people({ version: 'v1', ...options }); + } + + public getUserProfile = async ({ userId, email, name }: { userId?: string, email?: string, name?: string }) => { + logToFile(`[PeopleService] Starting getUserProfile with: userId=${userId}, email=${email}, name=${name}`); + try { + if (!userId && !email && !name) { + throw new Error('Either userId, email, or name must be provided.'); + } + if (userId) { + const resourceName = userId.startsWith('people/') ? userId : `people/${userId}`; + const res = await this.people.people.get({ + resourceName, + personFields: 'names,emailAddresses', + }); + logToFile(`[PeopleService] Finished getUserProfile for user: ${userId}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ results: [{ person: res.data }] }) + }] + }; + } else if (email || name) { + const query = email || name; + const res = await this.people.people.searchDirectoryPeople({ + query, + readMask: 'names,emailAddresses', + sources: ['DIRECTORY_SOURCE_TYPE_DOMAIN_CONTACT', 'DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE'], + }); + logToFile(`[PeopleService] Finished getUserProfile search for: ${query}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(res.data) + }] + }; + } else { + throw new Error('Either userId, email, or name must be provided.'); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[PeopleService] Error during people.getUserProfile: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + public getMe = async () => { + logToFile(`[PeopleService] Starting getMe`); + try { + const res = await this.people.people.get({ + resourceName: 'people/me', + personFields: 'names,emailAddresses', + }); + logToFile(`[PeopleService] Finished getMe`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(res.data) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[PeopleService] Error during people.getMe: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } +} \ No newline at end of file diff --git a/workspace-mcp-server/src/services/SheetsService.ts b/workspace-mcp-server/src/services/SheetsService.ts new file mode 100644 index 00000000..5c4db77f --- /dev/null +++ b/workspace-mcp-server/src/services/SheetsService.ts @@ -0,0 +1,245 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { google, sheets_v4, drive_v3, Auth } from 'googleapis'; +import { AuthManager } from '../auth/AuthManager'; +import { logToFile } from '../utils/logger'; +import { extractDocId } from '../utils/IdUtils'; +import { gaxiosOptions } from '../utils/GaxiosConfig'; +import { buildDriveSearchQuery, MIME_TYPES } from '../utils/DriveQueryBuilder'; + +export class SheetsService { + private sheets: sheets_v4.Sheets; + private drive: drive_v3.Drive; + + constructor(private authManager: AuthManager) { + this.sheets = {} as sheets_v4.Sheets; + this.drive = {} as drive_v3.Drive; + } + + public async initialize(): Promise { + const auth: Auth.OAuth2Client = await this.authManager.getAuthenticatedClient(); + const options = { ...gaxiosOptions, auth }; + this.sheets = google.sheets({ version: 'v4', ...options }); + this.drive = google.drive({ version: 'v3', ...options }); + } + + public getText = async ({ spreadsheetId, format = 'text' }: { spreadsheetId: string, format?: 'text' | 'csv' | 'json' }) => { + logToFile(`[SheetsService] Starting getText for spreadsheet: ${spreadsheetId} with format: ${format}`); + try { + const id = extractDocId(spreadsheetId) || spreadsheetId; + + // Get spreadsheet metadata + const spreadsheet = await this.sheets.spreadsheets.get({ + spreadsheetId: id, + includeGridData: false, + }); + + let content = ''; + const jsonData: Record = {}; + + // Add spreadsheet title (except for JSON format) + if (spreadsheet.data.properties?.title && format !== 'json') { + content += `Spreadsheet Title: ${spreadsheet.data.properties.title}\n\n`; + } + + // Get all sheet names + const sheetNames = spreadsheet.data.sheets?.map(sheet => sheet.properties?.title) || []; + + // Get data from all sheets + for (const sheetName of sheetNames) { + if (!sheetName) continue; + + try { + const response = await this.sheets.spreadsheets.values.get({ + spreadsheetId: id, + range: `'${sheetName}'`, + }); + + const values = response.data.values || []; + + if (format === 'json') { + // Collect data for JSON structure + jsonData[sheetName] = values; + } else { + // Add sheet name as context + content += `Sheet Name: ${sheetName}\n`; + + if (values.length === 0) { + content += '(Empty sheet)\n'; + } else { + // Process each row + values.forEach((row) => { + if (format === 'csv') { + // Convert to CSV format + const csvRow = row.map(cell => { + // Escape quotes and wrap in quotes if contains comma or quotes + const cellStr = String(cell || ''); + if (cellStr.includes(',') || cellStr.includes('"') || cellStr.includes('\n')) { + return `"${cellStr.replace(/"/g, '""')}"`; + } + return cellStr; + }).join(','); + content += csvRow + '\n'; + } else { + // Plain text format with pipe separators for readability + content += row.map(cell => cell || '').join(' | ') + '\n'; + } + }); + } + content += '\n'; + } + } catch (sheetError) { + logToFile(`[SheetsService] Error reading sheet ${sheetName}: ${sheetError}`); + if (format === 'json') { + // For JSON format, we'll skip sheets with errors + logToFile(`[SheetsService] Skipping sheet ${sheetName} in JSON output due to error`); + } else { + content += `Sheet Name: ${sheetName}\n(Error reading sheet)\n\n`; + } + } + } + + if (format === 'json') { + // Generate clean JSON output from collected data + content = JSON.stringify(jsonData, null, 2); + } + + logToFile(`[SheetsService] Finished getText for spreadsheet: ${id}`); + return { + content: [{ + type: "text" as const, + text: content.trim() + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[SheetsService] Error during sheets.getText: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + public getRange = async ({ spreadsheetId, range }: { spreadsheetId: string, range: string }) => { + logToFile(`[SheetsService] Starting getRange for spreadsheet: ${spreadsheetId}, range: ${range}`); + try { + const id = extractDocId(spreadsheetId) || spreadsheetId; + + const response = await this.sheets.spreadsheets.values.get({ + spreadsheetId: id, + range: range, + }); + + const values = response.data.values || []; + + logToFile(`[SheetsService] Finished getRange for spreadsheet: ${id}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + range: response.data.range, + values: values + }) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[SheetsService] Error during sheets.getRange: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + public find = async ({ query, pageToken, pageSize = 10 }: { query: string, pageToken?: string, pageSize?: number }) => { + logToFile(`[SheetsService] Searching for spreadsheets with query: ${query}`); + try { + const q = buildDriveSearchQuery(MIME_TYPES.SPREADSHEET, query); + logToFile(`[SheetsService] Executing Drive API query: ${q}`); + + const res = await this.drive.files.list({ + pageSize: pageSize, + fields: 'nextPageToken, files(id, name)', + q: q, + pageToken: pageToken, + }); + + const files = res.data.files || []; + const nextPageToken = res.data.nextPageToken; + + logToFile(`[SheetsService] Found ${files.length} spreadsheets.`); + + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + files: files, + nextPageToken: nextPageToken + }) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[SheetsService] Error during sheets.find: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + public getMetadata = async ({ spreadsheetId }: { spreadsheetId: string }) => { + logToFile(`[SheetsService] Starting getMetadata for spreadsheet: ${spreadsheetId}`); + try { + const id = extractDocId(spreadsheetId) || spreadsheetId; + + const spreadsheet = await this.sheets.spreadsheets.get({ + spreadsheetId: id, + includeGridData: false, + }); + + const metadata = { + spreadsheetId: spreadsheet.data.spreadsheetId, + title: spreadsheet.data.properties?.title, + sheets: spreadsheet.data.sheets?.map(sheet => ({ + sheetId: sheet.properties?.sheetId, + title: sheet.properties?.title, + index: sheet.properties?.index, + rowCount: sheet.properties?.gridProperties?.rowCount, + columnCount: sheet.properties?.gridProperties?.columnCount, + })), + locale: spreadsheet.data.properties?.locale, + timeZone: spreadsheet.data.properties?.timeZone, + }; + + logToFile(`[SheetsService] Finished getMetadata for spreadsheet: ${id}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(metadata) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[SheetsService] Error during sheets.getMetadata: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } +} diff --git a/workspace-mcp-server/src/services/SlidesService.ts b/workspace-mcp-server/src/services/SlidesService.ts new file mode 100644 index 00000000..1bb35f18 --- /dev/null +++ b/workspace-mcp-server/src/services/SlidesService.ts @@ -0,0 +1,195 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { google, slides_v1, drive_v3, Auth } from 'googleapis'; +import { AuthManager } from '../auth/AuthManager'; +import { logToFile } from '../utils/logger'; +import { extractDocId } from '../utils/IdUtils'; +import { gaxiosOptions } from '../utils/GaxiosConfig'; +import { buildDriveSearchQuery, MIME_TYPES } from '../utils/DriveQueryBuilder'; + +export class SlidesService { + private slides: slides_v1.Slides; + private drive: drive_v3.Drive; + + constructor(private authManager: AuthManager) { + this.slides = {} as slides_v1.Slides; + this.drive = {} as drive_v3.Drive; + } + + public async initialize(): Promise { + const auth: Auth.OAuth2Client = await this.authManager.getAuthenticatedClient(); + const options = { ...gaxiosOptions, auth }; + this.slides = google.slides({ version: 'v1', ...options }); + this.drive = google.drive({ version: 'v3', ...options }); + } + + public getText = async ({ presentationId }: { presentationId: string }) => { + logToFile(`[SlidesService] Starting getText for presentation: ${presentationId}`); + try { + const id = extractDocId(presentationId) || presentationId; + + // Get the presentation with all necessary fields + const presentation = await this.slides.presentations.get({ + presentationId: id, + fields: 'title,slides(pageElements(shape(text,shapeProperties),table(tableRows(tableCells(text)))))', + }); + + let content = ''; + + // Add presentation title + if (presentation.data.title) { + content += `Presentation Title: ${presentation.data.title}\n\n`; + } + + // Process each slide + if (presentation.data.slides) { + presentation.data.slides.forEach((slide, slideIndex) => { + content += `\n--- Slide ${slideIndex + 1} ---\n`; + + if (slide.pageElements) { + slide.pageElements.forEach(element => { + // Extract text from shapes + if (element.shape && element.shape.text) { + const shapeText = this.extractTextFromTextContent(element.shape.text); + if (shapeText) { + content += shapeText + '\n'; + } + } + + // Extract text from tables + if (element.table && element.table.tableRows) { + content += '\n--- Table Data ---\n'; + element.table.tableRows.forEach(row => { + const rowText: string[] = []; + if (row.tableCells) { + row.tableCells.forEach(cell => { + const cellText = cell.text ? this.extractTextFromTextContent(cell.text) : ''; + rowText.push(cellText.trim()); + }); + } + content += rowText.join(' | ') + '\n'; + }); + content += '--- End Table Data ---\n'; + } + }); + } + content += '\n'; + }); + } + + logToFile(`[SlidesService] Finished getText for presentation: ${id}`); + return { + content: [{ + type: "text" as const, + text: content.trim() + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[SlidesService] Error during slides.getText: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + private extractTextFromTextContent(textContent: slides_v1.Schema$TextContent): string { + let text = ''; + if (textContent.textElements) { + textContent.textElements.forEach(element => { + if (element.textRun && element.textRun.content) { + text += element.textRun.content; + } else if (element.paragraphMarker) { + // Add newline for paragraph markers + text += '\n'; + } + }); + } + return text; + } + + public find = async ({ query, pageToken, pageSize = 10 }: { query: string, pageToken?: string, pageSize?: number }) => { + logToFile(`[SlidesService] Searching for presentations with query: ${query}`); + try { + const q = buildDriveSearchQuery(MIME_TYPES.PRESENTATION, query); + logToFile(`[SlidesService] Executing Drive API query: ${q}`); + + const res = await this.drive.files.list({ + pageSize: pageSize, + fields: 'nextPageToken, files(id, name)', + q: q, + pageToken: pageToken, + }); + + const files = res.data.files || []; + const nextPageToken = res.data.nextPageToken; + + logToFile(`[SlidesService] Found ${files.length} presentations.`); + + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ + files: files, + nextPageToken: nextPageToken + }) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[SlidesService] Error during slides.find: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + public getMetadata = async ({ presentationId }: { presentationId: string }) => { + logToFile(`[SlidesService] Starting getMetadata for presentation: ${presentationId}`); + try { + const id = extractDocId(presentationId) || presentationId; + + const presentation = await this.slides.presentations.get({ + presentationId: id, + fields: 'presentationId,title,slides(objectId),pageSize,notesMaster,masters,layouts', + }); + + const metadata = { + presentationId: presentation.data.presentationId, + title: presentation.data.title, + slideCount: presentation.data.slides?.length || 0, + pageSize: presentation.data.pageSize, + hasMasters: !!presentation.data.masters?.length, + hasLayouts: !!presentation.data.layouts?.length, + hasNotesMaster: !!presentation.data.notesMaster, + }; + + logToFile(`[SlidesService] Finished getMetadata for presentation: ${id}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(metadata) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`[SlidesService] Error during slides.getMetadata: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } +} diff --git a/workspace-mcp-server/src/services/TimeService.ts b/workspace-mcp-server/src/services/TimeService.ts new file mode 100644 index 00000000..65ef5144 --- /dev/null +++ b/workspace-mcp-server/src/services/TimeService.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { logToFile } from '../utils/logger'; + +export class TimeService { + constructor() { + logToFile('TimeService initialized.'); + } + + private async handleErrors(fn: () => Promise): Promise<{ content: [{ type: "text"; text: string; }] }> { + try { + const result = await fn(); + return { + content: [{ + type: "text" as const, + text: JSON.stringify(result) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logToFile(`Error in TimeService: ${errorMessage}`); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errorMessage }) + }] + }; + } + } + + getCurrentDate = async () => { + logToFile('getCurrentDate called'); + return this.handleErrors(async () => { + return { date: new Date().toISOString().slice(0, 10) }; + }); + } + + getCurrentTime = async () => { + logToFile('getCurrentTime called'); + return this.handleErrors(async () => { + return { time: new Date().toISOString().slice(11, 19) }; + }); + } + + getTimeZone = async () => { + logToFile('getTimeZone called'); + return this.handleErrors(async () => { + return { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }; + }); + } +} diff --git a/workspace-mcp-server/src/utils/DriveQueryBuilder.ts b/workspace-mcp-server/src/utils/DriveQueryBuilder.ts new file mode 100644 index 00000000..7fda7dce --- /dev/null +++ b/workspace-mcp-server/src/utils/DriveQueryBuilder.ts @@ -0,0 +1,57 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Utility for building Google Drive API search queries + */ + +/** + * Builds a Drive API search query for a specific MIME type with optional title filtering + * @param mimeType The MIME type to search for (e.g., 'application/vnd.google-apps.document') + * @param query The search query, may include 'title:' prefix for title-only searches + * @returns The formatted Drive API query string + */ +export function buildDriveSearchQuery(mimeType: string, query: string): string { + let searchTerm = query; + const titlePrefix = 'title:'; + let q: string; + + if (searchTerm.trim().startsWith(titlePrefix)) { + // Extract search term after 'title:' prefix + searchTerm = searchTerm.trim().substring(titlePrefix.length).trim(); + + // Remove surrounding quotes if present + if ((searchTerm.startsWith("'") && searchTerm.endsWith("'")) || + (searchTerm.startsWith('"') && searchTerm.endsWith('"'))) { + searchTerm = searchTerm.substring(1, searchTerm.length - 1); + } + + // Search by name (title) only + q = `mimeType='${mimeType}' and name contains '${escapeQueryString(searchTerm)}'`; + } else { + // Search full text content + q = `mimeType='${mimeType}' and fullText contains '${escapeQueryString(searchTerm)}'`; + } + + return q; +} + +/** + * Escapes special characters in a query string for Drive API + * @param str The string to escape + * @returns The escaped string + */ +export function escapeQueryString(str: string): string { + return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); +} + +// Export MIME type constants for convenience +export const MIME_TYPES = { + DOCUMENT: 'application/vnd.google-apps.document', + PRESENTATION: 'application/vnd.google-apps.presentation', + SPREADSHEET: 'application/vnd.google-apps.spreadsheet', + FOLDER: 'application/vnd.google-apps.folder', +} as const; diff --git a/workspace-mcp-server/src/utils/GaxiosConfig.ts b/workspace-mcp-server/src/utils/GaxiosConfig.ts new file mode 100644 index 00000000..1689b00c --- /dev/null +++ b/workspace-mcp-server/src/utils/GaxiosConfig.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { GaxiosOptions } from 'gaxios'; +import { logToFile } from './logger'; + +export const gaxiosOptions: GaxiosOptions = { + retryConfig: { + retry: 3, + noResponseRetries: 3, + retryDelay: 1000, + httpMethodsToRetry: ['GET', 'HEAD', 'OPTIONS', 'DELETE', 'PUT'], + statusCodesToRetry: [ + [429, 429], + [500, 599], + ], + onRetryAttempt: (err) => { + const config = err.config as GaxiosOptions; + logToFile(`Retrying request to ${config.url}, attempt #${config.retryConfig?.currentRetryAttempt}`); + logToFile(`Error: ${err.message}`); + } + }, + timeout: 30000, +}; + +// Extended timeout for media upload operations +export const mediaUploadOptions: GaxiosOptions = { + ...gaxiosOptions, + timeout: 60000, // 60 seconds for media uploads +}; diff --git a/workspace-mcp-server/src/utils/IdUtils.ts b/workspace-mcp-server/src/utils/IdUtils.ts new file mode 100644 index 00000000..33b037f4 --- /dev/null +++ b/workspace-mcp-server/src/utils/IdUtils.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { logToFile } from './logger'; + +const DOC_ID_REGEX = /\/d\/([a-zA-Z0-9-_]+)/; + +/** + * Extracts a Google Doc/Sheet/etc. ID from a Google Workspace URL. + * + * @param url The URL to parse. + * @returns The extracted document ID, or undefined if no ID could be found. + */ +export function extractDocId(url: string): string | undefined { + logToFile(`[IdUtils] Attempting to extract doc ID from URL: ${url}`); + if (!url || typeof url !== 'string') { + logToFile(`[IdUtils] Invalid input: URL is null or not a string.`); + return undefined; + } + const match = url.match(DOC_ID_REGEX); + if (match && match[1]) { + const docId = match[1]; + logToFile(`[IdUtils] Successfully extracted doc ID: ${docId}`); + return docId; + } + logToFile(`[IdUtils] Could not extract doc ID from URL.`); + return undefined; +} diff --git a/workspace-mcp-server/src/utils/MimeHelper.ts b/workspace-mcp-server/src/utils/MimeHelper.ts new file mode 100644 index 00000000..f67fff77 --- /dev/null +++ b/workspace-mcp-server/src/utils/MimeHelper.ts @@ -0,0 +1,188 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Helper class for creating RFC 2822 compliant MIME messages for Gmail API + */ +export class MimeHelper { + /** + * Creates a base64url-encoded MIME message for Gmail API + */ + public static createMimeMessage({ + to, + subject, + body, + from, + cc, + bcc, + replyTo, + isHtml = false + }: { + to: string; + subject: string; + body: string; + from?: string; + cc?: string; + bcc?: string; + replyTo?: string; + isHtml?: boolean; + }): string { + // Encode subject for UTF-8 support + const utf8Subject = `=?utf-8?B?${Buffer.from(subject).toString('base64')}?=`; + + // Build message headers + const messageParts: string[] = []; + + // Add From header if provided, otherwise Gmail will use the authenticated user + if (from) { + messageParts.push(`From: ${from}`); + } + + messageParts.push(`To: ${to}`); + + if (cc) { + messageParts.push(`Cc: ${cc}`); + } + + if (bcc) { + messageParts.push(`Bcc: ${bcc}`); + } + + if (replyTo) { + messageParts.push(`Reply-To: ${replyTo}`); + } + + messageParts.push(`Subject: ${utf8Subject}`); + + // Add content type based on whether it's HTML or plain text + if (isHtml) { + messageParts.push('Content-Type: text/html; charset=utf-8'); + } else { + messageParts.push('Content-Type: text/plain; charset=utf-8'); + } + + messageParts.push(''); // Empty line between headers and body + messageParts.push(body); + + // Join all parts with CRLF as per RFC 2822 + const message = messageParts.join('\r\n'); + + // Encode to base64url format required by Gmail API + const encodedMessage = Buffer.from(message) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + + return encodedMessage; + } + + /** + * Creates a MIME message with attachments + */ + public static createMimeMessageWithAttachments({ + to, + subject, + body, + from, + cc, + bcc, + attachments, + isHtml = false + }: { + to: string; + subject: string; + body: string; + from?: string; + cc?: string; + bcc?: string; + attachments?: Array<{ + filename: string; + content: Buffer | string; + contentType?: string; + }>; + isHtml?: boolean; + }): string { + const boundary = `boundary_${Date.now()}_${Math.random().toString(36).substring(7)}`; + const utf8Subject = `=?utf-8?B?${Buffer.from(subject).toString('base64')}?=`; + + const messageParts: string[] = []; + + // Headers + if (from) { + messageParts.push(`From: ${from}`); + } + messageParts.push(`To: ${to}`); + if (cc) { + messageParts.push(`Cc: ${cc}`); + } + if (bcc) { + messageParts.push(`Bcc: ${bcc}`); + } + messageParts.push(`Subject: ${utf8Subject}`); + messageParts.push('MIME-Version: 1.0'); + + if (!attachments || attachments.length === 0) { + // Simple message without attachments + return this.createMimeMessage({ to, subject, body, from, cc, bcc, isHtml }); + } + + // Multipart message with attachments + messageParts.push(`Content-Type: multipart/mixed; boundary="${boundary}"`); + messageParts.push(''); + + // Body part + messageParts.push(`--${boundary}`); + if (isHtml) { + messageParts.push('Content-Type: text/html; charset=utf-8'); + } else { + messageParts.push('Content-Type: text/plain; charset=utf-8'); + } + messageParts.push(''); + messageParts.push(body); + + // Attachments + for (const attachment of attachments) { + messageParts.push(`--${boundary}`); + messageParts.push(`Content-Type: ${attachment.contentType || 'application/octet-stream'}`); + messageParts.push('Content-Transfer-Encoding: base64'); + messageParts.push(`Content-Disposition: attachment; filename="${attachment.filename}"`); + messageParts.push(''); + + const content = typeof attachment.content === 'string' + ? attachment.content + : attachment.content.toString('base64'); + + // Add content in chunks of 76 characters as per MIME spec + const chunks = content.match(/.{1,76}/g) || []; + messageParts.push(...chunks); + } + + // End boundary + messageParts.push(`--${boundary}--`); + + const message = messageParts.join('\r\n'); + + // Encode to base64url + return Buffer.from(message) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + } + + /** + * Decodes a base64url-encoded string (inverse of encoding) + */ + public static decodeBase64Url(encoded: string): string { + // Add back padding if needed + let base64 = encoded.replace(/-/g, '+').replace(/_/g, '/'); + while (base64.length % 4) { + base64 += '='; + } + return Buffer.from(base64, 'base64').toString('utf-8'); + } +} \ No newline at end of file diff --git a/workspace-mcp-server/src/utils/constants.ts b/workspace-mcp-server/src/utils/constants.ts new file mode 100644 index 00000000..882c66e6 --- /dev/null +++ b/workspace-mcp-server/src/utils/constants.ts @@ -0,0 +1,7 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export const GMAIL_SEARCH_MAX_RESULTS = 100; diff --git a/workspace-mcp-server/src/utils/logger.ts b/workspace-mcp-server/src/utils/logger.ts new file mode 100644 index 00000000..e4523edd --- /dev/null +++ b/workspace-mcp-server/src/utils/logger.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; + +// Navigate up two levels from src/utils to get to the project root +const projectRoot = path.join(__dirname, '..', '..'); +const logFilePath = path.join(projectRoot, 'logs', 'server.log'); + +async function ensureLogDirectoryExists() { + try { + await fs.mkdir(path.dirname(logFilePath), { recursive: true }); + } catch (error) { + // If we can't create the log directory, log to console as a fallback. + console.error('Could not create log directory:', error); + } +} + +// Ensure the directory exists when the module is loaded. +ensureLogDirectoryExists(); + +let isLoggingEnabled = false; + +export function setLoggingEnabled(enabled: boolean) { + isLoggingEnabled = enabled; +} + +export function logToFile(message: string) { + if (!isLoggingEnabled) { + return; + } + const timestamp = new Date().toISOString(); + const logMessage = `${timestamp} - ${message}\n`; + + fs.appendFile(logFilePath, logMessage).catch(err => { + // Fallback to console if file logging fails + console.error('Failed to write to log file:', err); + }); +} diff --git a/workspace-mcp-server/src/utils/markdownToDocsRequests.ts b/workspace-mcp-server/src/utils/markdownToDocsRequests.ts new file mode 100644 index 00000000..6a5b21f7 --- /dev/null +++ b/workspace-mcp-server/src/utils/markdownToDocsRequests.ts @@ -0,0 +1,239 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { docs_v1 } from 'googleapis'; +import { marked } from 'marked'; +import { JSDOM } from 'jsdom'; + +interface FormatRange { + start: number; + end: number; + type: 'bold' | 'italic' | 'code' | 'link' | 'heading'; + url?: string; + headingLevel?: number; + isParagraph?: boolean; +} + +interface ParsedMarkdown { + plainText: string; + formattingRequests: docs_v1.Schema$Request[]; +} + +/** + * Parses markdown text and generates Google Docs API requests for formatting. + * Uses the marked library to convert to HTML, then parses the HTML to extract formatting. + */ +export function parseMarkdownToDocsRequests(markdown: string, startIndex: number): ParsedMarkdown { + // Split markdown into lines to handle block elements like headings + const lines = markdown.split('\n'); + const htmlParts: string[] = []; + + for (const line of lines) { + // Check if this is a heading line + const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); + if (headingMatch) { + const level = headingMatch[1].length; + const content = headingMatch[2]; + // Parse inline content within the heading + try { + const inlineHtml = marked.parseInline(content) as string; + htmlParts.push(`${inlineHtml}`); + } catch (error) { + console.error('Markdown parsing failed for heading, falling back to raw content:', error); + htmlParts.push(`${content}`); + } + } else if (line.trim()) { + // For non-heading, non-empty lines, use parseInline + try { + const inlineHtml = marked.parseInline(line) as string; + htmlParts.push(`

${inlineHtml}

`); + } catch (error) { + console.error('Markdown parsing failed for line, falling back to raw content:', error); + htmlParts.push(`

${line}

`); + } + } else { + // Empty lines become paragraph breaks + htmlParts.push(''); + } + } + + // Convert markdown to HTML - handle both block and inline elements + const html = htmlParts.join('\n'); + + // If no conversion happened, return plain text + if (!html || html === markdown) { + return { + plainText: markdown, + formattingRequests: [] + }; + } + + // Parse HTML to extract text and formatting + // Create a wrapper div to handle inline HTML that might not have a parent element + const dom = new JSDOM(`
${html}
`); + const document = dom.window.document; + const wrapper = document.querySelector('div'); + + const formattingRanges: FormatRange[] = []; + let plainText = ''; + let currentPos = 0; + + // Recursive function to process nodes + function processNode(node: Node) { + if (node.nodeType === 3) { // Text node + const text = node.textContent || ''; + plainText += text; + currentPos += text.length; + } else if (node.nodeType === 1) { // Element node + const element = node as HTMLElement; + const tagName = element.tagName.toLowerCase(); + + const start = currentPos; + + // Process children first to get the text content + for (const child of Array.from(node.childNodes)) { + processNode(child); + } + + const end = currentPos; + + // Record formatting based on tag + if (tagName === 'strong' || tagName === 'b') { + formattingRanges.push({ start, end, type: 'bold' }); + } else if (tagName === 'em' || tagName === 'i') { + formattingRanges.push({ start, end, type: 'italic' }); + } else if (tagName === 'code') { + formattingRanges.push({ start, end, type: 'code' }); + } else if (tagName === 'a') { + const href = element.getAttribute('href') || ''; + formattingRanges.push({ start, end, type: 'link', url: href }); + } else if (tagName.match(/^h[1-6]$/)) { + const level = parseInt(tagName.charAt(1)); + // Mark the entire paragraph range for heading style + formattingRanges.push({ start, end, type: 'heading', headingLevel: level, isParagraph: true }); + } else if (tagName === 'p') { + // Add newline after paragraph content if not the last element + const nextSibling = element.nextSibling; + if (nextSibling && nextSibling.nodeType === 1) { + plainText += '\n'; + currentPos += 1; + } + } + } + } + + // Process all nodes + if (wrapper) { + for (const child of Array.from(wrapper.childNodes)) { + processNode(child); + } + } else { + // If parsing failed, just use the plain markdown (no formatting) + plainText = markdown; + } + + // Generate formatting requests + const formattingRequests: docs_v1.Schema$Request[] = []; + + for (const range of formattingRanges) { + const textStyle: docs_v1.Schema$TextStyle = {}; + const fields: string[] = []; + + if (range.type === 'bold') { + textStyle.bold = true; + fields.push('bold'); + } else if (range.type === 'italic') { + textStyle.italic = true; + fields.push('italic'); + } else if (range.type === 'code') { + textStyle.weightedFontFamily = { + fontFamily: 'Courier New', + weight: 400 + }; + textStyle.backgroundColor = { + color: { + rgbColor: { + red: 0.95, + green: 0.95, + blue: 0.95 + } + } + }; + fields.push('weightedFontFamily', 'backgroundColor'); + } else if (range.type === 'link' && range.url) { + textStyle.link = { + url: range.url + }; + textStyle.foregroundColor = { + color: { + rgbColor: { + red: 0.06, + green: 0.33, + blue: 0.80 + } + } + }; + textStyle.underline = true; + fields.push('link', 'foregroundColor', 'underline'); + } else if (range.type === 'heading' && range.headingLevel && range.isParagraph) { + // Use updateParagraphStyle for headings as per Google Docs API best practices + const headingStyles: { [key: number]: string } = { + 1: 'HEADING_1', + 2: 'HEADING_2', + 3: 'HEADING_3', + 4: 'HEADING_4', + 5: 'HEADING_5', + 6: 'HEADING_6' + }; + + const namedStyleType = headingStyles[range.headingLevel] || 'HEADING_1'; + + // Create a separate updateParagraphStyle request for headings + formattingRequests.push({ + updateParagraphStyle: { + paragraphStyle: { + namedStyleType: namedStyleType + }, + range: { + startIndex: startIndex + range.start, + endIndex: startIndex + range.end + }, + fields: 'namedStyleType' + } + }); + + // Skip the normal text style formatting for headings + continue; + } + + if (fields.length > 0) { + formattingRequests.push({ + updateTextStyle: { + range: { + startIndex: startIndex + range.start, + endIndex: startIndex + range.end + }, + textStyle: textStyle, + fields: fields.join(',') + } + }); + } + } + + return { + plainText, + formattingRequests + }; +} + +/** + * Handles line breaks and paragraphs in markdown text + */ +export function processMarkdownLineBreaks(text: string): string { + // Convert double line breaks to paragraph breaks + // Single line breaks remain as-is + return text.replace(/\n\n+/g, '\n\n'); +} \ No newline at end of file diff --git a/workspace-mcp-server/src/utils/open-wrapper.ts b/workspace-mcp-server/src/utils/open-wrapper.ts new file mode 100644 index 00000000..3575e860 --- /dev/null +++ b/workspace-mcp-server/src/utils/open-wrapper.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * This module acts as a drop-in replacement for the 'open' package. + * It intercepts browser launch requests and either: + * 1. Opens the browser securely using our secure-browser-launcher + * 2. Prints the URL to console if browser launch should be skipped or fails + */ + +import { openBrowserSecurely, shouldLaunchBrowser } from './secure-browser-launcher'; + +// Create a mock child process object that matches what open returns +const createMockChildProcess = () => ({ + unref: () => {}, + ref: () => {}, + pid: 123, + stdout: null, + stderr: null, + stdin: null, + channel: null, + connected: false, + exitCode: 0, + killed: false, + signalCode: null, + spawnargs: [], + spawnfile: '', +}); + +const openWrapper = async (url: string): Promise => { + // Check if we should launch the browser + if (!shouldLaunchBrowser()) { + console.log(`Browser launch not supported. Please open this URL in your browser: ${url}`); + return createMockChildProcess(); + } + + // Try to open the browser securely + try { + await openBrowserSecurely(url); + return createMockChildProcess(); + } catch { + console.log(`Failed to open browser. Please open this URL in your browser: ${url}`); + return createMockChildProcess(); + } +}; + +// Use standard ES Module export and let the compiler generate the CommonJS correct output. +export default openWrapper; \ No newline at end of file diff --git a/workspace-mcp-server/src/utils/paths.ts b/workspace-mcp-server/src/utils/paths.ts new file mode 100644 index 00000000..0b2e7781 --- /dev/null +++ b/workspace-mcp-server/src/utils/paths.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import path from 'node:path'; + +// Construct an absolute path to the project root. +// __dirname will be /path/to/project/src (in dev) or /path/to/project/dist (in prod). +// In both cases, going up one level gives us the project root. +export const PROJECT_ROOT = path.join(__dirname, '..', '..'); +export const ENCRYPTED_TOKEN_PATH = path.join( + PROJECT_ROOT, + 'gemini-cli-workspace-token.json', +); +export const ENCRYPTION_MASTER_KEY_PATH = path.join( + PROJECT_ROOT, + '.gemini-cli-workspace-master-key', +); diff --git a/workspace-mcp-server/src/utils/secure-browser-launcher.ts b/workspace-mcp-server/src/utils/secure-browser-launcher.ts new file mode 100644 index 00000000..6126d44e --- /dev/null +++ b/workspace-mcp-server/src/utils/secure-browser-launcher.ts @@ -0,0 +1,232 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { execFile, ExecFileOptions } from 'node:child_process'; +import { platform } from 'node:os'; +import { URL } from 'node:url'; + + +function withTimeout(promise: Promise, ms: number): Promise { + let timeoutId: NodeJS.Timeout; + const timeout = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error('Timeout')), ms); + }); + return Promise.race([promise, timeout]).finally(() => clearTimeout(timeoutId)); +} + +/** + * Validates that a URL is safe to open in a browser. + * Only allows HTTP and HTTPS URLs to prevent command injection. + * + * @param url The URL to validate + * @throws Error if the URL is invalid or uses an unsafe protocol + */ +function validateUrl(url: string): void { + let parsedUrl: URL; + + try { + parsedUrl = new URL(url); + } catch { + throw new Error('Invalid URL'); + } + + // Only allow HTTP and HTTPS protocols + if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { + throw new Error( + `Unsafe protocol: ${parsedUrl.protocol}. Only HTTP and HTTPS are allowed.` + ); + } + + // Additional validation: ensure no newlines or control characters + if (/[\r\n\x00-\x1F]/.test(url)) { + throw new Error('URL contains invalid characters'); + } +} + +/** + * Opens a URL in the default browser using platform-specific commands. + * This implementation avoids shell injection vulnerabilities by: + * 1. Validating the URL to ensure it's HTTP/HTTPS only + * 2. Using execFile instead of exec to avoid shell interpretation + * 3. Passing the URL as an argument rather than constructing a command string + * + * @param url The URL to open + * @param execFileFn The function to execute a command. Defaults to node's execFile. + * @throws Error if the URL is invalid or if opening the browser fails + */ +export async function openBrowserSecurely( + url: string, + execFileFn: typeof execFile = execFile +): Promise { + // Validate the URL first + validateUrl(url); + + const platformName = platform(); + let command: string; + let args: string[]; + + switch (platformName) { + case 'darwin': + // macOS + command = 'open'; + args = [url]; + break; + + case 'win32': + // Windows - use PowerShell with Start-Process + // This avoids the cmd.exe shell which is vulnerable to injection + command = 'powershell.exe'; + args = [ + '-NoProfile', + '-NonInteractive', + '-WindowStyle', + 'Hidden', + '-Command', + `Start-Process '${url.replace(/'/g, "''")}'`, + ]; + break; + + case 'linux': + case 'freebsd': + case 'openbsd': + // Linux and BSD variants + // Try xdg-open first, fall back to other options + command = 'xdg-open'; + args = [url]; + break; + + default: + throw new Error(`Unsupported platform: ${platformName}`); + } + + const options: Record = { + // Don't inherit parent's environment to avoid potential issues + env: { + ...process.env, + // Ensure we're not in a shell that might interpret special characters + SHELL: undefined, + }, + // Detach the browser process so it doesn't block + detached: true, + stdio: 'ignore', + }; + + const tryCommand = (cmd: string, cmdArgs: string[]): Promise => { + return new Promise((resolve, reject) => { + const child = execFileFn( + cmd, + cmdArgs, + options as ExecFileOptions, + (error) => { + if (error) { + // This callback handles errors after the process has run, + // but for our case, the 'error' event is more important for spawn failures. + reject(error); + } + } + ); + + // The 'error' event is critical. It fires if the command cannot be found or spawned. + child.on('error', (error) => { + reject(error); + }); + + // If the process spawns successfully, 'xdg-open' and similar commands + // exit almost immediately. We don't need to wait for the browser to close. + // We can consider the job done if the process exits with code 0. + child.on('exit', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Process exited with code ${code}`)); + } + }); + }); + }; + + try { + await withTimeout(tryCommand(command, args), 5000); + } catch (error) { + // For Linux, try fallback commands if xdg-open fails + if ( + (platformName === 'linux' || + platformName === 'freebsd' || + platformName === 'openbsd') && + command === 'xdg-open' + ) { + const fallbackCommands = [ + 'gnome-open', + 'kde-open', + 'firefox', + 'chromium', + 'google-chrome', + ]; + + for (const fallbackCommand of fallbackCommands) { + try { + await withTimeout(tryCommand(fallbackCommand, [url]), 5000); + return; // Success! + } catch { + // Try next command + continue; + } + } + } + + // Re-throw the error if all attempts failed + throw new Error( + `Failed to open browser: ${ + error instanceof Error ? error.message : 'Unknown error' + }` + ); + } +} + +/** + * Checks if the current environment should attempt to launch a browser. + * This is the same logic as in browser.ts for consistency. + * + * @returns True if the tool should attempt to launch a browser + */ +export function shouldLaunchBrowser(): boolean { + // A list of browser names that indicate we should not attempt to open a + // web browser for the user. + const browserBlocklist = ['www-browser']; + const browserEnv = process.env.BROWSER; + if (browserEnv && browserBlocklist.includes(browserEnv)) { + return false; + } + + // Common environment variables used in CI/CD or other non-interactive shells. + if (process.env.CI || process.env.DEBIAN_FRONTEND === 'noninteractive') { + return false; + } + + // The presence of SSH_CONNECTION indicates a remote session. + // We should not attempt to launch a browser unless a display is explicitly available + // (checked below for Linux). + const isSSH = !!process.env.SSH_CONNECTION; + + // On Linux, the presence of a display server is a strong indicator of a GUI. + if (platform() === 'linux') { + // These are environment variables that can indicate a running compositor on Linux. + const displayVariables = ['DISPLAY', 'WAYLAND_DISPLAY', 'MIR_SOCKET']; + const hasDisplay = displayVariables.some((v) => !!process.env[v]); + if (!hasDisplay) { + return false; + } + } + + // If in an SSH session on a non-Linux OS (e.g., macOS), don't launch browser. + // The Linux case is handled above (it's allowed if DISPLAY is set). + if (isSSH && platform() !== 'linux') { + return false; + } + + // For non-Linux OSes, we generally assume a GUI is available + // unless other signals (like SSH) suggest otherwise. + return true; +} diff --git a/workspace-mcp-server/src/utils/validation.ts b/workspace-mcp-server/src/utils/validation.ts new file mode 100644 index 00000000..a33d499b --- /dev/null +++ b/workspace-mcp-server/src/utils/validation.ts @@ -0,0 +1,182 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { z } from 'zod'; + +/** + * Email validation schema + * Validates email format according to RFC 5322 + */ +export const emailSchema = z.string().email('Invalid email format'); + +/** + * Validates multiple email addresses (for CC/BCC fields) + */ +export const emailArraySchema = z.union([ + emailSchema, + z.array(emailSchema) +]); + +/** + * ISO 8601 datetime validation schema + * Accepts formats like: + * - 2024-01-15T10:30:00Z + * - 2024-01-15T10:30:00-05:00 + * - 2024-01-15T10:30:00.000Z + */ +export const iso8601DateTimeSchema = z.string().refine( + (val) => { + const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|[+-]\d{2}:\d{2})$/; + if (!iso8601Regex.test(val)) return false; + + // Additional check: ensure it's a valid date + const date = new Date(val); + return !isNaN(date.getTime()); + }, + { + message: 'Invalid ISO 8601 datetime format. Expected format: YYYY-MM-DDTHH:mm:ss[.sss][Z|±HH:mm]' + } +); + +/** + * Google Drive document/file ID validation + * Google IDs are typically alphanumeric strings with hyphens and underscores + */ +export const googleDocumentIdSchema = z.string().regex( + /^[a-zA-Z0-9_-]+$/, + 'Invalid document ID format. Document IDs should only contain letters, numbers, hyphens, and underscores' +); + +/** + * Google Drive URL validation + * Accepts various Google Workspace URLs and extracts the document ID + */ +export const googleWorkspaceUrlSchema = z.string().regex( + /^https:\/\/(docs|drive|sheets|slides)\.google\.com\/.+\/d\/([a-zA-Z0-9_-]+)/, + 'Invalid Google Workspace URL format' +); + +/** + * Folder name validation + * Prevents problematic characters in folder names + */ +export const folderNameSchema = z.string() + .min(1, 'Folder name cannot be empty') + .max(255, 'Folder name too long (max 255 characters)') + .refine( + (val) => !(/[<>:"/\\|?*\x00-\x1F]/.test(val)), + 'Folder name contains invalid characters' + ); + +/** + * Calendar ID validation + * Can be 'primary' or an email address + */ +export const calendarIdSchema = z.union([ + z.literal('primary'), + emailSchema +]); + +/** + * Search query sanitization + * Escapes potentially dangerous characters from search queries + * Preserves quotes for exact phrase searching + */ +export const searchQuerySchema = z.string() + .transform((val) => { + // Escape backslashes first, then escape quotes + // This preserves the ability to search for exact phrases + return val + .replace(/\\/g, '\\\\') // Escape backslashes + .replace(/'/g, "\\'") // Escape single quotes + .replace(/"/g, '\\"'); // Escape double quotes + }); + +/** + * Page size validation for pagination + */ +export const pageSizeSchema = z.number() + .int('Page size must be an integer') + .min(1, 'Page size must be at least 1') + .max(100, 'Page size cannot exceed 100'); + + +/** + * Helper function to validate email + */ +export function validateEmail(email: string): { success: boolean; error?: string } { + try { + emailSchema.parse(email); + return { success: true }; + } catch (error) { + if (error instanceof z.ZodError) { + return { success: false, error: error.errors[0].message }; + } + return { success: false, error: 'Invalid email format' }; + } +} + +/** + * Helper function to validate ISO 8601 datetime + */ +export function validateDateTime(datetime: string): { success: boolean; error?: string } { + try { + iso8601DateTimeSchema.parse(datetime); + return { success: true }; + } catch (error) { + if (error instanceof z.ZodError) { + return { success: false, error: error.errors[0].message }; + } + return { success: false, error: 'Invalid datetime format' }; + } +} + +/** + * Helper function to validate Google document ID + */ +export function validateDocumentId(id: string): { success: boolean; error?: string } { + try { + googleDocumentIdSchema.parse(id); + return { success: true }; + } catch (error) { + if (error instanceof z.ZodError) { + return { success: false, error: error.errors[0].message }; + } + return { success: false, error: 'Invalid document ID' }; + } +} + +/** + * Helper function to extract document ID from URL or return the ID if already valid + */ +export function extractDocumentId(urlOrId: string): string { + // First check if it's already a valid ID + if (googleDocumentIdSchema.safeParse(urlOrId).success) { + return urlOrId; + } + + // Try to extract from URL + const urlMatch = urlOrId.match(/\/d\/([a-zA-Z0-9_-]+)/); + if (urlMatch && urlMatch[1]) { + return urlMatch[1]; + } + + throw new Error('Invalid document ID or URL'); +} + +/** + * Validation error class for consistent error handling + */ +export class ValidationError extends Error { + constructor( + message: string, + public field: string, + public value: unknown + ) { + super(message); + this.name = 'ValidationError'; + } +} \ No newline at end of file diff --git a/workspace-mcp-server/tsconfig.json b/workspace-mcp-server/tsconfig.json new file mode 100644 index 00000000..83e60126 --- /dev/null +++ b/workspace-mcp-server/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "dist", + "node_modules", + "src/**/*.test.ts", + "src/**/*.spec.ts" + ] +} \ No newline at end of file diff --git a/workspace-mcp-server/tsconfig.test.json b/workspace-mcp-server/tsconfig.test.json new file mode 100644 index 00000000..4885ae80 --- /dev/null +++ b/workspace-mcp-server/tsconfig.test.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "strict": false, + "noImplicitAny": false + }, + "include": [ + "src/**/*.test.ts", + "src/**/*.spec.ts" + ] +} \ No newline at end of file