diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/add-husky.md b/.changeset/add-husky.md new file mode 100644 index 0000000..4374b11 --- /dev/null +++ b/.changeset/add-husky.md @@ -0,0 +1,5 @@ +--- +"@nylas/connect": patch +--- + +Add Husky git hooks: pre-commit and pre-push run ggshield secret scans. \ No newline at end of file diff --git a/.changeset/add-oxlint-linter.md b/.changeset/add-oxlint-linter.md new file mode 100644 index 0000000..6c20f4b --- /dev/null +++ b/.changeset/add-oxlint-linter.md @@ -0,0 +1,14 @@ +--- +"@nylas/connect": none +--- + +Add Oxlint as the linter across the workspace and standardize scripts/CI. + +- Add `oxlint` as a workspace devDependency +- Use `lint` (auto-fix) and `lint:check` (no fix) across packages +- Configure Nx to cache `lint:check` and skip caching for `lint` +- Update PR workflow to run `pnpm lint:check` + +This is a tooling-only change; no runtime impact. + + diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..d20e7bf --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [], + "snapshot": { + "useCalculatedVersion": true, + "prereleaseTemplate": "{tag}-{datetime}" + } +} diff --git a/.changeset/silent-eggs-dream.md b/.changeset/silent-eggs-dream.md new file mode 100644 index 0000000..0a93143 --- /dev/null +++ b/.changeset/silent-eggs-dream.md @@ -0,0 +1,5 @@ +--- +"@nylas/connect": patch +--- + +Validating the github actions workflow diff --git a/.github/RELEASE.md b/.github/RELEASE.md new file mode 100644 index 0000000..cf81eee --- /dev/null +++ b/.github/RELEASE.md @@ -0,0 +1,98 @@ +# Release Process + +This repository uses [Changesets](https://github.com/changesets/changesets) for automated package versioning and publishing. + +## How It Works + +### 1. Creating Changes +When you make changes that should trigger a release: + +```bash +# Add a changeset describing your changes +pnpm changeset + +# Follow the prompts to: +# - Select which packages are affected +# - Choose the type of change (patch, minor, major) +# - Write a description of the change +``` + +This creates a markdown file in `.changeset/` describing the change. + +### 2. Automated Release Process + +When changesets are pushed to `main`: + +1. **Release PR Creation**: The GitHub Action automatically creates a "Version Packages" PR +2. **Review Process**: The PR shows exactly what will be released and requires review +3. **Publishing**: When the PR is merged, packages are automatically published to NPM +4. **GitHub Releases**: Release notes are automatically created with changelogs + +### 3. Manual Testing + +You can test releases locally: + +```bash +# See what would be published (dry run) +pnpm publish:dry-run + +# Build and publish locally (requires NPM_TOKEN) +pnpm publish +``` + +## Setup Requirements + + +## Changeset Types + +- **patch**: Bug fixes, documentation updates, internal changes +- **minor**: New features, non-breaking changes +- **major**: Breaking changes, API changes + +## Example Workflow + +```bash +# 1. Make your changes +git checkout -b feature/new-auth-method +# ... make changes ... + +# 2. Add changeset +pnpm changeset +# Select: @nylas/connect โ†’ minor โ†’ "Add new OAuth flow support" + +# 3. Commit and push +git add .changeset/ +git commit -m "feat: add new OAuth flow support" +git push origin feature/new-auth-method + +# 4. Create PR and merge to main +# 5. Release PR is automatically created +# 6. Review and merge release PR +# 7. Packages are published automatically! +``` + +## Troubleshooting + +### Release PR Not Created +- Check that changesets exist in `.changeset/` (not just config files) +- Verify the GitHub Action ran successfully +- Ensure you have the required permissions + +### Publishing Fails +- Verify `NPM_TOKEN` secret is set correctly +- Check NPM token has publish permissions for `@nylas` scope +- Ensure package versions don't already exist on NPM + +### Manual Recovery +If automation fails, you can manually release: + +```bash +# Update versions +pnpm version + +# Build and publish +pnpm publish + +# Create git tags +git push --follow-tags +``` diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml new file mode 100644 index 0000000..e2ef065 --- /dev/null +++ b/.github/workflows/pr-tests.yml @@ -0,0 +1,73 @@ +name: PR Tests + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + # Also run on pushes to main for consistency + push: + branches: [main] + +# Cancel in-progress runs for the same PR +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test Suite + runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # Fetch full history for Nx affected commands (optional optimization) + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ vars.NODE_VERSION }} + registry-url: "https://registry.npmjs.org" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: "10.6.3" + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Type check + run: pnpm typecheck + + - name: Lint (check only) + run: pnpm lint:check + + - name: Check formatting + run: pnpm format:check + + - name: Build packages + run: pnpm build + + - name: Run tests + run: pnpm test + + - name: Run tests with coverage + run: pnpm --filter @nylas/connect coverage diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bbb6188 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,87 @@ +name: Release + +on: + push: + branches: + - main + workflow_dispatch: + +# Prevent multiple releases from running at the same time +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + release: + name: Release + runs-on: blacksmith-2vcpu-ubuntu-2404 + permissions: + contents: write # to create release commits and tags + pull-requests: write # to create release PRs + id-token: write # for NPM provenance + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # Need full history for changesets + fetch-depth: 0 + # Use a token that can trigger workflows (for release PR creation) + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ vars.NODE_VERSION }} + registry-url: "https://registry.npmjs.org" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: "10.6.3" + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Type check + run: pnpm typecheck + + - name: Check formatting + run: pnpm format:check + + - name: Build packages + run: pnpm build + + - name: Run tests + run: pnpm test + + - name: Run tests with coverage + run: pnpm --filter @nylas/connect coverage + + - name: Create Release Pull Request or Publish to NPM + id: changesets + uses: changesets/action@v1 + with: + # This expects a script called "version" and "publish" + version: pnpm version + publish: pnpm publish:dry-run + title: "chore: version packages" + commit: "chore: version packages" + createGithubReleases: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_PROVENANCE: true diff --git a/.gitignore b/.gitignore index de18578..e771874 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,14 @@ area51 ###### Docker ###### !.docker/** + + + +.nx/cache +.nx/workspace-data +.cursor/rules/nx-rules.mdc +.github/instructions/nx.instructions.md + +# Nx Project Graph export assets +static/ +project-graph.html \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..460b80f --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +pnpm format +pnpm lint +pnpm typecheck diff --git a/.nxignore b/.nxignore new file mode 100644 index 0000000..d972077 --- /dev/null +++ b/.nxignore @@ -0,0 +1,11 @@ +# Nx ignore patterns +node_modules/ +dist/ +.git/ +.changeset/ +*.log +.DS_Store +coverage/ +.nyc_output/ +*.html +static/ diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..5e6700f --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,20 @@ +{ + "env": { + "browser": true, + "node": true, + "es2022": true + }, + "ignorePatterns": ["node_modules/", "dist/", "coverage/", "**/*.d.ts"], + "rules": { + "no-console": "warn", + "no-debugger": "error", + "eqeqeq": ["error", "always"], + "curly": "error", + "no-var": "error", + "prefer-const": "warn", + "no-unused-vars": [ + "warn", + { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } + ] + } +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index ca6fd7b..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -default_stages: [commit] -repos: - # GitGuardian - - repo: https://github.com/gitguardian/ggshield - rev: v1.27.0 - hooks: - - id: ggshield - language_version: python3 - stages: [commit] diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..876ea09 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,30 @@ +# Dependencies +node_modules/ +**/node_modules/ + +# Build outputs +dist/ +**/dist/ +build/ +**/build/ + +# Package manager files +pnpm-lock.yaml +package-lock.json +yarn.lock + +# Documentation (often has specific formatting) +*.md +**/*.md + +# Generated files +coverage/ +**/coverage/ + +# IDE files +.vscode/ +.idea/ + +# Logs +*.log +**/*.log diff --git a/.prettierrc.mjs b/.prettierrc.mjs new file mode 100644 index 0000000..dd651ca --- /dev/null +++ b/.prettierrc.mjs @@ -0,0 +1,3 @@ +export default { + +}; \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1f4c6b6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,249 @@ +# Contributing to Nylas Web + +๐Ÿ‘‹ Thank you for your interest in contributing to Nylas Web! We're excited to have you as part of our open source community. + +This repository contains modern, developer-friendly JavaScript/TypeScript packages for integrating with the Nylas platform. Your contributions help make email, calendar, and contacts integration easier for developers everywhere. + +## ๐Ÿ“‹ Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Development Setup](#development-setup) +- [How to Contribute](#how-to-contribute) +- [Pull Request Process](#pull-request-process) +- [Testing](#testing) +- [Style Guide](#style-guide) +- [Getting Help](#getting-help) + +## ๐Ÿค Code of Conduct + +This project adheres to a code of conduct that promotes a welcoming and inclusive environment. By participating, you are expected to uphold these standards. Please report unacceptable behavior to the project maintainers. + +## ๐Ÿš€ Getting Started + +### Prerequisites + +- **Node.js**: Version 22 or higher +- **pnpm**: We use pnpm for package management +- **Git**: For version control + +### Development Setup + +1. **Fork and clone the repository** + ```bash + git clone https://github.com/YOUR_USERNAME/web.git + cd web + ``` + +2. **Install dependencies** + ```bash + pnpm install + ``` + +3. **Build all packages** + ```bash + pnpm build + ``` + +4. **Run tests to ensure everything is working** + ```bash + pnpm test + ``` + +### Project Structure + +This is a monorepo containing multiple packages: + +- **`packages/nylas-connect/`** - Modern OAuth connection library for Nylas APIs +- More packages coming soon! + +Each package is independently versioned and published to npm. + +## ๐Ÿ›  How to Contribute + +### Reporting Bugs + +Found a bug? Help us fix it! + +1. **Search existing issues** first to avoid duplicates +2. **Create a new issue** using our bug report template +3. **Include**: + - Clear description of the problem + - Steps to reproduce + - Expected vs actual behavior + - Environment details (Node version, browser, etc.) + - Code samples if applicable + +[**Report a Bug โ†’**](https://github.com/nylas/web/issues/new?template=bug_report.md) + +### Suggesting Features + +Have an idea for improvement? + +1. **Check existing feature requests** to see if it's already been suggested +2. **Create a feature request** with: + - Clear description of the feature + - Use case and benefits + - Possible implementation approach (if you have ideas) + +[**Request a Feature โ†’**](https://github.com/nylas/web/issues/new?template=feature_request.md) + +### Contributing Code + +We welcome code contributions! Here's how to get started: + +#### For Bug Fixes +- Feel free to submit a PR directly +- Reference the issue number in your PR description + +#### For New Features +- **Please open an issue first** to discuss the feature +- This helps ensure alignment with project goals +- Avoids duplicate work + +#### Development Workflow + +1. **Create a feature branch** + ```bash + git checkout -b feature/your-feature-name + ``` + +2. **Make your changes** + - Write clean, well-documented code + - Follow our style guide + - Add tests for new functionality + +3. **Test your changes** + ```bash + pnpm test + pnpm lint + ``` + +4. **Commit your changes** + ```bash + git add . + git commit -m "feat: add amazing new feature" + ``` + +5. **Push and create a PR** + ```bash + git push origin feature/your-feature-name + ``` + +## ๐Ÿ”„ Pull Request Process + +### Before Submitting + +- [ ] Tests pass locally (`pnpm test`) +- [ ] Code follows our style guide (`pnpm lint`) +- [ ] Changes are documented (JSDoc, README updates, etc.) +- [ ] Commit messages follow [conventional commit format](https://conventionalcommits.org/) + +### PR Requirements + +1. **Clear title and description** + - Explain what changes you made and why + - Reference any related issues + +2. **License confirmation** + - All external contributors must include this text in the PR description: + + > "I confirm that this contribution is made under the terms of the MIT license and that I have the authority necessary to make this contribution on behalf of its copyright owner." + +3. **Maintain test coverage** + - New features require tests + - Bug fixes should include regression tests + +### Review Process + +- Maintainers will review your PR as soon as possible +- We may request changes or ask questions +- Once approved, a maintainer will merge your PR + +## ๐Ÿงช Testing + +We use **Vitest** for testing. Our test philosophy: + +- **Unit tests** for individual functions and components +- **Integration tests** for complex workflows +- **High coverage** to ensure reliability + +### Running Tests + +```bash +# Run all tests +pnpm test + +# Run tests in watch mode +pnpm test:watch + +# Run tests for specific package +pnpm --filter @nylas/connect test +``` + +### Writing Tests + +- Place test files next to the code they test (`.test.ts` suffix) +- Use descriptive test names +- Test both happy paths and error cases +- Mock external dependencies appropriately + +## ๐Ÿ“ Style Guide + +### Code Style + +- **ESM only** - No CommonJS support +- **TypeScript** - Strongly typed code +- **Modern JavaScript** - Use latest ES features +- **Functional programming** - Prefer pure functions when possible + +### Formatting + +We use automated formatting tools: + +```bash +# Format code +pnpm format + +# Check formatting +pnpm format:check +``` + +### Commit Messages + +We follow [Conventional Commits](https://conventionalcommits.org/): + +``` +type(scope): description + +feat(connect): add popup authentication flow +fix(connect): resolve token refresh issue +docs(readme): update installation instructions +``` + +Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` + +## ๐Ÿ†˜ Getting Help + +### Questions & Discussions + +- **General questions**: [GitHub Discussions](https://github.com/nylas/web/discussions) +- **Bug reports**: [GitHub Issues](https://github.com/nylas/web/issues) +- **Feature requests**: [GitHub Issues](https://github.com/nylas/web/issues) + +### Nylas Support + +- **Developer documentation**: [developer.nylas.com](https://developer.nylas.com) +- **Community forum**: [discuss.nylas.com](https://discuss.nylas.com) +- **Support email**: support@nylas.com + +--- + +## ๐Ÿ™ Recognition + +Contributors are recognized in our [CONTRIBUTORS.md](./CONTRIBUTORS.md) file and release notes. Thank you for helping make Nylas Web better! + +--- + +*This project is licensed under the MIT License. See [LICENSE](./LICENSE) for details.* + diff --git a/README.md b/README.md index 0802706..2d8380d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,87 @@ -# StandardTemplate -Standard template with branch protections +# Nylas Web + +> Modern, open source JavaScript/TypeScript packages for integrating with the Nylas platform + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Node.js](https://img.shields.io/badge/Node.js-22%2B-green.svg)](https://nodejs.org/) +[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/) + +## ๐Ÿ“ฆ Packages + +This monorepo contains developer-friendly packages for building applications with Nylas APIs. Each package is independently versioned and published to npm. + +### [`@nylas/connect`](./packages/nylas-connect/) + +Modern, secure OAuth connection library for Nylas APIs. + +- **ESM-only**, TypeScript-first, zero runtime dependencies +- Works in modern browsers and Node 22+ +- Automatic session, token, and scope management +- Popup and inline OAuth flows + +[**๐Ÿ“– Documentation โ†’**](./packages/nylas-connect/README.md) + +```bash +npm install @nylas/connect +``` + +--- + +*More packages coming soon! This repository will expand to include additional Nylas integration tools and utilities.* + +## ๐Ÿš€ Quick Start + +1. **Install a package** + ```bash + npm install @nylas/connect + ``` + +2. **Follow the package documentation** + - Each package has comprehensive documentation in its README + - Examples and guides are included + +3. **Get your Nylas credentials** + - Sign up at [nylas.com](https://nylas.com) + - Get your Client ID from the [Nylas Dashboard](https://dashboard.nylas.com) + +## ๐Ÿ›  Development + +This is a monorepo managed with **pnpm workspaces**. + +```bash +# Install dependencies +pnpm install + +# Build all packages +pnpm build + +# Run tests +pnpm test + +# Lint code +pnpm lint +``` + +## ๐Ÿค Contributing + +We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) for details on: + +- Setting up your development environment +- Submitting bug reports and feature requests +- Creating pull requests +- Code style and testing guidelines + +## ๐Ÿ“„ License + +This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. + +## ๐Ÿ†˜ Support + +- **๐Ÿ“š Documentation**: [developer.nylas.com](https://developer.nylas.com) +- **๐Ÿ’ฌ Community**: [GitHub Discussions](https://github.com/nylas/web/discussions) +- **๐Ÿ› Issues**: [GitHub Issues](https://github.com/nylas/web/issues) +- **โœ‰๏ธ Email**: support@nylas.com + +--- + +*Built with โค๏ธ by the Nylas team and open source contributors.* \ No newline at end of file diff --git a/nx.json b/nx.json new file mode 100644 index 0000000..2959c35 --- /dev/null +++ b/nx.json @@ -0,0 +1,57 @@ +{ + "$schema": "./node_modules/nx/schemas/nx-schema.json", + "targetDefaults": { + "build": { + "cache": true, + "inputs": ["default", "^default"], + "outputs": ["{projectRoot}/dist"] + }, + "test": { + "cache": true, + "inputs": ["default", "^default"] + }, + "format": { + "cache": true + }, + "format:check": { + "cache": true + }, + "lint": { + "cache": false + }, + "lint:check": { + "cache": true, + "inputs": ["default", "^default"] + }, + "typecheck": { + "cache": true, + "inputs": ["default", "^default"] + }, + "clean": { + "cache": false + } + }, + "namedInputs": { + "default": ["{projectRoot}/**/*", "sharedGlobals"], + "sharedGlobals": [ + "{workspaceRoot}/pnpm-workspace.yaml", + "{workspaceRoot}/pnpm-lock.yaml", + "{workspaceRoot}/package.json" + ] + }, + "tasksRunnerOptions": { + "default": { + "runner": "nx/tasks-runners/default", + "options": { + "cacheableOperations": [ + "build", + "test", + "format", + "format:check", + "lint:check", + "typecheck" + ] + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5d6869c --- /dev/null +++ b/package.json @@ -0,0 +1,72 @@ +{ + "name": "@nylas/web", + "version": "1.0.0", + "description": "Modern, open source JavaScript/TypeScript packages for integrating with the Nylas platform", + "type": "module", + "private": true, + "engines": { + "node": ">=22.0.0", + "pnpm": ">=9.0.0" + }, + "packageManager": "pnpm@10.6.3", + "scripts": { + "build": "nx run-many -t build", + "test": "nx run-many -t test", + "test:watch": "nx run-many -t test:watch", + "format": "nx run-many -t format", + "format:check": "nx run-many -t format:check", + "lint": "nx run-many -t lint --parallel", + "lint:check": "nx run-many -t lint:check --parallel", + "clean": "nx run-many -t clean", + "dev": "nx run-many -t dev", + "typecheck": "nx run-many -t typecheck", + "changeset": "changeset", + "version": "changeset version", + "publish": "nx run-many -t build && changeset publish", + "publish:dry-run": "nx run-many -t build && changeset publish --dry-run", + "publish:no-build": "changeset publish", + "postinstall": "husky install" + }, + "keywords": [ + "nylas", + "email", + "calendar", + "contacts", + "oauth", + "authentication", + "api", + "sdk", + "typescript", + "javascript", + "esm", + "web", + "browser", + "node", + "integration", + "platform" + ], + "author": "Nylas Inc. ", + "license": "MIT", + "homepage": "https://github.com/nylas/web#readme", + "repository": { + "type": "git", + "url": "https://github.com/nylas/web.git" + }, + "bugs": { + "url": "https://github.com/nylas/web/issues" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nylas" + }, + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "@changesets/cli": "^2.27.9", + "husky": "^8.0.3", + "nx": "21.5.2", + "oxlint": "^1.16.0", + "prettier": "^3.4.2" + } +} diff --git a/packages/nylas-connect/.gitignore b/packages/nylas-connect/.gitignore new file mode 100644 index 0000000..3224a90 --- /dev/null +++ b/packages/nylas-connect/.gitignore @@ -0,0 +1,144 @@ +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# End of https://www.toptal.com/developers/gitignore/api/node \ No newline at end of file diff --git a/packages/nylas-connect/LICENSE.md b/packages/nylas-connect/LICENSE.md new file mode 100644 index 0000000..9c9bbc7 --- /dev/null +++ b/packages/nylas-connect/LICENSE.md @@ -0,0 +1,22 @@ +The MIT License (MIT) +---- + +Copyright (c) 2014-2015 InboxApp, Inc. and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/nylas-connect/README.md b/packages/nylas-connect/README.md new file mode 100644 index 0000000..f7c7f2a --- /dev/null +++ b/packages/nylas-connect/README.md @@ -0,0 +1,214 @@ +# @nylas/connect + +[![npm version](https://img.shields.io/npm/v/@nylas/connect.svg)](https://www.npmjs.com/package/@nylas/connect) +[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +> ๐Ÿš€ Modern, secure, developer-friendly OAuth connection for Nylas APIs + +## Highlights + +- **๐Ÿ”’ Secure by default** - PKCE flow, automatic token management, no secrets in browser +- **โšก Zero dependencies** - Lightweight, fast, and reliable +- **๐ŸŽฏ TypeScript-first** - Full type safety and IntelliSense support +- **๐ŸŒ Universal** - Works in modern browsers and Node.js 18+ +- **๐Ÿ“ฑ Flexible flows** - Popup (recommended) or redirect authentication +- **๐Ÿ’พ Smart persistence** - Automatic session and token storage + +## Install + +```bash +npm install @nylas/connect +``` + +**Prerequisites:** Node.js 18+ and a modern browser + +## Usage + +```typescript +import { NylasConnect } from '@nylas/connect'; + +const nylasConnect = new NylasConnect({ + clientId: 'your-nylas-client-id', + redirectUri: 'http://localhost:3000/auth/callback' +}); + +// Connect with popup (recommended) +const result = await nylasConnect.connect({ method: 'popup' }); +console.log('Connected as:', result.email); +``` + +Environment variables (recommended): + +```typescript +// Use environment variables +const nylasConnect = new NylasConnect(); +// Reads from NYLAS_CLIENT_ID and NYLAS_REDIRECT_URI +``` + +## Connection Methods + +### Popup Flow (Recommended) + +```typescript +const result = await nylasConnect.connect({ method: 'popup' }); +``` + +- User stays in your app +- Seamless experience +- Best for SPAs + +### Redirect Flow + +```typescript +const url = await nylasConnect.connect({ method: 'inline' }); +window.location.href = url; +``` + +- Full page redirect +- Works when popups blocked +- Better for mobile + +### Callback Handler + +```typescript +// At your redirect URI (e.g., /auth/callback) +await nylasConnect.callback(); +``` + +## Environment Setup + +```env +NYLAS_CLIENT_ID=your-nylas-client-id +NYLAS_REDIRECT_URI=http://localhost:3000/auth/callback +``` + +**Note:** With modern bundlers, prefix environment variables: +- Vite: `VITE_NYLAS_CLIENT_ID` +- Next.js: `NEXT_PUBLIC_NYLAS_CLIENT_ID` + +## Session Management + +```typescript +// Check current session +const session = await nylasConnect.getSession(); +if (session) { + console.log('User:', session.grantInfo?.email); +} + +// Logout +await nylasConnect.logout(); +``` + +## Error Handling + +```typescript +try { + await nylasConnect.connect({ method: 'popup' }); +} catch (error) { + console.error('Connection failed:', error.message); +} +``` + +## Configuration + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `clientId` | `string` | - | Nylas Client ID | +| `redirectUri` | `string` | - | OAuth redirect URI | +| `apiUrl` | `string` | `https://api.us.nylas.com` | API base URL | +| `persistTokens` | `boolean` | `true` | Store tokens in localStorage | +| `debug` | `boolean` | `true` on localhost | Enable debug logging | + +## API + +### `connect(options?)` + +Start OAuth flow. Returns `ConnectResult` for popup or URL string for redirect. + +```typescript +// Popup +await nylasConnect.connect({ method: 'popup' }); + +// Redirect +const url = await nylasConnect.connect({ method: 'inline' }); +``` + +### `callback(url?)` + +Handle OAuth callback. Auto-detects current URL if none provided. + +### `getSession(grantId?)` + +Get current session. Returns `null` if no active session. + +### `logout(grantId?)` + +Clear stored tokens and logout. + +## Advanced Usage + +### Backend-Only Flow + +For server-side token exchange: + +```typescript +// Client: build auth URL without PKCE +const { url, state } = await nylasConnect.getAuthUrl(); +window.location.href = url; + +// Server: exchange code using Nylas Node SDK +const { grantId } = await nylas.auth.exchangeCodeForToken({ + clientId: process.env.NYLAS_CLIENT_ID, + clientSecret: process.env.NYLAS_CLIENT_SECRET, + code: req.query.code, + redirectUri: process.env.NYLAS_REDIRECT_URI +}); +``` + +### Custom Scopes + +```typescript +await nylasConnect.connect({ + method: 'popup', + scopes: ['https://www.googleapis.com/auth/gmail.readonly'] +}); +``` + +### Event Handling + +```typescript +const unsubscribe = nylasConnect.onConnectStateChange((event, session) => { + if (event === 'CONNECT_SUCCESS') { + console.log('Connected:', session?.grantInfo?.email); + } +}); + +// Clean up +unsubscribe(); +``` + +## FAQ + +### Popup vs Redirect? + +**Popup:** Better UX, works in SPAs, requires popup permission +**Redirect:** Works everywhere, better for mobile, full page navigation + +### Do I need custom scopes? + +Usually no. Nylas handles default scopes automatically. Override only for specific provider permissions. + +### Which region? + +Match your Nylas account region: +- US: `https://api.us.nylas.com` +- EU: `https://api.eu.nylas.com` + +### Token refresh? + +Automatic. @nylas/connect handles token refresh in the background. + +## License + +MIT ยฉ [Nylas](https://nylas.com) \ No newline at end of file diff --git a/packages/nylas-connect/auth-instance.js b/packages/nylas-connect/auth-instance.js new file mode 100644 index 0000000..7111985 --- /dev/null +++ b/packages/nylas-connect/auth-instance.js @@ -0,0 +1,11 @@ +import { NylasConnect } from "./src/index.ts"; + +// Shared auth configuration +export const auth = new NylasConnect({ + clientId: "3531f8b3-3d7f-4412-a073-2d4aa397508f", + apiUrl: "https://api-staging.us.nylas.com/v3", + redirectUri: + (globalThis.window?.location?.origin || "http://localhost:3000") + + "/callback.html", + provider: "google", +}); diff --git a/packages/nylas-connect/callback.html b/packages/nylas-connect/callback.html new file mode 100644 index 0000000..2ba49b6 --- /dev/null +++ b/packages/nylas-connect/callback.html @@ -0,0 +1,19 @@ + + + + + + Auth Callback + + + + + diff --git a/packages/nylas-connect/index.html b/packages/nylas-connect/index.html new file mode 100644 index 0000000..c70b2f2 --- /dev/null +++ b/packages/nylas-connect/index.html @@ -0,0 +1,31 @@ + + + + + + Nylas Auth - Development + + + +

Open the console to see the output

+ + + diff --git a/packages/nylas-connect/package.json b/packages/nylas-connect/package.json new file mode 100644 index 0000000..bac829e --- /dev/null +++ b/packages/nylas-connect/package.json @@ -0,0 +1,55 @@ +{ + "name": "@nylas/connect", + "version": "0.0.1", + "description": "Modern, lightweight Nylas connection library with PKCE support", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "build:watch": "vite build --watch", + "preview": "vite preview", + "typecheck": "tsc --noEmit", + "format": "pnpm --workspace-root prettier --write packages/nylas-connect/", + "format:check": "pnpm --workspace-root prettier --check packages/nylas-connect/", + "lint": "oxlint --fix .", + "lint:check": "oxlint .", + "clean": "rm -rf dist", + "test": "vitest run", + "test:watch": "vitest", + "coverage": "vitest run --coverage", + "prepublishOnly": "pnpm run clean && pnpm run build" + }, + "keywords": [ + "nylas", + "connect", + "connection", + "oauth2", + "pkce", + "popup", + "inline" + ], + "author": "Nylas Inc.", + "license": "MIT", + "packageManager": "pnpm@10.7.1", + "devDependencies": { + "@types/node": "^20.11.13", + "@vitest/coverage-v8": "^2.1.9", + "happy-dom": "^13.10.1", + "typescript": "^5.3.3", + "vite": "^5.0.10", + "vite-plugin-dts": "^3.7.0", + "vitest": "^2.1.8" + } +} diff --git a/packages/nylas-connect/src/connect-client.test.ts b/packages/nylas-connect/src/connect-client.test.ts new file mode 100644 index 0000000..38fe906 --- /dev/null +++ b/packages/nylas-connect/src/connect-client.test.ts @@ -0,0 +1,761 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { NylasConnect } from "./connect-client"; +import { logger } from "./utils/logger"; +import { LogLevel } from "./types"; + +// Make PKCE deterministic for assertions +vi.mock("./crypto/pkce", () => ({ + generatePKCE: async () => ({ + codeVerifier: "verifier123", + codeChallenge: "challengeABC", + }), + generateState: () => "stateXYZ", +})); + +function base64url(json: object): string { + const str = JSON.stringify(json); + return Buffer.from(str) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); +} + +describe("NylasConnect (fundamentals)", () => { + const clientId = "client_123"; + const redirectUri = "https://app.example/callback"; + + beforeEach(() => { + localStorage.clear(); + vi.restoreAllMocks(); + }); + + it("connect() in inline mode returns a URL with required params and stores auth state", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + apiUrl: "https://api.us.nylas.com", + }); + + const url = await auth.connect(); + expect(typeof url).toBe("string"); + expect(url).toContain("/connect/auth"); + expect(url).toContain(`client_id=${encodeURIComponent(clientId)}`); + expect(url).toContain(`redirect_uri=${encodeURIComponent(redirectUri)}`); + expect(url).toContain("code_challenge=challengeABC"); + expect(url).toContain("state=stateXYZ"); + + // Stored auth state should be present in localStorage under the token storage prefix + const prefixedKey = `@nylas/connect:nylas_auth_state_${clientId}`; + const stored = localStorage.getItem(prefixedKey); + expect(stored).toBeTruthy(); + const parsed = JSON.parse(stored as string); + expect(parsed.codeVerifier).toBe("verifier123"); + expect(parsed.state).toBe("stateXYZ"); + }); + + it("handleRedirectCallback() exchanges code for tokens, stores session, and returns ConnectResult", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + apiUrl: "https://api.us.nylas.com", + }); + + // Prime storage by calling connect() first + await auth.connect(); + + // Mock token endpoint + const header = base64url({ alg: "none", typ: "JWT" }); + const payload = base64url({ + sub: "user_1", + email: "alice@example.com", + name: "Alice", + provider: "google", + email_verified: true, + }); + const idToken = `${header}.${payload}.sig`; + + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + access_token: "access_abc", + id_token: idToken, + token_type: "Bearer", + expires_in: 3600, + scope: "email profile", + grant_id: "grant_1", + }), + }); + vi.stubGlobal("fetch", mockFetch); + + const result = await auth.handleRedirectCallback( + `${redirectUri}?code=auth_code_1&state=stateXYZ`, + ); + + expect(result.accessToken).toBe("access_abc"); + expect(result.idToken).toBe(idToken); + expect(result.grantId).toBe("grant_1"); + expect(result.scope).toBe("email profile"); + expect(result.grantInfo?.email).toBe("alice@example.com"); + + // Session should be retrievable + const session = await auth.getSession(); + expect(session).not.toBeNull(); + expect(session?.grantId).toBe("grant_1"); + + // Tokens should be stored under token_grant + expect(localStorage.getItem("@nylas/connect:token_grant_1")).toBeTruthy(); + expect(localStorage.getItem("@nylas/connect:token_default")).toBeTruthy(); + }); + + it("logout(grantId) removes the specific session and emits SIGNED_OUT", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + apiUrl: "https://api.us.nylas.com", + }); + + // Seed a fake session + const fakeSession = { + accessToken: "t", + idToken: "h.e.y", + grantId: "g", + expiresAt: Date.now() + 100000, + scope: "email", + }; + localStorage.setItem("@nylas/connect:token_g", JSON.stringify(fakeSession)); + + const spy = vi.fn(); + auth.onConnectStateChange(spy); + await auth.logout("g"); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("SIGNED_OUT", null, { + grantId: "g", + reason: "user_initiated", + }); + expect(localStorage.getItem("@nylas/connect:token_g")).toBeNull(); + }); + + it("setLogLevel controls logger levels correctly", () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + apiUrl: "https://api.us.nylas.com", + }); + + // Spy on console methods to verify logger behavior + const consoleSpy = { + log: vi.spyOn(console, "log").mockImplementation(() => {}), + info: vi.spyOn(console, "info").mockImplementation(() => {}), + warn: vi.spyOn(console, "warn").mockImplementation(() => {}), + error: vi.spyOn(console, "error").mockImplementation(() => {}), + }; + + try { + // Set WARN level - only warn and error messages should show + auth.setLogLevel(LogLevel.WARN); + + // Reset spies + Object.values(consoleSpy).forEach((spy) => spy.mockClear()); + + // Test that only warn and error messages show + logger.debug("debug should not show"); + logger.info("info should not show"); + logger.warn("warn should show"); + logger.error("error should show"); + + expect(consoleSpy.log).not.toHaveBeenCalled(); + expect(consoleSpy.info).not.toHaveBeenCalled(); + expect(consoleSpy.warn).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[WARN\]/), + "warn should show", + ); + expect(consoleSpy.error).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[ERROR\]/), + "error should show", + ); + + // Disable all logs + auth.setLogLevel("off"); + Object.values(consoleSpy).forEach((spy) => spy.mockClear()); + logger.debug("debug off"); + logger.info("info off"); + logger.warn("warn off"); + logger.error("error off"); + expect(consoleSpy.log).not.toHaveBeenCalled(); + expect(consoleSpy.info).not.toHaveBeenCalled(); + expect(consoleSpy.warn).not.toHaveBeenCalled(); + expect(consoleSpy.error).not.toHaveBeenCalled(); + } finally { + // Clean up spies + Object.values(consoleSpy).forEach((spy) => spy.mockRestore()); + // Reset logger to default state (off) + logger.setLevel("off"); + } + }); +}); + +describe("NylasConnect (backend-only getAuthUrl)", () => { + const clientId = "client_backend_1"; + const redirectUri = "https://app.example/callback"; + + beforeEach(() => { + localStorage.clear(); + vi.restoreAllMocks(); + }); + + it("getAuthUrl returns URL without PKCE and does not persist auth state", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + apiUrl: "https://api.us.nylas.com", + }); + + const { url, state, scopes } = await auth.getAuthUrl(); + + expect(typeof url).toBe("string"); + expect(state).toBe("stateXYZ"); // from mocked generateState + expect(Array.isArray(scopes)).toBe(true); + + // URL assertions + expect(url).toContain("/connect/auth"); + expect(url).toContain(`client_id=${encodeURIComponent(clientId)}`); + expect(url).toContain(`redirect_uri=${encodeURIComponent(redirectUri)}`); + expect(url).toContain("response_type=code"); + expect(url).toContain("state=stateXYZ"); + expect(url).toContain("access_type=online"); + expect(url).not.toContain("code_challenge="); + expect(url).not.toContain("code_challenge_method="); + + // Should NOT have stored OAuth auth state in localStorage + const oauthStateKey = `@nylas/connect:nylas_auth_state_${clientId}`; + expect(localStorage.getItem(oauthStateKey)).toBeNull(); + }); + + it("getAuthUrl includes provider and scopes when provided", async () => { + const auth = new NylasConnect({ clientId, redirectUri }); + + const { url } = await auth.getAuthUrl({ + provider: "google", + scopes: [ + "https://www.googleapis.com/auth/gmail.readonly", + "https://www.googleapis.com/auth/calendar.readonly", + ], + }); + + expect(url).toContain("provider=google"); + expect(url).toContain( + "scope=" + + encodeURIComponent( + "https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/calendar.readonly", + ), + ); + + // Still no PKCE params in backend-only URL + expect(url).not.toContain("code_challenge="); + expect(url).not.toContain("code_challenge_method="); + }); +}); + +describe("NylasConnect (Provider-Specific Scopes)", () => { + const clientId = "client_123"; + const redirectUri = "https://app.example/callback"; + + beforeEach(() => { + localStorage.clear(); + vi.restoreAllMocks(); + }); + + it("should use simple array scopes when defaultScopes is an array", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + defaultScopes: ["email", "profile"], + }); + + const url = await auth.connect(); + expect(typeof url).toBe("string"); + expect(url).toContain("scope=email+profile"); + }); + + it("should use provider-specific scopes when provider is specified", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + defaultScopes: { + google: ["https://www.googleapis.com/auth/gmail.readonly"], + microsoft: ["https://graph.microsoft.com/Mail.Read"], + }, + }); + + // Test Google scopes + const googleUrl = await auth.connect({ provider: "google" }); + expect(typeof googleUrl).toBe("string"); + expect(googleUrl).toContain( + "scope=" + + encodeURIComponent("https://www.googleapis.com/auth/gmail.readonly"), + ); + + // Test Microsoft scopes + const msUrl = await auth.connect({ provider: "microsoft" }); + expect(typeof msUrl).toBe("string"); + expect(msUrl).toContain( + "scope=" + encodeURIComponent("https://graph.microsoft.com/Mail.Read"), + ); + }); + + it("should return empty scopes when provider is specified but not in config", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + defaultScopes: { + google: ["https://www.googleapis.com/auth/gmail.readonly"], + }, + }); + + // Test provider not in config + const url = await auth.connect({ provider: "microsoft" }); + expect(typeof url).toBe("string"); + expect(url).not.toContain("scope="); + }); + + it("should return empty scopes when no provider specified with provider-specific config", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + defaultScopes: { + google: ["https://www.googleapis.com/auth/gmail.readonly"], + microsoft: ["https://graph.microsoft.com/Mail.Read"], + }, + }); + + // No provider specified + const url = await auth.connect(); + expect(typeof url).toBe("string"); + expect(url).not.toContain("scope="); + }); + + it("should prioritize options.scopes over defaultScopes", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + defaultScopes: { + google: ["https://www.googleapis.com/auth/gmail.readonly"], + }, + }); + + // options.scopes should override defaultScopes + const url = await auth.connect({ + provider: "google", + scopes: ["custom", "scopes"], + }); + expect(typeof url).toBe("string"); + expect(url).toContain("scope=custom+scopes"); + expect(url).not.toContain("gmail.readonly"); + }); + + it("should prioritize options.scopes over simple array defaultScopes", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + defaultScopes: ["email", "profile"], + }); + + // options.scopes should override defaultScopes + const url = await auth.connect({ + scopes: ["custom", "scopes"], + }); + expect(typeof url).toBe("string"); + expect(url).toContain("scope=custom+scopes"); + expect(url).not.toContain("email"); + expect(url).not.toContain("profile"); + }); + + it("should handle empty provider-specific scopes correctly", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + defaultScopes: { + google: [], + microsoft: ["https://graph.microsoft.com/Mail.Read"], + }, + }); + + // Test provider with empty scopes + const googleUrl = await auth.connect({ provider: "google" }); + expect(typeof googleUrl).toBe("string"); + expect(googleUrl).not.toContain("scope="); + + // Test provider with scopes + const msUrl = await auth.connect({ provider: "microsoft" }); + expect(typeof msUrl).toBe("string"); + expect(msUrl).toContain( + "scope=" + encodeURIComponent("https://graph.microsoft.com/Mail.Read"), + ); + }); + + it("should handle no defaultScopes specified", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + // No defaultScopes specified + }); + + const url = await auth.connect({ provider: "google" }); + expect(typeof url).toBe("string"); + expect(url).not.toContain("scope="); + }); +}); + +describe("NylasConnect (callback deduplication)", () => { + const clientId = "client_123"; + const redirectUri = "https://app.example/callback"; + const authCode = "auth_code_123"; + const state = "stateXYZ"; // Use the mocked state from PKCE module + const callbackUrl = `${redirectUri}?code=${authCode}&state=${state}`; + + beforeEach(() => { + localStorage.clear(); + vi.restoreAllMocks(); + }); + + it("should prevent duplicate callback processing for same auth code", async () => { + const mockIdTokenPayload = { + sub: "user123", + email: "test@example.com", + name: "Test Account", + provider: "google", + }; + + const mockTokenResponse = { + access_token: "access_token_123", + id_token: `header.${base64url(mockIdTokenPayload)}.signature`, + grant_id: "grant_123", + expires_in: 3600, + scope: "openid email", + }; + + const mockFetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockTokenResponse), + }); + + vi.stubGlobal("fetch", mockFetch); + + const auth = new NylasConnect({ clientId, redirectUri }); + + // Prime storage by calling connect() first to set up auth state + await auth.connect(); + + // First callback should succeed + const result1 = await auth.callback(callbackUrl); + expect(result1).toBeTruthy(); + expect(mockFetch).toHaveBeenCalledTimes(1); + + // Second callback with same auth code should throw error + await expect(auth.callback(callbackUrl)).rejects.toThrow( + "Authorization code has already been processed", + ); + + // Fetch should not be called again + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should handle concurrent callback processing for same auth code", async () => { + const mockIdTokenPayload = { + sub: "user123", + email: "test@example.com", + name: "Test Account", + provider: "google", + }; + + const mockTokenResponse = { + access_token: "access_token_123", + id_token: `header.${base64url(mockIdTokenPayload)}.signature`, + grant_id: "grant_123", + expires_in: 3600, + scope: "openid email", + }; + + // Add a delay to simulate network request + const mockFetch = vi.fn().mockImplementation( + () => + new Promise((resolve) => + setTimeout( + () => + resolve({ + ok: true, + json: () => Promise.resolve(mockTokenResponse), + }), + 100, + ), + ), + ); + + vi.stubGlobal("fetch", mockFetch); + + const auth = new NylasConnect({ clientId, redirectUri }); + + // Prime storage by calling connect() first to set up auth state + await auth.connect(); + + // Start two concurrent callbacks with same auth code + const promise1 = auth.callback(callbackUrl); + const promise2 = auth.callback(callbackUrl); + + // Both promises should resolve to the same result + const [result1, result2] = await Promise.all([promise1, promise2]); + + expect(result1).toEqual(result2); + expect(result1.grantId).toBe("grant_123"); + + // Fetch should only be called once despite two concurrent requests + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should not interfere with processing different auth codes", async () => { + const mockIdTokenPayload = { + sub: "user123", + email: "test@example.com", + name: "Test Account", + provider: "google", + }; + + const mockTokenResponse = { + access_token: "access_token_123", + id_token: `header.${base64url(mockIdTokenPayload)}.signature`, + grant_id: "grant_123", + expires_in: 3600, + scope: "openid email", + }; + + const mockFetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockTokenResponse), + }); + + vi.stubGlobal("fetch", mockFetch); + + const auth = new NylasConnect({ clientId, redirectUri }); + + // Prime storage by calling connect() first to set up auth state + await auth.connect(); + + // Process first auth code successfully + const result1 = await auth.callback( + `${redirectUri}?code=auth_code_123&state=${state}`, + ); + expect(result1.grantId).toBe("grant_123"); + + // Verify that a different auth code is not marked as already processed + // (This should fail at auth state validation, not at deduplication) + await expect( + auth.callback(`${redirectUri}?code=auth_code_456&state=${state}`), + ).rejects.toThrow("No stored auth state found"); + + // Fetch should only be called once for the successful callback + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should allow retry after failed callback processing", async () => { + const mockIdTokenPayload = { + sub: "user123", + email: "test@example.com", + name: "Test Account", + provider: "google", + }; + + const mockTokenResponse = { + access_token: "access_token_123", + id_token: `header.${base64url(mockIdTokenPayload)}.signature`, + grant_id: "grant_123", + expires_in: 3600, + scope: "openid email", + }; + + // First call fails, second succeeds + const mockFetch = vi + .fn() + .mockRejectedValueOnce(new Error("Network error")) + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockTokenResponse), + }); + + vi.stubGlobal("fetch", mockFetch); + + const auth = new NylasConnect({ clientId, redirectUri }); + + // Prime storage by calling connect() first to set up auth state + await auth.connect(); + + // First callback should fail + await expect(auth.callback(callbackUrl)).rejects.toThrow( + "Token exchange failed", + ); + + // Set up auth state for retry (since first failure may have cleaned it up) + await auth.connect(); + + // Second callback with same auth code should succeed (retry allowed) + const result = await auth.callback(callbackUrl); + expect(result.grantId).toBe("grant_123"); + + // Both requests should have been made + expect(mockFetch).toHaveBeenCalledTimes(2); + }); +}); + +describe("NylasConnect (sessions, validation, and events)", () => { + const clientId = "client_123"; + const redirectUri = "https://app.example/callback"; + + beforeEach(() => { + localStorage.clear(); + vi.restoreAllMocks(); + }); + + it("getSession returns null and emits SESSION_EXPIRED when token is expired", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + apiUrl: "https://api.us.nylas.com", + }); + + const expiredSession = { + accessToken: "t", + idToken: "h.e.y", + grantId: "g_expired", + expiresAt: Date.now() - 1000, + scope: "email", + }; + localStorage.setItem( + "@nylas/connect:token_default", + JSON.stringify(expiredSession), + ); + + const spy = vi.fn(); + auth.onConnectStateChange(spy); + + const session = await auth.getSession(); + expect(session).toBeNull(); + + expect(spy).toHaveBeenCalledWith("SESSION_EXPIRED", null, { + grantId: "g_expired", + expiresAt: expiredSession.expiresAt, + }); + expect(localStorage.getItem("@nylas/connect:token_default")).toBeNull(); + }); + + it("onAuthStateChange unsubscribe stops subsequent emissions", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + apiUrl: "https://api.us.nylas.com", + }); + const spy = vi.fn(); + const unsubscribe = auth.onConnectStateChange(spy); + + await auth.connect(); // emits CONNECT_STARTED + expect(spy).toHaveBeenCalled(); + + spy.mockClear(); + unsubscribe(); + + await auth.logout("any"); // would emit SIGNED_OUT if subscribed + expect(spy).not.toHaveBeenCalled(); + }); + + describe("getConnectionStatus", () => { + it("returns not_connected when no session", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + apiUrl: "https://api.us.nylas.com", + }); + const spy = vi.fn(); + auth.onConnectStateChange(spy); + + const status = await auth.getConnectionStatus(); + expect(status).toBe("not_connected"); + expect(spy).not.toHaveBeenCalledWith( + "CONNECTION_STATUS_CHANGED", + expect.anything(), + expect.anything(), + ); + }); + + it("returns connected when token validates", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + apiUrl: "https://api.us.nylas.com", + }); + + const session = { + accessToken: "t_ok", + idToken: "h.e.y", + grantId: "g_ok", + expiresAt: Date.now() + 60_000, + scope: "email", + }; + localStorage.setItem( + "@nylas/connect:token_default", + JSON.stringify(session), + ); + + const spy = vi.fn(); + auth.onConnectStateChange(spy); + + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ data: { grant_id: "g_ok" } }), + }), + ); + + const status = await auth.getConnectionStatus(); + expect(status).toBe("connected"); + const emitted = spy.mock.calls.map((c) => c[0]); + expect(emitted).not.toContain("CONNECTION_STATUS_CHANGED"); + }); + + it("returns invalid when token fails validation", async () => { + const auth = new NylasConnect({ + clientId, + redirectUri, + apiUrl: "https://api.us.nylas.com", + }); + + const session = { + accessToken: "t_bad", + idToken: "h.e.y", + grantId: "g_bad", + expiresAt: Date.now() + 60_000, + scope: "email", + }; + localStorage.setItem( + "@nylas/connect:token_default", + JSON.stringify(session), + ); + + const spy = vi.fn(); + auth.onConnectStateChange(spy); + + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({}), + }), + ); + + const status = await auth.getConnectionStatus(); + expect(status).toBe("invalid"); + const emitted = spy.mock.calls.map((c) => c[0]); + expect(emitted).not.toContain("CONNECTION_STATUS_CHANGED"); + }); + }); +}); diff --git a/packages/nylas-connect/src/connect-client.ts b/packages/nylas-connect/src/connect-client.ts new file mode 100644 index 0000000..01d0476 --- /dev/null +++ b/packages/nylas-connect/src/connect-client.ts @@ -0,0 +1,1064 @@ +import type { + ConnectConfig, + ConnectOptions, + ConnectResult, + GrantInfo, + TokenResponse, + ConnectState, + TokenStorage, + Environment, + ConnectionStatus, + ConnectStateChangeCallback, + ConnectEvent, + ConnectEventData, + SessionData, + LogLevel, + Provider, +} from "./types"; +import { ConnectStatus } from "./types"; +import { generatePKCE, generateState } from "./crypto/pkce"; +import { BrowserTokenStorage } from "./storage/token-storage"; +import { MemoryTokenStorage } from "./storage/memory-storage"; +import { logger } from "./utils/logger"; +import { + ConfigError, + NetworkError, + OAuthError, + TokenError, + createOAuthError, +} from "./errors/connect-errors"; + +import { openPopup, waitForCallback } from "./utils/popup"; +import { + buildAuthUrl, + parseConnectCallback, + cleanUrl, + isConnectCallback, +} from "./utils/redirect"; + +/** + * Modern Nylas authentication client + */ +export class NylasConnect { + private config: Required> & + Pick; + private storage: TokenStorage; + private connectStateCallbacks: Set = new Set(); + + // Callback deduplication state to prevent duplicate processing + private callbackState: { + processing: Map>; // Track active callbacks by auth code + processed: Set; // Track successfully processed auth codes + lastCleanup: number; // Timestamp of last cleanup + } = { + processing: new Map(), + processed: new Set(), + lastCleanup: Date.now(), + }; + + constructor(config: ConnectConfig = {}) { + // Resolve configuration with environment variables and defaults + const resolvedConfig = this.resolveConfig(config); + + // Validate required configuration + this.validateConfig(resolvedConfig); + + // Set configuration with smart defaults + this.config = { + clientId: resolvedConfig.clientId!, + redirectUri: resolvedConfig.redirectUri!, + apiUrl: resolvedConfig.apiUrl!, + environment: resolvedConfig.environment!, + defaultScopes: resolvedConfig.defaultScopes || [], + debug: resolvedConfig.debug!, + persistTokens: resolvedConfig.persistTokens!, + autoHandleCallback: resolvedConfig.autoHandleCallback!, + logLevel: resolvedConfig.logLevel, + }; + + // Configure logger based on config + this.configureLogger(); + + // Initialize storage + this.storage = this.createStorage(); + + // Log configuration (without sensitive data) + logger.info("NylasConnect initialized", { + clientId: this.config.clientId.substring(0, 8) + "...", + apiUrl: this.config.apiUrl, + environment: this.config.environment, + defaultScopes: this.config.defaultScopes, + }); + } + + /** + * Resolve configuration with environment variables and smart defaults + */ + private resolveConfig(config: ConnectConfig): ConnectConfig { + const environment = this.detectEnvironment(config.environment); + + return { + clientId: config.clientId || this.getEnvVar("NYLAS_CLIENT_ID"), + redirectUri: + config.redirectUri || + this.getEnvVar("NYLAS_REDIRECT_URI") || + this.detectRedirectUri(), + apiUrl: config.apiUrl || "https://api.us.nylas.com", + environment, + defaultScopes: config.defaultScopes, + debug: config.debug ?? environment === "development", + persistTokens: config.persistTokens ?? true, + autoHandleCallback: config.autoHandleCallback ?? true, + logLevel: config.logLevel, + }; + } + + /** + * Configure logger based on auth config + */ + private configureLogger(): void { + // If logLevel is explicitly set, use it (overrides debug flag) + if (this.config.logLevel !== undefined) { + logger.setLevel(this.config.logLevel); + } else if (this.config.debug) { + // If debug is true, enable debug level + logger.enable(); + } else { + // If debug is false, disable logging + logger.disable(); + } + } + + /** + * Validate required configuration + */ + private validateConfig(config: ConnectConfig): void { + if (!config.clientId) { + throw new ConfigError( + "clientId is required", + "The Nylas Client ID is required to connect users", + "Set clientId in the constructor or use the NYLAS_CLIENT_ID environment variable", + ); + } + + if (!config.redirectUri) { + throw new ConfigError( + "redirectUri is required", + "A valid redirect URI is required for OAuth flow", + "Set redirectUri in the constructor or use the NYLAS_REDIRECT_URI environment variable", + ); + } + + // Validate redirect URI format + try { + new URL(config.redirectUri); + } catch { + throw new ConfigError( + "redirectUri must be a valid URL", + "The redirect URI must be a valid HTTP/HTTPS URL", + "Ensure your redirectUri starts with http:// or https:// and is properly formatted", + ); + } + } + + /** + * Detect current environment + */ + private detectEnvironment(specified?: Environment): Environment { + if (specified) { + return specified; + } + + // Check environment variables + const nodeEnv = this.getEnvVar("NODE_ENV"); + if (nodeEnv === "production") { + return "production"; + } + if (nodeEnv === "staging" || nodeEnv === "test") { + return "staging"; + } + + // Check if running in development (localhost, etc.) + if (globalThis.window) { + const hostname = globalThis.window.location.hostname; + if ( + hostname === "localhost" || + hostname === "127.0.0.1" || + hostname.endsWith(".local") + ) { + return "development"; + } + } + + return "production"; // Default to production for safety + } + + /** + * Auto-detect redirect URI for development + */ + private detectRedirectUri(): string | undefined { + if (globalThis.window) { + const { protocol, hostname, port } = globalThis.window.location; + const portSuffix = + port && port !== "80" && port !== "443" ? `:${port}` : ""; + return `${protocol}//${hostname}${portSuffix}/auth/callback`; + } + return undefined; + } + + /** + * Get environment variable (works in both browser and Node.js) + */ + private getEnvVar(name: string): string | undefined { + // Node.js environment + if (typeof process !== "undefined" && process.env) { + return process.env[name]; + } + + // Browser environment - check for build-time injected variables + if (globalThis.window) { + // Vite/Webpack style environment variables + const envVar = + (globalThis.window as any).process?.env?.[name] || + (globalThis.window as any).__ENV__?.[name] || + (globalThis.window as any)[name]; + return envVar; + } + + return undefined; + } + + /** + * Subscribe to auth state changes + */ + onConnectStateChange(callback: ConnectStateChangeCallback): () => void { + this.connectStateCallbacks.add(callback); + return () => this.connectStateCallbacks.delete(callback); + } + + /** + * Trigger auth state change event with optional event data + */ + private triggerConnectStateChange( + event: T, + session: ConnectResult | null, + data?: ConnectEventData[T], + ): void { + this.connectStateCallbacks.forEach((callback) => { + try { + callback(event, session, data); + } catch (error) { + logger.error("Error in auth state change callback:", error); + } + }); + } + + /** + * Connect using OAuth2 flow + * @param options Authentication options + * @returns Promise for popup flow, Promise for inline flow + */ + async connect(options: ConnectOptions = {}): Promise { + const scopes = this.resolveScopes(options); + + // Emit CONNECT_STARTED event + this.triggerConnectStateChange("CONNECT_STARTED", null, { + method: options.method || "inline", + provider: options.provider, + scopes, + }); + + // 1. Check for existing valid session + const existingSession = await this.getSession(); + if (existingSession) { + // Emit SESSION_RESTORED instead of just SIGNED_IN for existing sessions + this.triggerConnectStateChange("SESSION_RESTORED", existingSession, { + session: existingSession, + fromStorage: true, + }); + return existingSession; + } + + const { codeVerifier, codeChallenge } = await generatePKCE(); + const state = options.state || generateState(); + + logger.info("Starting authentication", { + method: options.method, + scopes, + provider: options.provider, + }); + + // Store auth state for later retrieval using global key + const authState: ConnectState = { + codeVerifier, + state, + scopes, + timestamp: Date.now(), + }; + + // Use a global key that includes client ID and state for uniqueness + const authStateKey = this.authStateKey(); + const oauthStorage = this.createOAuthStorage(); + await oauthStorage.set(authStateKey, JSON.stringify(authState)); + + // Also store the current auth state key for easy retrieval + logger.debug("Auth state stored", { authStateKey, authState }); + + const authUrl = buildAuthUrl({ + apiUrl: this.config.apiUrl, + clientId: this.config.clientId, + redirectUri: this.config.redirectUri, + scopes, + codeChallenge, + state, + provider: options.provider, + loginHint: options.loginHint, + }); + + if (options.method === "popup") { + // Popup flow - wait for completion + logger.info("Starting popup authentication flow"); + + // Emit CONNECT_POPUP_OPENED event + this.triggerConnectStateChange("CONNECT_POPUP_OPENED", null, { + url: authUrl, + provider: options.provider, + }); + + try { + const popup = openPopup(authUrl, { + width: options.popupWidth || 500, + height: options.popupHeight || 600, + }); + + const authCode = await waitForCallback(popup, state); + + // Emit CONNECT_POPUP_CLOSED event + this.triggerConnectStateChange("CONNECT_POPUP_CLOSED", null, { + reason: "completed", + }); + + const result = await this.exchangeCodeForTokens(authCode, codeVerifier); + + // Emit CONNECT_SUCCESS before SIGNED_IN + this.triggerConnectStateChange("CONNECT_SUCCESS", result, { + grantId: result.grantId, + provider: result.grantInfo?.provider || "unknown", + scopes: result.scope.split(" "), + }); + + // Trigger auth state change + this.triggerConnectStateChange("SIGNED_IN", result, { + session: result, + isNewLogin: true, + }); + + return result; + } catch (error) { + // Determine if this was a cancellation or error + const isCancellation = + error instanceof Error && + (error.message.includes("closed") || + error.message.includes("cancelled")); + + // Emit CONNECT_POPUP_CLOSED with appropriate reason + this.triggerConnectStateChange("CONNECT_POPUP_CLOSED", null, { + reason: isCancellation ? "cancelled" : "error", + }); + + // Emit appropriate event based on error type + if (isCancellation) { + this.triggerConnectStateChange("CONNECT_CANCELLED", null, { + reason: error.message, + }); + } else { + this.triggerConnectStateChange("CONNECT_ERROR", null, { + error: error as any, + step: "popup_authentication", + }); + } + + throw error; + } + } else { + // Inline flow - return URL for manual redirect + logger.info("Starting inline authentication flow", { url: authUrl }); + + // Emit CONNECT_REDIRECT event + this.triggerConnectStateChange("CONNECT_REDIRECT", null, { + url: authUrl, + provider: options.provider, + }); + + return authUrl; + } + } + + /** + * Build an authorization URL for backend-only flows (no PKCE). + * This does not store any state or perform token exchange. + * Intended for server-side (confidential) exchanges using an API key. + */ + async getAuthUrl(options: ConnectOptions = {}): Promise<{ + url: string; + state: string; + scopes: string[]; + }> { + const scopes = this.resolveScopes(options); + const state = options.state || generateState(); + + const url = buildAuthUrl({ + apiUrl: this.config.apiUrl, + clientId: this.config.clientId, + redirectUri: this.config.redirectUri, + scopes, + state, + provider: options.provider, + loginHint: options.loginHint, + // No codeChallenge: backend will exchange using API key (no PKCE) + }); + + return { url, state, scopes }; + } + + /** + * Handle inline callback after OAuth flow + */ + async handleRedirectCallback(url?: string): Promise { + logger.info("Handling inline callback"); + + const { code, state, error, errorDescription } = parseConnectCallback(url); + + // Emit CONNECT_CALLBACK_RECEIVED event + this.triggerConnectStateChange("CONNECT_CALLBACK_RECEIVED", null, { + code, + state, + error, + }); + + if (error) { + // Emit CONNECT_ERROR for OAuth errors + const authError = createOAuthError(error, errorDescription); + this.triggerConnectStateChange("CONNECT_ERROR", null, { + error: authError, + step: "callback_processing", + }); + throw authError; + } + + if (!code) { + throw new OAuthError( + "invalid_request", + "No authorization code found in callback", + ); + } + + if (!state) { + throw new OAuthError( + "invalid_request", + "No state parameter found in callback", + ); + } + + // Retrieve stored auth state using multiple fallback strategies + let storedState: ConnectState; + + logger.debug("Attempting to retrieve auth state", { + state, + clientId: this.config.clientId.substring(0, 8) + "...", + }); + + const authStateKey = this.authStateKey(); + const oauthStorage = this.createOAuthStorage(); + const storedStateStr = await oauthStorage.get(authStateKey); + logger.debug("Specific key", { + authStateKey, + found: !!storedStateStr, + }); + + if (!storedStateStr) { + logger.error("No auth state found with any strategy"); + throw new OAuthError("invalid_request", "No stored auth state found"); + } + + try { + storedState = JSON.parse(storedStateStr); + } catch { + throw new OAuthError("invalid_request", "Invalid stored auth state"); + } + + // Validate auth state age (15 minutes TTL) + const MAX_AGE_MS = 15 * 60 * 1000; + if (Date.now() - storedState.timestamp > MAX_AGE_MS) { + await oauthStorage.remove(authStateKey); + throw new OAuthError("invalid_request", "Auth state expired"); + } + + // Verify state parameter + if (state !== storedState.state) { + throw new OAuthError("invalid_request", "State parameter mismatch"); + } + + // Exchange code for tokens BEFORE cleaning URL + const result = await this.exchangeCodeForTokens( + code, + storedState.codeVerifier, + ); + + // Emit CONNECT_SUCCESS event + this.triggerConnectStateChange("CONNECT_SUCCESS", result, { + grantId: result.grantId, + provider: result.grantInfo?.provider || "unknown", + scopes: result.scope.split(" "), + }); + + // Emit SIGNED_IN event + this.triggerConnectStateChange("SIGNED_IN", result, { + session: result, + isNewLogin: true, + }); + + // Clean up URL parameters only after successful token exchange + cleanUrl(); + // Clean up stored state using the correct key + await oauthStorage.remove(authStateKey); + // Also clean up the current state key tracker + return result; + } + + /** + * Clean up old callback state to prevent memory leaks + */ + private cleanupCallbackState(): void { + const now = Date.now(); + const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes + // Only cleanup every 5 minutes to avoid excessive work + if (now - this.callbackState.lastCleanup < CLEANUP_INTERVAL) { + return; + } + // Note: We can't easily track when codes were processed without changing the Set + // For now, we'll periodically clear the entire processed set since auth codes + // are single-use and this prevents indefinite memory growth + if (this.callbackState.processed.size > 100) { + logger.debug("Cleaning up processed callback codes"); + this.callbackState.processed.clear(); + } + + this.callbackState.lastCleanup = now; + } + + /** + * Simplified callback handler that works fowh callbacks + */ + async callback(url?: string): Promise { + // Cleanup old callback state periodically + this.cleanupCallbackState(); + + const targetUrl = + url || (globalThis.window ? globalThis.window.location.href : ""); + + // Check if this URL contains auth callback parameters + if (!isConnectCallback(targetUrl)) { + // throw new OAuthError( + // "invalid_request", + // "No authentication callback parameters found in URL", + // "This URL does not appear to be an OAuth callback URL", + // ); + return; // NO OP -- fail silently if not valid URL + } + + // Parse callback parameters + const { code, state, error, errorDescription } = + parseConnectCallback(targetUrl); + + // Handle OAuth errors + if (error) { + throw createOAuthError(error, errorDescription); + } + + // Deduplication logic for authorization codes + if (code) { + // Check if this authorization code was already processed successfully + if (this.callbackState.processed.has(code)) { + logger.debug("Authorization code already processed", { + code: code.substring(0, 8) + "...", + }); + throw new OAuthError( + "invalid_request", + "Authorization code has already been processed", + "Each authorization code can only be used once", + ); + } + + // Check if this authorization code is currently being processed + if (this.callbackState.processing.has(code)) { + logger.debug( + "Authorization code currently being processed, returning existing promise", + { + code: code.substring(0, 8) + "...", + }, + ); + return await this.callbackState.processing.get(code)!; + } + } + + // For popup flow, send message to parent window + if (globalThis.window?.opener) { + // Add origin check for security + const allowedOrigin = new URL(this.config.redirectUri).origin; + if (code && state) { + globalThis.window.opener.postMessage( + { + type: ConnectStatus.SUCCESS, + code, + state, + }, + allowedOrigin, + ); + } else { + globalThis.window.opener.postMessage( + { + type: ConnectStatus.ERROR, + error: error || "invalid_request", + error_description: + errorDescription || "Missing code or state parameter", + }, + allowedOrigin, + ); + } + globalThis.window.close(); + // Return a placeholder result (the actual result will be handled by the parent) + return { + accessToken: "", + idToken: "", + grantId: "", + expiresAt: 0, + scope: "", + }; + } + + // For inline flow, use this auth instance with deduplication protection + if (code) { + // Track this processing attempt + const processingPromise = this.handleRedirectCallback(targetUrl); + this.callbackState.processing.set(code, processingPromise); + + try { + const result = await processingPromise; + + // Mark as successfully processed + this.callbackState.processed.add(code); + logger.debug("Authorization code successfully processed", { + code: code.substring(0, 8) + "...", + grantId: result.grantId, + }); + + return result; + } catch (error) { + // Don't mark as processed on error - allow retry + logger.debug("Authorization code processing failed", { + code: code.substring(0, 8) + "...", + error: (error as Error).message, + }); + throw error; + } finally { + // Always remove from processing map + this.callbackState.processing.delete(code); + } + } + + // Fallback for edge cases without auth code + return await this.handleRedirectCallback(targetUrl); + } + + /** + * Validate an access token + */ + private async validateToken(token?: string): Promise { + let accessToken = token; + let grantId = "unknown"; + + if (!accessToken) { + const session = await this.getSession(); + if (!session) { + return false; + } + accessToken = session.accessToken; + grantId = session.grantId; + } + + try { + const response = await fetch(`${this.config.apiUrl}/connect/tokeninfo`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `access_token=${encodeURIComponent(accessToken)}`, + }); + + const data = await response.json(); + const isValid = !!(response.ok && data?.data); + + if (!isValid) { + // Emit TOKEN_VALIDATION_ERROR event + this.triggerConnectStateChange("TOKEN_VALIDATION_ERROR", null, { + grantId: data?.grant_id || grantId, + error: new TokenError( + "Token validation failed", + "Token is invalid or expired", + ), + }); + } + + return isValid; + } catch (error) { + logger.error("Token validation failed", error); + + // Emit NETWORK_ERROR event + this.triggerConnectStateChange("NETWORK_ERROR", null, { + operation: "token_validation", + error: new NetworkError( + "Token validation failed", + "Network request failed", + error as Error, + ), + }); + + return false; + } + } + + /** + * Get current connection status + */ + async getConnectionStatus(grantId?: string): Promise { + const session = await this.getSession(grantId); + if (!session) { + return "not_connected"; + } + const isValid = await this.validateToken(session.accessToken); + const status = isValid ? "connected" : "invalid"; + + // Note: To emit CONNECTION_STATUS_CHANGED, we'd need to track previous status + // This could be added as a class property if needed + + return status; + } + + /** + * Get current session data + */ + + async getSession(grantId?: string): Promise { + const key = this.tokenKey(grantId); + + try { + const tokenStr = await this.storage.get(key); + + if (!tokenStr) { + return null; + } + + const tokenData = JSON.parse(tokenStr); + + // Check if session is expired + if (Date.now() >= tokenData.expiresAt) { + logger.info("Session expired, removing from storage"); + await this.storage.remove(key); + + // Emit SESSION_EXPIRED event + this.triggerConnectStateChange("SESSION_EXPIRED", null, { + grantId: tokenData.grantId || grantId || "unknown", + expiresAt: tokenData.expiresAt, + }); + + return null; + } + + return tokenData as SessionData; + } catch (error) { + await this.storage.remove(key); + + // Emit SESSION_INVALID event + this.triggerConnectStateChange("SESSION_INVALID", null, { + grantId: grantId || "unknown", + reason: "Invalid stored session data", + }); + + // Also emit STORAGE_ERROR for storage issues + if (error instanceof Error) { + this.triggerConnectStateChange("STORAGE_ERROR", null, { + operation: "get_session", + key: key, + error: error, + }); + } + + return null; + } + } + + /** + * Logout and clear stored tokens + */ + async logout(grantId?: string): Promise { + if (grantId) { + await this.storage.remove(this.tokenKey(grantId)); + } else { + await this.storage.clear(); + // Emit STORAGE_CLEARED event when clearing all storage + this.triggerConnectStateChange("STORAGE_CLEARED", null, { + reason: "Grant logout", + }); + } + + // Trigger enhanced SIGNED_OUT event + this.triggerConnectStateChange("SIGNED_OUT", null, { + grantId, + reason: "user_initiated", + }); + + logger.info("Grant logged out", { grantId }); + } + + /** + * Set logging level + * @param level - Log level: LogLevel enum values or "off" + */ + setLogLevel(level: LogLevel | "off"): void { + logger.setLevel(level); + } + + /** + * Check if defaultScopes is configured as provider-specific + */ + private isProviderSpecificScopes( + scopes?: string[] | Partial>, + ): scopes is Partial> { + return ( + scopes !== null && + scopes !== undefined && + !Array.isArray(scopes) && + typeof scopes === "object" + ); + } + + /** + * Resolve scopes based on provider and configuration + */ + private resolveScopes(options: ConnectOptions): string[] { + // Priority: options.scopes > provider-specific scopes > default scopes > empty array + if (options.scopes) { + return options.scopes; + } + + if (this.isProviderSpecificScopes(this.config.defaultScopes)) { + // Provider-specific scopes configuration + if (options.provider) { + return this.config.defaultScopes[options.provider] || []; + } + // No provider specified with provider-specific config - return empty array + return []; + } + + // Simple array format or undefined + return this.config.defaultScopes || []; + } + + /** + * Exchange authorization code for tokens + */ + private async exchangeCodeForTokens( + code: string, + codeVerifier: string, + ): Promise { + logger.debug( + "Exchanging authorization code for tokens", + code, + codeVerifier, + ); + + const payload = { + client_id: this.config.clientId, + redirect_uri: this.config.redirectUri, + code, + grant_type: "authorization_code", + code_verifier: codeVerifier, + }; + + try { + const response = await fetch(`${this.config.apiUrl}/connect/token`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams(payload).toString(), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + logger.error("Token exchange failed", { + status: response.status, + statusText: response.statusText, + errorData, + payload, + url: `${this.config.apiUrl}/connect/token`, + }); + + const networkError = new NetworkError( + `Token exchange failed: ${response.status}`, + errorData.error_description || + errorData.error || + `HTTP ${response.status}: ${response.statusText}`, + new Error(`HTTP ${response.status}`), + ); + + // Emit NETWORK_ERROR event + this.triggerConnectStateChange("NETWORK_ERROR", null, { + operation: "token_exchange", + error: networkError, + }); + + throw networkError; + } + + const tokenResponse: TokenResponse = await response.json(); + + // Parse grant info from ID token + const grantInfo = this.parseIdToken(tokenResponse.id_token); + + const authResult: ConnectResult = { + accessToken: tokenResponse.access_token, + idToken: tokenResponse.id_token, + grantId: tokenResponse.grant_id, + expiresAt: Date.now() + tokenResponse.expires_in * 1000, + scope: tokenResponse.scope, + grantInfo, + }; + + // Store tokens + const key = this.tokenKey(tokenResponse.grant_id); + await this.storage.set(key, JSON.stringify(authResult)); + + // Also store as default if no other default exists + const defaultToken = await this.storage.get(this.tokenKey()); + if (!defaultToken) { + await this.storage.set(this.tokenKey(), JSON.stringify(authResult)); + } + + logger.info("Authentication successful", { + grantId: tokenResponse.grant_id, + scope: tokenResponse.scope, + }); + + return authResult; + } catch (error) { + if (error instanceof NetworkError) { + // Already handled and emitted above + throw error; + } + + const networkError = new NetworkError( + "Token exchange failed", + "Failed to exchange authorization code for tokens", + error as Error, + ); + + // Emit NETWORK_ERROR event for unexpected errors + this.triggerConnectStateChange("NETWORK_ERROR", null, { + operation: "token_exchange", + error: networkError, + }); + + throw networkError; + } + } + + /** + * Parse grant information from ID token + */ + private parseIdToken(idToken: string): GrantInfo { + try { + // Simple JWT parsing (header.payload.signature) + const parts = idToken.split("."); + if (parts.length !== 3) { + throw new Error("Invalid JWT format"); + } + + // Decode payload (base64url) + const payload = parts[1]; + const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/")); + const claims = JSON.parse(decoded); + + return { + id: claims.sub, + email: claims.email, + name: claims.name || claims.given_name || claims.email, + picture: claims.picture, + provider: claims.provider || "unknown", + emailVerified: claims.email_verified, + givenName: claims.given_name, + familyName: claims.family_name, + }; + } catch (error) { + logger.error("Failed to parse ID token", error); + throw new TokenError( + "Invalid ID token", + "Failed to parse grant information from token", + ); + } + } + + /** + * Create appropriate storage instance + * Respects persistTokens setting - uses memory storage when persistTokens is false + */ + private createStorage(): TokenStorage { + // If persistTokens is false, always use memory storage + if (!this.config.persistTokens) { + return new MemoryTokenStorage(); + } + + try { + if (globalThis.window?.localStorage) { + const testKey = "__nylas_auth_test__"; + localStorage.setItem(testKey, "test"); + localStorage.removeItem(testKey); + return new BrowserTokenStorage(); + } + } catch { + // localStorage not available - fallback to memory storage + } + + // Fallback to memory storage when localStorage is not available + return new MemoryTokenStorage(); + } + + /** + * Create storage instance for OAuth temporary state + * Always uses localStorage for OAuth flow reliability + */ + private createOAuthStorage(): TokenStorage { + try { + if (globalThis.window?.localStorage) { + const testKey = "__nylas_auth_test__"; + localStorage.setItem(testKey, "test"); + localStorage.removeItem(testKey); + return new BrowserTokenStorage(); + } + } catch { + // localStorage not available - fallback to memory storage for OAuth flow + } + + // Fallback to memory storage when localStorage is not available + // OAuth state will be stored in memory (works for popup flow) + return new MemoryTokenStorage(); + } + + private tokenKey(grantId?: string): string { + return grantId ? `token_${grantId}` : "token_default"; + } + private authStateKey(): string { + return `nylas_auth_state_${this.config.clientId}`; + } +} diff --git a/packages/nylas-connect/src/crypto/pkce.test.ts b/packages/nylas-connect/src/crypto/pkce.test.ts new file mode 100644 index 0000000..97b025e --- /dev/null +++ b/packages/nylas-connect/src/crypto/pkce.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from "vitest"; +import { generatePKCE, generateState } from "./pkce"; + +describe("PKCE helpers", () => { + it("generateState returns a url-safe string of reasonable length", () => { + const s = generateState(); + expect(typeof s).toBe("string"); + expect(s.length).toBeGreaterThanOrEqual(16); + expect(/^[A-Za-z0-9\-._~]+$/.test(s)).toBe(true); + }); + + it("generatePKCE returns verifier and challenge", async () => { + const pair = await generatePKCE(); + expect(pair.codeVerifier).toBeDefined(); + expect(pair.codeChallenge).toBeDefined(); + expect(pair.codeVerifier.length).toBeGreaterThan(30); + expect(/^[A-Za-z0-9\-._~]+$/.test(pair.codeVerifier)).toBe(true); + expect(/^[A-Za-z0-9\-_]+$/.test(pair.codeChallenge)).toBe(true); + }); +}); diff --git a/packages/nylas-connect/src/crypto/pkce.ts b/packages/nylas-connect/src/crypto/pkce.ts new file mode 100644 index 0000000..fe64077 --- /dev/null +++ b/packages/nylas-connect/src/crypto/pkce.ts @@ -0,0 +1,43 @@ +import type { PKCEPair } from "../types"; + +/** + * Generate PKCE code verifier and challenge pair + * Uses restricted character set for compatibility with Nylas servers + * and pkce-challenge package for challenge generation + */ +export async function generatePKCE(): Promise { + const codeVerifier = generateRandomString(64); + const encoder = new TextEncoder(); + const data = encoder.encode(codeVerifier); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const hashArray = new Uint8Array(hashBuffer); + const sha256Hash = Array.from(hashArray, (byte) => + byte.toString(16).padStart(2, "0"), + ).join(""); + const codeChallenge = btoa(sha256Hash) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); + return { codeVerifier, codeChallenge }; +} + +/** + * Generate a cryptographically secure random string + * Uses only URL-safe base64 characters for PKCE compatibility (no dots or tildes) + */ +function generateRandomString(length: number): string { + // Use RFC 7636 compliant character set for PKCE code verifier + // Must include unreserved characters: A-Z, a-z, 0-9, -, ., _, ~ + const charset = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; + const array = new Uint8Array(length); + crypto.getRandomValues(array); + return Array.from(array, (byte) => charset[byte % charset.length]).join(""); +} + +/** + * Generate a secure random state parameter + */ +export function generateState(): string { + return generateRandomString(32); +} diff --git a/packages/nylas-connect/src/errors/connect-errors.ts b/packages/nylas-connect/src/errors/connect-errors.ts new file mode 100644 index 0000000..6babebd --- /dev/null +++ b/packages/nylas-connect/src/errors/connect-errors.ts @@ -0,0 +1,283 @@ +import type { ConnectError } from "../types"; + +/** + * Base authentication error class + */ +export class NylasConnectError extends Error implements ConnectError { + public readonly code: string; + public readonly description?: string; + public readonly fix?: string; + public readonly docsUrl?: string; + public readonly originalError?: Error; + + constructor( + code: string, + message: string, + description?: string, + fix?: string, + docsUrl?: string, + originalError?: Error, + ) { + super(message); + this.name = "NylasConnectError"; + this.code = code; + this.description = description; + this.fix = fix; + this.docsUrl = docsUrl || "https://developer.nylas.com/docs/v3/auth/"; + this.originalError = originalError; + } +} + +/** + * Configuration validation error + */ +export class ConfigError extends NylasConnectError { + constructor(message: string, description?: string, fix?: string) { + super( + "config_error", + message, + description, + fix, + "https://developer.nylas.com/docs/v3/auth/", + ); + this.name = "ConfigError"; + } +} + +/** + * Network/HTTP request error + */ +export class NetworkError extends NylasConnectError { + constructor(message: string, description?: string, originalError?: Error) { + super( + "network_error", + message, + description, + "Check your network connection and Nylas service status", + "https://developer.nylas.com/docs/v3/auth/", + originalError, + ); + this.name = "NetworkError"; + } +} + +/** + * OAuth flow error + */ +export class OAuthError extends NylasConnectError { + constructor( + code: string, + message: string, + description?: string, + fix?: string, + ) { + super( + `oauth_${code}`, + message, + description, + fix || + "Check the Nylas documentation for more information about this error.", + "https://developer.nylas.com/docs/v3/auth/hosted-oauth-apikey/", + ); + this.name = "OAuthError"; + } +} + +/** + * OAuth Access Denied Error + */ +export class OAuthAccessDeniedError extends OAuthError { + constructor(description?: string) { + super( + "access_denied", + "Grant access was denied", + description, + "The authentication was cancelled. Try authenticating again when ready to proceed.", + ); + this.name = "OAuthAccessDeniedError"; + } +} + +/** + * OAuth Invalid Request Error + */ +export class OAuthInvalidRequestError extends OAuthError { + constructor(description?: string) { + super( + "invalid_request", + "The request is missing a required parameter or is otherwise invalid", + description, + "Check that your clientId and redirectUri are correct in your NylasConnect configuration.", + ); + this.name = "OAuthInvalidRequestError"; + } +} + +/** + * OAuth Invalid Client Error + */ +export class OAuthInvalidClientError extends OAuthError { + constructor(description?: string) { + super( + "invalid_client", + "Client authentication failed", + description, + "Verify your Nylas Client ID is correct and the application is properly configured.", + ); + this.name = "OAuthInvalidClientError"; + } +} + +/** + * OAuth Invalid Grant Error + */ +export class OAuthInvalidGrantError extends OAuthError { + constructor(description?: string) { + super( + "invalid_grant", + "The provided authorization grant is invalid", + description, + "The authorization code may have expired. Try authenticating again.", + ); + this.name = "OAuthInvalidGrantError"; + } +} + +/** + * OAuth Unauthorized Client Error + */ +export class OAuthUnauthorizedClientError extends OAuthError { + constructor(description?: string) { + super( + "unauthorized_client", + "The client is not authorized to request an access token", + description, + "Check your application configuration in the Nylas Dashboard.", + ); + this.name = "OAuthUnauthorizedClientError"; + } +} + +/** + * OAuth Unsupported Grant Type Error + */ +export class OAuthUnsupportedGrantTypeError extends OAuthError { + constructor(description?: string) { + super( + "unsupported_grant_type", + "The authorization grant type is not supported", + description, + "Ensure you're using the correct OAuth flow. Use PKCE for web applications.", + ); + this.name = "OAuthUnsupportedGrantTypeError"; + } +} + +/** + * OAuth Invalid Scope Error + */ +export class OAuthInvalidScopeError extends OAuthError { + constructor(description?: string) { + super( + "invalid_scope", + "The requested scope is invalid or unknown", + description, + "Check that the requested scopes are valid and enabled for your application.", + ); + this.name = "OAuthInvalidScopeError"; + } +} + +/** + * OAuth Server Error + */ +export class OAuthServerError extends OAuthError { + constructor(description?: string) { + super( + "server_error", + "The authorization server encountered an unexpected condition", + description, + "This is a temporary server issue. Try again in a few moments.", + ); + this.name = "OAuthServerError"; + } +} + +/** + * OAuth Temporarily Unavailable Error + */ +export class OAuthTemporarilyUnavailableError extends OAuthError { + constructor(description?: string) { + super( + "temporarily_unavailable", + "The authorization server is temporarily unavailable", + description, + "The Nylas service is temporarily unavailable. Try again later.", + ); + this.name = "OAuthTemporarilyUnavailableError"; + } +} + +/** + * Token validation/parsing error + */ +export class TokenError extends NylasConnectError { + constructor(message: string, description?: string, originalError?: Error) { + super( + "token_error", + message, + description, + "Ensure tokens are valid and not expired", + "https://developer.nylas.com/docs/v3/auth/", + originalError, + ); + this.name = "TokenError"; + } +} + +/** + * Popup window error + */ +export class PopupError extends NylasConnectError { + constructor(message: string, description?: string) { + super( + "popup_error", + message, + description, + "Ensure popups are not blocked and try authenticating again", + "https://developer.nylas.com/docs/v3/auth/", + ); + this.name = "PopupError"; + } +} + +/** + * Create appropriate error from OAuth response + */ +export function createOAuthError( + error: string, + errorDescription?: string, +): OAuthError { + switch (error) { + case "access_denied": + return new OAuthAccessDeniedError(errorDescription); + case "invalid_request": + return new OAuthInvalidRequestError(errorDescription); + case "invalid_client": + return new OAuthInvalidClientError(errorDescription); + case "invalid_grant": + return new OAuthInvalidGrantError(errorDescription); + case "unauthorized_client": + return new OAuthUnauthorizedClientError(errorDescription); + case "unsupported_grant_type": + return new OAuthUnsupportedGrantTypeError(errorDescription); + case "invalid_scope": + return new OAuthInvalidScopeError(errorDescription); + case "server_error": + return new OAuthServerError(errorDescription); + case "temporarily_unavailable": + return new OAuthTemporarilyUnavailableError(errorDescription); + default: + return new OAuthError(error, `OAuth error: ${error}`, errorDescription); + } +} diff --git a/packages/nylas-connect/src/index.ts b/packages/nylas-connect/src/index.ts new file mode 100644 index 0000000..b5fb143 --- /dev/null +++ b/packages/nylas-connect/src/index.ts @@ -0,0 +1,47 @@ +// Core exports +export { NylasConnect } from "./connect-client"; + +// Type exports +export type { + ConnectConfig, + ConnectOptions, + ConnectResult, + GrantInfo, + ConnectError, + PKCEPair, + TokenStorage, + TokenResponse, + LogLevel, + Provider, + Environment, + ConnectMethod, + ConnectionStatus, + ConnectEvent, + ConnectEventData, + ConnectStateChangeCallback, + SessionData, + // OAuth scope types + GoogleScope, + MicrosoftScope, + YahooScope, + NylasScope, + ProviderScopes, +} from "./types"; + +// Error exports +export { + NylasConnectError, + ConfigError, + NetworkError, + OAuthError, + TokenError, + PopupError, +} from "./errors/connect-errors"; + +// Storage exports +export { BrowserTokenStorage } from "./storage/token-storage"; +export { MemoryTokenStorage } from "./storage/memory-storage"; + +// Utility exports +export { parseConnectCallback, isConnectCallback } from "./utils/redirect"; +export { logger } from "./utils/logger"; diff --git a/packages/nylas-connect/src/storage/memory-storage.ts b/packages/nylas-connect/src/storage/memory-storage.ts new file mode 100644 index 0000000..829fef1 --- /dev/null +++ b/packages/nylas-connect/src/storage/memory-storage.ts @@ -0,0 +1,48 @@ +import type { TokenStorage } from "../types"; + +/** + * In-memory fallback implementation of TokenStorage + * Used when localStorage is not available + */ +export class MemoryTokenStorage implements TokenStorage { + private store = new Map(); + + async set(key: string, value: string): Promise { + this.store.set(key, value); + } + + async get(key: string): Promise { + return this.store.get(key) || null; + } + + async remove(key: string): Promise { + this.store.delete(key); + } + + async clear(): Promise { + this.store.clear(); + } +} + +/** + * Create appropriate storage instance based on environment + */ +export async function createStorage(): Promise { + try { + // Test localStorage availability + if (globalThis.window?.localStorage) { + const testKey = "__nylas_auth_test__"; + localStorage.setItem(testKey, "test"); + localStorage.removeItem(testKey); + + // Import dynamically to avoid issues in non-browser environments + const { BrowserTokenStorage } = await import("./token-storage"); + return new BrowserTokenStorage(); + } + } catch { + // localStorage not available or blocked + } + + // Fallback to memory storage + return new MemoryTokenStorage(); +} diff --git a/packages/nylas-connect/src/storage/token-storage.test.ts b/packages/nylas-connect/src/storage/token-storage.test.ts new file mode 100644 index 0000000..2d32974 --- /dev/null +++ b/packages/nylas-connect/src/storage/token-storage.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { MemoryTokenStorage, createStorage } from "./memory-storage"; + +describe("Token storage", () => { + beforeEach(() => { + // ensure clean environment + delete globalThis.window; + }); + + it("MemoryTokenStorage stores, retrieves, removes, and clears", async () => { + const storage = new MemoryTokenStorage(); + await storage.set("a", "1"); + expect(await storage.get("a")).toBe("1"); + await storage.remove("a"); + expect(await storage.get("a")).toBeNull(); + await storage.set("b", "2"); + await storage.set("c", "3"); + await storage.clear(); + expect(await storage.get("b")).toBeNull(); + expect(await storage.get("c")).toBeNull(); + }); + + it("createStorage falls back to memory when window/localStorage missing", async () => { + const storage = await createStorage(); + // Should be memory fallback + await storage.set("k", "v"); + expect(await storage.get("k")).toBe("v"); + }); +}); diff --git a/packages/nylas-connect/src/storage/token-storage.ts b/packages/nylas-connect/src/storage/token-storage.ts new file mode 100644 index 0000000..63b10bd --- /dev/null +++ b/packages/nylas-connect/src/storage/token-storage.ts @@ -0,0 +1,47 @@ +import type { TokenStorage } from "../types"; + +/** + * Browser localStorage implementation of TokenStorage + */ +export class BrowserTokenStorage implements TokenStorage { + private prefix = "@nylas/connect:"; + + private getKey(key: string): string { + return `${this.prefix}${key}`; + } + + async set(key: string, value: string): Promise { + try { + localStorage.setItem(this.getKey(key), value); + } catch (error) { + throw new Error(`Failed to store value: ${error}`); + } + } + + async get(key: string): Promise { + try { + return localStorage.getItem(this.getKey(key)); + } catch (error) { + throw new Error(`Failed to retrieve value: ${error}`); + } + } + + async remove(key: string): Promise { + try { + localStorage.removeItem(this.getKey(key)); + } catch (error) { + throw new Error(`Failed to remove value: ${error}`); + } + } + + async clear(): Promise { + try { + const keys = Object.keys(localStorage).filter((key) => + key.startsWith(this.prefix), + ); + keys.forEach((key) => localStorage.removeItem(key)); + } catch (error) { + throw new Error(`Failed to clear storage: ${error}`); + } + } +} diff --git a/packages/nylas-connect/src/types.ts b/packages/nylas-connect/src/types.ts new file mode 100644 index 0000000..432d269 --- /dev/null +++ b/packages/nylas-connect/src/types.ts @@ -0,0 +1,361 @@ +/** + * Supported OAuth providers + */ +export type Provider = "google" | "microsoft" | "imap" | "icloud"; + +/** + * Google OAuth scopes for Nylas APIs + * All scopes are prefixed with 'https://www.googleapis.com/auth/' + */ +export type GoogleScope = + // Gmail scopes + | "https://www.googleapis.com/auth/gmail.readonly" + | "https://www.googleapis.com/auth/gmail.modify" + | "https://www.googleapis.com/auth/gmail.send" + | "https://www.googleapis.com/auth/gmail.compose" + | "https://www.googleapis.com/auth/gmail.metadata" + // Calendar scopes + | "https://www.googleapis.com/auth/calendar.readonly" + | "https://www.googleapis.com/auth/calendar.events.readonly" + | "https://www.googleapis.com/auth/calendar.events" + | "https://www.googleapis.com/auth/calendar" + // Contacts scopes + | "https://www.googleapis.com/auth/contacts.readonly" + | "https://www.googleapis.com/auth/contacts"; + +/** + * Microsoft OAuth scopes for Nylas APIs + * All scopes are prefixed with 'https://graph.microsoft.com/' + */ +export type MicrosoftScope = + // Mail scopes + | "https://graph.microsoft.com/Mail.Read" + | "https://graph.microsoft.com/Mail.ReadWrite" + | "https://graph.microsoft.com/Mail.Send" + | "https://graph.microsoft.com/Mail.Read.Shared" + | "https://graph.microsoft.com/Mail.ReadWrite.Shared" + // Calendar scopes + | "https://graph.microsoft.com/Calendars.Read" + | "https://graph.microsoft.com/Calendars.ReadWrite" + | "https://graph.microsoft.com/Calendars.Read.Shared" + | "https://graph.microsoft.com/Calendars.ReadWrite.Shared" + // Contacts scopes + | "https://graph.microsoft.com/Contacts.Read" + | "https://graph.microsoft.com/Contacts.ReadWrite" + | "https://graph.microsoft.com/Contacts.Read.Shared" + | "https://graph.microsoft.com/Contacts.ReadWrite.Shared"; + +/** + * Yahoo OAuth scopes for Nylas APIs + */ +export type YahooScope = "email" | "mail-r"; + +/** + * Union of all supported OAuth scopes + */ +export type NylasScope = GoogleScope | MicrosoftScope | YahooScope | string; + +/** + * Provider-specific scope mappings + */ +export type ProviderScopes = { + google?: GoogleScope[]; + microsoft?: MicrosoftScope[]; + yahoo?: YahooScope[]; + imap?: never; // IMAP doesn't support scopes + icloud?: string[]; // iCloud uses custom scopes +}; + +/** + * Environment configuration + */ +export type Environment = "development" | "staging" | "production"; + +/** + * Core configuration for NylasConnect + */ +export interface ConnectConfig { + /** Nylas Client ID (can be read from NYLAS_CLIENT_ID env var) */ + clientId?: string; + /** Redirect URI for OAuth flow (can be read from NYLAS_REDIRECT_URI env var) */ + redirectUri?: string; + /** Nylas Auth API URL (defaults based on environment) */ + apiUrl?: "https://api.us.nylas.com" | "https://api.eu.nylas.com" | string; + /** Environment (auto-detected or specified) */ + environment?: Environment; + /** Default scopes to request - can be a simple array or provider-specific object */ + defaultScopes?: NylasScope[] | ProviderScopes; + /** Enable debug mode (auto-enabled in development) */ + debug?: boolean; + /** Control token persistence (default: true). When false, tokens are stored in memory only and won't survive page reloads */ + persistTokens?: boolean; + /** Control automatic callback handling in React hook (default: true) */ + autoHandleCallback?: boolean; + /** Set specific log level for the logger (overrides debug flag) */ + logLevel?: LogLevel | "off"; +} + +/** + * Authentication method + */ +export type ConnectMethod = "popup" | "inline"; + +/** + * Options for authentication flow + */ +export interface ConnectOptions { + /** Authentication method (default: 'inline') */ + method?: ConnectMethod; + /** Override default scopes */ + scopes?: NylasScope[]; + /** Specific provider */ + provider?: T; + /** Email hint for login */ + loginHint?: string; + /** Custom state parameter */ + state?: string; + /** Popup window width (default: 500) */ + popupWidth?: number; + /** Popup window height (default: 600) */ + popupHeight?: number; +} + +/** + * Result of successful authentication + */ +export interface ConnectResult { + /** Access token */ + accessToken: string; + /** ID token */ + idToken: string; + /** Grant ID */ + grantId: string; + /** Token expiration timestamp */ + expiresAt: number; + /** Granted scopes */ + scope: string; + /** Grant information */ + grantInfo?: GrantInfo; +} + +/** + * Grant information derived from ID token claims + */ +export interface GrantInfo { + /** Subject ID (sub claim) */ + id: string; + /** Email address */ + email: string; + /** Display name */ + name?: string; + /** Profile picture URL */ + picture?: string; + /** OAuth provider */ + provider: string; + /** Email verified flag */ + emailVerified?: boolean; + /** Given name */ + givenName?: string; + /** Family name */ + familyName?: string; +} + +/** + * Structured authentication error + */ +export interface ConnectError extends Error { + /** Error code */ + code: string; + /** Error description */ + description?: string; + /** Suggestion on how to fix the error */ + fix?: string; + /** Link to relevant documentation */ + docsUrl?: string; + /** Original error if wrapped */ + originalError?: Error; +} + +/** + * PKCE code pair + */ +export interface PKCEPair { + /** Code verifier */ + codeVerifier: string; + /** Code challenge */ + codeChallenge: string; +} + +/** + * Token storage interface + */ +export interface TokenStorage { + /** Store a value */ + set(key: string, value: string): Promise; + /** Retrieve a value */ + get(key: string): Promise; + /** Remove a value */ + remove(key: string): Promise; + /** Clear all values */ + clear(): Promise; +} + +/** + * OAuth token response + */ +export interface TokenResponse { + access_token: string; + id_token: string; + token_type: string; + expires_in: number; + scope: string; + grant_id: string; + refresh_token?: string; +} + +/** + * Debug log levels + */ +export enum LogLevel { + ERROR = "error", + WARN = "warn", + INFO = "info", + DEBUG = "debug", +} + +/** + * Connection status + */ +export type ConnectionStatus = + | "connected" + | "expired" + | "invalid" + | "not_connected"; + +/** + * Comprehensive auth events for state changes and user actions + */ +export type ConnectEvent = + // Authentication Flow Events + | "CONNECT_STARTED" // When connect() is called + | "CONNECT_REDIRECT" // When redirecting to OAuth provider + | "CONNECT_POPUP_OPENED" // When popup window opens + | "CONNECT_POPUP_CLOSED" // When popup window closes + | "CONNECT_CALLBACK_RECEIVED" // When callback URL is processed + | "CONNECT_SUCCESS" // When authentication completes successfully + | "CONNECT_ERROR" // When authentication fails + | "CONNECT_CANCELLED" // When authentication is cancelled + + // Session Management Events + | "SIGNED_IN" // When a grant becomes connected + | "SIGNED_OUT" // When a grant signs out + | "SESSION_RESTORED" // When existing session is found on init + | "SESSION_EXPIRED" // When session expires naturally + | "SESSION_INVALID" // When session is found to be invalid + + // Token Management Events + | "TOKEN_REFRESHED" // When access token is refreshed (existing) + | "TOKEN_REFRESH_ERROR" // When token refresh fails + | "TOKEN_VALIDATION_ERROR" // When token validation fails + + // Grant & Profile Events + | "GRANT_UPDATED" // When grant info changes + | "GRANT_PROFILE_LOADED" // When grant profile is fetched + + // Connection & Network Events + | "CONNECTION_STATUS_CHANGED" // When connection status changes + | "NETWORK_ERROR" // When network requests fail + + // Storage Events + | "STORAGE_ERROR" // When storage operations fail + | "STORAGE_CLEARED"; // When auth storage is cleared + +/** + * Event data payloads for different event types + */ +export interface ConnectEventData { + // Authentication Flow + CONNECT_STARTED: { + method: ConnectMethod; + provider?: string; + scopes: NylasScope[]; + }; + CONNECT_REDIRECT: { url: string; provider?: string }; + CONNECT_POPUP_OPENED: { url: string; provider?: string }; + CONNECT_POPUP_CLOSED: { reason: "completed" | "cancelled" | "error" }; + CONNECT_CALLBACK_RECEIVED: { code?: string; state?: string; error?: string }; + CONNECT_SUCCESS: { grantId: string; provider: string; scopes: NylasScope[] }; + CONNECT_ERROR: { error: ConnectError; step: string }; + CONNECT_CANCELLED: { reason: string }; + + // Session Management + SIGNED_IN: { session: ConnectResult; isNewLogin: boolean }; + SIGNED_OUT: { + grantId?: string; + reason: "user_initiated" | "expired" | "invalid"; + }; + SESSION_RESTORED: { session: ConnectResult; fromStorage: boolean }; + SESSION_EXPIRED: { grantId: string; expiresAt: number }; + SESSION_INVALID: { grantId: string; reason: string }; + + // Token Management + TOKEN_REFRESHED: { grantId: string; newExpiresAt: number }; + TOKEN_REFRESH_ERROR: { grantId: string; error: ConnectError }; + TOKEN_VALIDATION_ERROR: { grantId: string; error: ConnectError }; + + // Grant & Profile + GRANT_UPDATED: { grantInfo: GrantInfo; changes: Partial }; + GRANT_PROFILE_LOADED: { grantInfo: GrantInfo; source: "token" | "api" }; + + // Connection & Network + CONNECTION_STATUS_CHANGED: { + status: ConnectionStatus; + previousStatus: ConnectionStatus; + grantId?: string; + }; + NETWORK_ERROR: { operation: string; error: ConnectError }; + + // Storage + STORAGE_ERROR: { operation: string; key: string; error: Error }; + STORAGE_CLEARED: { reason: string }; +} + +/** + * Enhanced auth state change callback with optional event data + */ +export type ConnectStateChangeCallback = ( + event: T, + session: ConnectResult | null, + data?: ConnectEventData[T], +) => void; + +/** + * Auth status message types for popup/redirect flows + */ +export enum ConnectStatus { + SUCCESS = "NYLAS_CONNECT_SUCCESS", + ERROR = "NYLAS_CONNECT_ERROR", +} + +/** + * Internal auth state for storage + */ +export interface ConnectState { + codeVerifier: string; + state: string; + scopes: NylasScope[]; + timestamp: number; +} + +/** + * Session data stored in storage + */ +export interface SessionData { + accessToken: string; + idToken: string; + grantId: string; + expiresAt: number; + scope: string; + grantInfo?: GrantInfo; + refreshToken?: string; +} diff --git a/packages/nylas-connect/src/utils/logger.test.ts b/packages/nylas-connect/src/utils/logger.test.ts new file mode 100644 index 0000000..c58d778 --- /dev/null +++ b/packages/nylas-connect/src/utils/logger.test.ts @@ -0,0 +1,276 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { logger } from "./logger"; +import { LogLevel } from "../types"; + +describe("Logger", () => { + let consoleSpy: { + log: ReturnType; + info: ReturnType; + warn: ReturnType; + error: ReturnType; + }; + + beforeEach(() => { + // Spy on console methods + consoleSpy = { + log: vi.spyOn(console, "log").mockImplementation(() => {}), + info: vi.spyOn(console, "info").mockImplementation(() => {}), + warn: vi.spyOn(console, "warn").mockImplementation(() => {}), + error: vi.spyOn(console, "error").mockImplementation(() => {}), + }; + }); + + afterEach(() => { + // Reset all spies + Object.values(consoleSpy).forEach((spy) => spy.mockRestore()); + // Reset logger to off (default level) + logger.setLevel("off"); + }); + + describe("Log Levels", () => { + it("should log all levels when set to debug", () => { + logger.setLevel(LogLevel.DEBUG); + + logger.debug("debug message"); + logger.info("info message"); + logger.warn("warn message"); + logger.error("error message"); + + expect(consoleSpy.log).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[DEBUG\]/), + "debug message", + ); + expect(consoleSpy.info).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[INFO\]/), + "info message", + ); + expect(consoleSpy.warn).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[WARN\]/), + "warn message", + ); + expect(consoleSpy.error).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[ERROR\]/), + "error message", + ); + }); + + it("should not log debug when set to info level", () => { + logger.setLevel(LogLevel.INFO); + + logger.debug("debug message"); + logger.info("info message"); + logger.warn("warn message"); + logger.error("error message"); + + expect(consoleSpy.log).not.toHaveBeenCalled(); + expect(consoleSpy.info).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[INFO\]/), + "info message", + ); + expect(consoleSpy.warn).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[WARN\]/), + "warn message", + ); + expect(consoleSpy.error).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[ERROR\]/), + "error message", + ); + }); + + it("should only log errors when set to error level", () => { + logger.setLevel(LogLevel.ERROR); + + logger.debug("debug message"); + logger.info("info message"); + logger.warn("warn message"); + logger.error("error message"); + + expect(consoleSpy.log).not.toHaveBeenCalled(); + expect(consoleSpy.info).not.toHaveBeenCalled(); + expect(consoleSpy.warn).not.toHaveBeenCalled(); + expect(consoleSpy.error).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[ERROR\]/), + "error message", + ); + }); + + it("should not log anything when disabled", () => { + logger.setLevel("off"); + + logger.debug("debug message"); + logger.info("info message"); + logger.warn("warn message"); + logger.error("error message"); + + expect(consoleSpy.log).not.toHaveBeenCalled(); + expect(consoleSpy.info).not.toHaveBeenCalled(); + expect(consoleSpy.warn).not.toHaveBeenCalled(); + expect(consoleSpy.error).not.toHaveBeenCalled(); + }); + }); + + describe("Enable/Disable Methods", () => { + it("should enable debug level when enable() is called", () => { + logger.disable(); + logger.enable(); + + logger.debug("debug message"); + expect(consoleSpy.log).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[DEBUG\]/), + "debug message", + ); + }); + + it("should disable all logging when disable() is called", () => { + logger.enable(); + logger.disable(); + + logger.debug("debug message"); + logger.info("info message"); + logger.warn("warn message"); + logger.error("error message"); + + expect(consoleSpy.log).not.toHaveBeenCalled(); + expect(consoleSpy.info).not.toHaveBeenCalled(); + expect(consoleSpy.warn).not.toHaveBeenCalled(); + expect(consoleSpy.error).not.toHaveBeenCalled(); + }); + }); + + describe("Backward Compatibility", () => { + it("should support log() method as alias for info", () => { + logger.setLevel(LogLevel.INFO); + + logger.log("log message"); + expect(consoleSpy.info).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[INFO\]/), + "log message", + ); + }); + }); + + describe("Message Formatting", () => { + it("should include timestamp and prefix", () => { + logger.setLevel(LogLevel.INFO); + logger.info("test message"); + + expect(consoleSpy.info).toHaveBeenCalledWith( + expect.stringMatching( + /^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[NYLAS-AUTH\] \[INFO\]$/, + ), + "test message", + ); + }); + + it("should handle multiple arguments", () => { + logger.setLevel(LogLevel.INFO); + logger.info("message", { key: "value" }, 123); + + expect(consoleSpy.info).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[INFO\]/), + "message", + { key: "value" }, + 123, + ); + }); + }); + + describe("Auto-enable on localhost", () => { + let originalLocation: Location; + + beforeEach(() => { + // Store original location + originalLocation = window.location; + }); + + afterEach(() => { + // Restore original location + Object.defineProperty(window, "location", { + value: originalLocation, + writable: true, + }); + }); + + it("should auto-enable debug on localhost", () => { + // Mock window.location for localhost + Object.defineProperty(window, "location", { + value: { + hostname: "localhost", + search: "", + }, + writable: true, + }); + + // Create a new logger instance to test initialization + const testLogger = new (logger.constructor as any)(); + + testLogger.debug("debug on localhost"); + expect(consoleSpy.log).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[DEBUG\]/), + "debug on localhost", + ); + }); + + it("should auto-enable debug on 127.0.0.1", () => { + Object.defineProperty(window, "location", { + value: { + hostname: "127.0.0.1", + search: "", + }, + writable: true, + }); + + const testLogger = new (logger.constructor as any)(); + + testLogger.debug("debug on 127.0.0.1"); + expect(consoleSpy.log).toHaveBeenCalledWith( + expect.stringMatching(/\[.*\] \[NYLAS-AUTH\] \[DEBUG\]/), + "debug on 127.0.0.1", + ); + }); + + it("should stay disabled on production domains", () => { + Object.defineProperty(window, "location", { + value: { + hostname: "example.com", + search: "", + }, + writable: true, + }); + + const testLogger = new (logger.constructor as any)(); + + testLogger.debug("debug on production"); + testLogger.info("info on production"); + + expect(consoleSpy.log).not.toHaveBeenCalled(); + expect(consoleSpy.info).not.toHaveBeenCalled(); + }); + + it("should respect explicit debug flag over localhost detection", () => { + // Set up localhost environment + Object.defineProperty(window, "location", { + value: { + hostname: "localhost", + search: "", + }, + writable: true, + }); + + // Explicitly disable via localStorage (should override localhost detection) + localStorage.setItem("NYLAS_CONNECT_DEBUG", "false"); + + try { + const testLogger = new (logger.constructor as any)(); + + testLogger.debug("should not show"); + testLogger.info("should not show"); + + expect(consoleSpy.log).not.toHaveBeenCalled(); + expect(consoleSpy.info).not.toHaveBeenCalled(); + } finally { + localStorage.removeItem("NYLAS_CONNECT_DEBUG"); + } + }); + }); +}); diff --git a/packages/nylas-connect/src/utils/logger.ts b/packages/nylas-connect/src/utils/logger.ts new file mode 100644 index 0000000..fd33929 --- /dev/null +++ b/packages/nylas-connect/src/utils/logger.ts @@ -0,0 +1,152 @@ +/** + * Smart logger with production-safe defaults + * - Disabled by default in production + * - Auto-enables debug mode on localhost/development + * - Can be manually controlled via environment variables or API + */ + +import { LogLevel } from "../types"; + +interface Logger { + debug(...args: any[]): void; + info(...args: any[]): void; + warn(...args: any[]): void; + error(...args: any[]): void; + log(...args: any[]): void; // Alias for info for backward compatibility + setLevel(level: LogLevel | "off"): void; + enable(): void; + disable(): void; +} + +class NylasConnectLogger implements Logger { + private currentLevel: LogLevel | "off"; + private levelPriority: { [key in LogLevel | "off"]: number } = { + off: -1, + [LogLevel.DEBUG]: 0, + [LogLevel.INFO]: 1, + [LogLevel.WARN]: 2, + [LogLevel.ERROR]: 3, + }; + + constructor() { + // Check for debug flag in various environments to set initial level + this.currentLevel = this.getInitialLevel(); + } + + private getInitialLevel(): LogLevel | "off" { + // Check for explicit debug flag first (highest priority) + const explicitDebugFlag = this.getExplicitDebugFlag(); + if (explicitDebugFlag !== null) { + return explicitDebugFlag ? LogLevel.DEBUG : "off"; + } + + // Auto-enable debug on localhost + if (this.isLocalhost()) { + return LogLevel.DEBUG; + } + + // Default to off (disabled) for production and non-localhost environments + return "off"; + } + + private getExplicitDebugFlag(): boolean | null { + // Browser environment + if (typeof window !== "undefined") { + // Check localStorage first + const localStorageFlag = localStorage.getItem("NYLAS_CONNECT_DEBUG"); + if (localStorageFlag !== null) { + return localStorageFlag === "true"; + } + } + + // Node.js environment + if (typeof process !== "undefined" && process.env) { + const envFlag = process.env.NYLAS_CONNECT_DEBUG; + if (envFlag !== undefined) { + return envFlag === "true"; + } + } + + return null; // No explicit flag set + } + + private isLocalhost(): boolean { + // Browser environment + if (globalThis.window) { + const hostname = globalThis.window.location.hostname; + return ( + hostname === "localhost" || + hostname === "127.0.0.1" || + hostname === "::1" || + hostname.endsWith(".local") + ); + } + + // Node.js environment - check for common development indicators + if (typeof process !== "undefined" && process.env) { + const nodeEnv = process.env.NODE_ENV; + const isDev = nodeEnv === "development" || nodeEnv === "dev"; + const hasLocalhost = + process.env.HOST?.includes("localhost") || + process.env.HOSTNAME?.includes("localhost"); + return isDev || hasLocalhost || false; + } + + return false; + } + + private shouldLog(level: LogLevel): boolean { + return ( + this.currentLevel !== "off" && + this.levelPriority[level] >= this.levelPriority[this.currentLevel] + ); + } + + private formatMessage(level: LogLevel, ...args: any[]): void { + if (!this.shouldLog(level)) { + return; + } + + const timestamp = new Date().toISOString(); + const prefix = `[${timestamp}] [NYLAS-AUTH] [${level.toUpperCase()}]`; + + console[level === LogLevel.DEBUG ? "log" : level](prefix, ...args); + } + + debug(...args: any[]): void { + this.formatMessage(LogLevel.DEBUG, ...args); + } + + info(...args: any[]): void { + this.formatMessage(LogLevel.INFO, ...args); + } + + warn(...args: any[]): void { + this.formatMessage(LogLevel.WARN, ...args); + } + + error(...args: any[]): void { + this.formatMessage(LogLevel.ERROR, ...args); + } + + log(...args: any[]): void { + // Alias for info to maintain backward compatibility with console.log + this.formatMessage(LogLevel.INFO, ...args); + } + + setLevel(level: LogLevel | "off"): void { + this.currentLevel = level; + } + + enable(): void { + // Enable with debug level when explicitly enabled + this.currentLevel = LogLevel.DEBUG; + } + + disable(): void { + this.currentLevel = "off"; + } +} + +// Export a singleton instance +export const logger = new NylasConnectLogger(); diff --git a/packages/nylas-connect/src/utils/popup.ts b/packages/nylas-connect/src/utils/popup.ts new file mode 100644 index 0000000..273eb11 --- /dev/null +++ b/packages/nylas-connect/src/utils/popup.ts @@ -0,0 +1,169 @@ +import { PopupError } from "../errors/connect-errors"; +import { ConnectStatus } from "../types"; +import { logger } from "./logger"; + +/** + * Popup window configuration + */ +export interface PopupConfig { + width: number; + height: number; + centered?: boolean; +} + +/** + * Default popup configuration + */ +const DEFAULT_POPUP_CONFIG: PopupConfig = { + width: 500, + height: 600, + centered: true, +}; + +/** + * Open a popup window for OAuth authentication + */ +export function openPopup( + url: string, + config: Partial = {}, +): Window { + const finalConfig = { ...DEFAULT_POPUP_CONFIG, ...config }; + + // Calculate position for centering + let left = 0; + let top = 0; + + if (finalConfig.centered && globalThis.window) { + left = + globalThis.window.screenX + + (globalThis.window.outerWidth - finalConfig.width) / 2; + top = + globalThis.window.screenY + + (globalThis.window.outerHeight - finalConfig.height) / 2.5; + } + + const features = [ + `width=${finalConfig.width}`, + `height=${finalConfig.height}`, + `left=${left}`, + `top=${top}`, + "scrollbars=yes", + "resizable=yes", + "status=no", + "toolbar=no", + "menubar=no", + "location=no", + ].join(","); + + logger.debug("Opening popup window", { url, features }); + + if (!globalThis.window) { + throw new PopupError( + "Popup functionality requires browser environment", + "Window object is not available. This may be due to server-side rendering or non-browser environment.", + ); + } + + const popup = globalThis.window.open(url, "nylas-auth-popup", features); + + if (!popup) { + throw new PopupError( + "Failed to open popup window", + "Popup may have been blocked by browser. Please allow popups and try again.", + ); + } + + return popup; +} + +/** + * Wait for authentication callback in popup + */ +export function waitForCallback( + popup: Window, + expectedState: string, +): Promise { + return new Promise((resolve, reject) => { + if (!globalThis.window) { + reject( + new PopupError("Window object not available for message handling"), + ); + return; + } + + const checkClosed = setInterval(() => { + if (popup.closed) { + clearInterval(checkClosed); + reject( + new PopupError("Popup was closed before authentication completed"), + ); + } + }, 1000); + + const messageHandler = (event: MessageEvent) => { + // Verify origin if needed + logger.debug("Received popup message", event.data); + if (event.data.type === ConnectStatus.SUCCESS) { + clearInterval(checkClosed); + globalThis.window.removeEventListener("message", messageHandler); + popup.close(); + + // Verify state parameter + if (event.data.state !== expectedState) { + reject(new PopupError("Invalid state parameter in callback")); + return; + } + + if (event.data.code) { + resolve(event.data.code); + } else { + reject(new PopupError("No authorization code received")); + } + } else if (event.data.type === ConnectStatus.ERROR) { + clearInterval(checkClosed); + globalThis.window.removeEventListener("message", messageHandler); + popup.close(); + + reject( + new PopupError( + event.data.error || "Authentication failed", + event.data.error_description, + ), + ); + } + }; + + globalThis.window.addEventListener("message", messageHandler); + + // Fallback timeout + setTimeout(() => { + if (!popup.closed) { + clearInterval(checkClosed); + globalThis.window.removeEventListener("message", messageHandler); + popup.close(); + reject(new PopupError("Authentication timeout")); + } + }, 300000); // 5 minutes + }); +} + +/** + * Send message from popup to parent window + * This should be called from the redirect URI page + */ +export function sendPopupMessage(data: any): void { + if ( + globalThis.window?.opener && + globalThis.window.name === "nylas-auth-popup" + ) { + logger.debug("Sending message to parent window", data); + + // Check if postMessage is available + if (typeof globalThis.window.opener.postMessage === "function") { + globalThis.window.opener.postMessage( + data, + globalThis.window.location.origin, + ); + } + } +} diff --git a/packages/nylas-connect/src/utils/redirect.ts b/packages/nylas-connect/src/utils/redirect.ts new file mode 100644 index 0000000..dba68e4 --- /dev/null +++ b/packages/nylas-connect/src/utils/redirect.ts @@ -0,0 +1,126 @@ +import { logger } from "./logger"; + +/** + * Parse authorization code and state from URL parameters + */ +export function parseConnectCallback(url?: string): { + code?: string; + state?: string; + error?: string; + errorDescription?: string; +} { + const targetUrl = + url || (globalThis.window ? globalThis.window.location.href : ""); + const urlObj = new URL(targetUrl); + const params = urlObj.searchParams; + + logger.debug("Parsing auth callback URL", { url: targetUrl }); + + return { + code: params.get("code") || undefined, + state: params.get("state") || undefined, + error: params.get("error") || undefined, + errorDescription: params.get("error_description") || undefined, + }; +} + +/** + * Check if current URL contains auth callback parameters + */ +export function isConnectCallback(url?: string): boolean { + const { code, error } = parseConnectCallback(url); + return !!(code || error); +} + +/** + * Clean auth parameters from URL without triggering navigation + */ +export function cleanUrl(): void { + if (!globalThis.window?.history?.replaceState) { + return; + } + + const url = new URL(globalThis.window.location.href); + const authParams = ["code", "state", "error", "error_description"]; + let cleaned = false; + + authParams.forEach((param) => { + if (url.searchParams.has(param)) { + url.searchParams.delete(param); + cleaned = true; + } + }); + + if (cleaned) { + logger.debug("Cleaning auth parameters from URL"); + globalThis.window.history.replaceState( + globalThis.window.history.state, + document.title, + url.toString(), + ); + } +} + +/** + * Build authorization URL + */ +export function buildAuthUrl(config: { + apiUrl: string; + clientId: string; + redirectUri: string; + scopes: string[]; + state: string; + provider?: string; + loginHint?: string; + codeChallenge?: string; // optional to support backend-only (no PKCE) +}): string { + // When PKCE is used (frontend flow), use URLSearchParams which encodes spaces as '+'. + if (config.codeChallenge) { + const url = new URL(`${config.apiUrl}/connect/auth`); + url.searchParams.set("client_id", config.clientId); + url.searchParams.set("redirect_uri", config.redirectUri); + url.searchParams.set("response_type", "code"); + url.searchParams.set("state", config.state); + url.searchParams.set("access_type", "online"); + url.searchParams.set("code_challenge", config.codeChallenge); + url.searchParams.set("code_challenge_method", "S256"); + if (config.scopes && config.scopes.length > 0) { + url.searchParams.set("scope", config.scopes.join(" ")); + } + if (config.provider) { + url.searchParams.set("provider", config.provider); + } + if (config.loginHint) { + url.searchParams.set("login_hint", config.loginHint); + } + logger.debug("Built authorization URL", { url: url.toString() }); + return url.toString(); + } + + // Backend-only flow (no PKCE): build manually so spaces become '%20'. + const base = `${config.apiUrl.replace(/\/+$/, "")}/connect/auth`; + const params: Array<[string, string]> = [ + ["client_id", config.clientId], + ["redirect_uri", config.redirectUri], + ["response_type", "code"], + ["state", config.state], + ["access_type", "online"], + ]; + + if (config.scopes && config.scopes.length > 0) { + params.push(["scope", config.scopes.join(" ")]); + } + if (config.provider) { + params.push(["provider", config.provider]); + } + if (config.loginHint) { + params.push(["login_hint", config.loginHint]); + } + + const query = params + .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) + .join("&"); + const url = `${base}?${query}`; + logger.debug("Built authorization URL", { url }); + return url; +} diff --git a/packages/nylas-connect/tsconfig.json b/packages/nylas-connect/tsconfig.json new file mode 100644 index 0000000..413473c --- /dev/null +++ b/packages/nylas-connect/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + // Relaxed strictness + "noImplicitAny": false, + "strictNullChecks": false, + "noImplicitReturns": false, + + // Modern settings + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/nylas-connect/vite.config.ts b/packages/nylas-connect/vite.config.ts new file mode 100644 index 0000000..dbc0204 --- /dev/null +++ b/packages/nylas-connect/vite.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from "vite"; +import dts from "vite-plugin-dts"; +import path from "path"; + +export default defineConfig({ + plugins: [dts({ insertTypesEntry: true })], + build: { + lib: { + entry: path.resolve(__dirname, "src/index.ts"), + formats: ["es"], + fileName: "index", + }, + rollupOptions: { + external: ["react", "react-dom"], + output: { + globals: { + react: "React", + "react-dom": "ReactDOM", + }, + }, + }, + sourcemap: true, + minify: "esbuild", + target: "es2022", + }, + server: { + port: 3000, + open: true, + host: true, + }, +}); diff --git a/packages/nylas-connect/vitest.config.ts b/packages/nylas-connect/vitest.config.ts new file mode 100644 index 0000000..9e4a936 --- /dev/null +++ b/packages/nylas-connect/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "happy-dom", + include: ["src/**/*.test.ts"], + setupFiles: ["./vitest.setup.ts"], + coverage: { + provider: "v8", + reporter: ["text", "lcov"], + reportsDirectory: "./coverage", + }, + }, +}); diff --git a/packages/nylas-connect/vitest.setup.ts b/packages/nylas-connect/vitest.setup.ts new file mode 100644 index 0000000..ee2a528 --- /dev/null +++ b/packages/nylas-connect/vitest.setup.ts @@ -0,0 +1,16 @@ +// Minimal polyfills for tests +import { webcrypto as nodeWebcrypto } from "node:crypto"; + +if (!globalThis.crypto) { + Object.defineProperty(globalThis, "crypto", { value: nodeWebcrypto }); +} + +// Polyfill btoa/atob if missing (Node) +if (typeof globalThis.btoa !== "function") { + globalThis.btoa = (data: string) => + Buffer.from(data, "binary").toString("base64"); +} +if (typeof globalThis.atob !== "function") { + globalThis.atob = (data: string) => + Buffer.from(data, "base64").toString("binary"); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..577a16c --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3542 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@changesets/cli': + specifier: ^2.27.9 + version: 2.29.7(@types/node@20.19.13) + husky: + specifier: ^8.0.3 + version: 8.0.3 + nx: + specifier: 21.5.2 + version: 21.5.2 + oxlint: + specifier: ^1.16.0 + version: 1.16.0 + prettier: + specifier: ^3.4.2 + version: 3.6.2 + + packages/nylas-connect: + devDependencies: + '@types/node': + specifier: ^20.11.13 + version: 20.19.13 + '@vitest/coverage-v8': + specifier: ^2.1.9 + version: 2.1.9(vitest@2.1.9(@types/node@20.19.13)(happy-dom@13.10.1)) + happy-dom: + specifier: ^13.10.1 + version: 13.10.1 + typescript: + specifier: ^5.3.3 + version: 5.9.2 + vite: + specifier: ^5.0.10 + version: 5.4.20(@types/node@20.19.13) + vite-plugin-dts: + specifier: ^3.7.0 + version: 3.9.1(@types/node@20.19.13)(rollup@4.50.1)(typescript@5.9.2)(vite@5.4.20(@types/node@20.19.13)) + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@20.19.13)(happy-dom@13.10.1) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@changesets/apply-release-plan@7.0.13': + resolution: {integrity: sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg==} + + '@changesets/assemble-release-plan@6.0.9': + resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.29.7': + resolution: {integrity: sha512-R7RqWoaksyyKXbKXBTbT4REdy22yH81mcFK6sWtqSanxUCbUi9Uf+6aqxZtDQouIqPdem2W56CdxXgsxdq7FLQ==} + hasBin: true + + '@changesets/config@3.1.1': + resolution: {integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.3': + resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} + + '@changesets/get-release-plan@4.0.13': + resolution: {integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.1': + resolution: {integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.5': + resolution: {integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + + '@emnapi/core@1.5.0': + resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} + + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@inquirer/external-editor@1.0.2': + resolution: {integrity: sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/diff-sequences@30.0.1': + resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@microsoft/api-extractor-model@7.28.13': + resolution: {integrity: sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==} + + '@microsoft/api-extractor@7.43.0': + resolution: {integrity: sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==} + hasBin: true + + '@microsoft/tsdoc-config@0.16.2': + resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==} + + '@microsoft/tsdoc@0.14.2': + resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} + + '@napi-rs/wasm-runtime@0.2.4': + resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nx/nx-darwin-arm64@21.5.2': + resolution: {integrity: sha512-PrfZbV2blRHoWLor+xDVwPY/dk46kbsmuTXCZRYlNAwko521Y9dCAJT0UOROic3zoUasQ+TwqsQextIcKCotIA==} + cpu: [arm64] + os: [darwin] + + '@nx/nx-darwin-x64@21.5.2': + resolution: {integrity: sha512-YaLY2Cqbjrl+pDddHV7GFtokn81GLvoqg+i9k0Eiid8B0dDLBZpJ3VQKr4RkTzxBX38UuHbJUwrZc8L9z8vqEw==} + cpu: [x64] + os: [darwin] + + '@nx/nx-freebsd-x64@21.5.2': + resolution: {integrity: sha512-2z/Wd42/KHFyT0zRVxWHlaRBQz12Fd1A0FCGJzuWI8G2meh9tYt4MN96gQ4q/rLQ0fmfFEEECq6pmOfCi8t9Mg==} + cpu: [x64] + os: [freebsd] + + '@nx/nx-linux-arm-gnueabihf@21.5.2': + resolution: {integrity: sha512-lY2O1py8x+l39XAFFuplKlzouPC9K/gERYEB/b5jHGf7PGfNj0BX2MDmUztgTty6kKUnkRele39aSoQqWok0gA==} + cpu: [arm] + os: [linux] + + '@nx/nx-linux-arm64-gnu@21.5.2': + resolution: {integrity: sha512-gcpkXXPpWaf8wB0FZUaKmk8Jdv+QMHLiOcQuuXYi1X0vbgotVTl/y+dccwG1EZml6V5JIRGtg2YDM61a7Olp1Q==} + cpu: [arm64] + os: [linux] + + '@nx/nx-linux-arm64-musl@21.5.2': + resolution: {integrity: sha512-oCSUwT0hORgFJWIGjwl6x4/2mVusw+3YAcSrvDePAXadjPSEMLZlJEE+4HExzqLFFBTxc+ucvyOIk08P4BtNJg==} + cpu: [arm64] + os: [linux] + + '@nx/nx-linux-x64-gnu@21.5.2': + resolution: {integrity: sha512-rgJTQk0iaidxEIMOuRQJS36Sk4+qcpJP0uwymvgyoTpZyBdkX38NHH3D+E6sudPSFWsiVxJpkCzYE4ScSKF8Ew==} + cpu: [x64] + os: [linux] + + '@nx/nx-linux-x64-musl@21.5.2': + resolution: {integrity: sha512-KeS36526VruYO9HzhFGqhE5tbps7e94DV0b4j5wfPr7V51EfPzvjAiMWllsQDARv67wdbQ80c0Wg516XTlekgA==} + cpu: [x64] + os: [linux] + + '@nx/nx-win32-arm64-msvc@21.5.2': + resolution: {integrity: sha512-jlRTycYKOiSqc0fcqvabOH/HZX9BOG0S8EGsLmqEr2OkJLZc25ByD1n22P486R2n+m8GQwL6pX+L1LPpOPmz0A==} + cpu: [arm64] + os: [win32] + + '@nx/nx-win32-x64-msvc@21.5.2': + resolution: {integrity: sha512-Ur8GPdz52kLS5uE9IQf0wBtGyvQm4Y3M1ZDjRkR+oGf26aVGNTK6C0+kMJPuggR4Z6lurmHYA34ViGi2hHPPpA==} + cpu: [x64] + os: [win32] + + '@oxlint/darwin-arm64@1.16.0': + resolution: {integrity: sha512-t9sBjbcG15Jgwgw2wY+rtfKEazdkKM/YhcdyjmGYeSjBXaczLfp/gZe03taC2qUHK+t6cxSYNkOLXRLWxaf3tw==} + cpu: [arm64] + os: [darwin] + + '@oxlint/darwin-x64@1.16.0': + resolution: {integrity: sha512-c9aeLQATeu27TK8gR/p8GfRBsuakx0zs+6UHFq/s8Kux+8tYb3pH1pql/XWUPbxubv48F2MpnD5zgjOrShAgag==} + cpu: [x64] + os: [darwin] + + '@oxlint/linux-arm64-gnu@1.16.0': + resolution: {integrity: sha512-ZoBtxtRHhftbiKKeScpgUKIg4cu9s7rsBPCkjfMCY0uLjhKqm6ShPEaIuP8515+/Csouciz1ViZhbrya5ligAg==} + cpu: [arm64] + os: [linux] + + '@oxlint/linux-arm64-musl@1.16.0': + resolution: {integrity: sha512-a/Dys7CTyj1eZIkD59k9Y3lp5YsHBUeZXR7qHTplKb41H+Ivm5OQPf+rfbCBSLMfCPZCeKQPW36GXOSYLNE1uw==} + cpu: [arm64] + os: [linux] + + '@oxlint/linux-x64-gnu@1.16.0': + resolution: {integrity: sha512-rsfv90ytLhl+s7aa8eE8gGwB1XGbiUA2oyUee/RhGRyeoZoe9/hHNtIcE2XndMYlJToROKmGyrTN4MD2c0xxLQ==} + cpu: [x64] + os: [linux] + + '@oxlint/linux-x64-musl@1.16.0': + resolution: {integrity: sha512-djwSL4harw46kdCwaORUvApyE9Y6JSnJ7pF5PHcQlJ7S1IusfjzYljXky4hONPO0otvXWdKq1GpJqhmtM0/xbg==} + cpu: [x64] + os: [linux] + + '@oxlint/win32-arm64@1.16.0': + resolution: {integrity: sha512-lQBfW4hBiQ47P12UAFXyX3RVHlWCSYp6I89YhG+0zoLipxAfyB37P8G8N43T/fkUaleb8lvt0jyNG6jQTkCmhg==} + cpu: [arm64] + os: [win32] + + '@oxlint/win32-x64@1.16.0': + resolution: {integrity: sha512-B5se3JnM4Xu6uHF78hAY9wdk/sdLFib1YwFsLY6rkQKEMFyi+vMZZlDaAS+s+Dt9q7q881U2OhNznZenJZdPdQ==} + cpu: [x64] + os: [win32] + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.50.1': + resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.50.1': + resolution: {integrity: sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.50.1': + resolution: {integrity: sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.50.1': + resolution: {integrity: sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.50.1': + resolution: {integrity: sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.50.1': + resolution: {integrity: sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': + resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.50.1': + resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.50.1': + resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.50.1': + resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': + resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.50.1': + resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.50.1': + resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.50.1': + resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.50.1': + resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.50.1': + resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.50.1': + resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.50.1': + resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.50.1': + resolution: {integrity: sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.50.1': + resolution: {integrity: sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.50.1': + resolution: {integrity: sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==} + cpu: [x64] + os: [win32] + + '@rushstack/node-core-library@4.0.2': + resolution: {integrity: sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/rig-package@0.5.2': + resolution: {integrity: sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==} + + '@rushstack/terminal@0.10.0': + resolution: {integrity: sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/ts-command-line@4.19.1': + resolution: {integrity: sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==} + + '@sinclair/typebox@0.34.41': + resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} + + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + + '@types/argparse@1.0.38': + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@20.19.13': + resolution: {integrity: sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==} + + '@vitest/coverage-v8@2.1.9': + resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} + peerDependencies: + '@vitest/browser': 2.1.9 + vitest: 2.1.9 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + '@volar/language-core@1.11.1': + resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} + + '@volar/source-map@1.11.1': + resolution: {integrity: sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==} + + '@volar/typescript@1.11.1': + resolution: {integrity: sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==} + + '@vue/compiler-core@3.5.21': + resolution: {integrity: sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==} + + '@vue/compiler-dom@3.5.21': + resolution: {integrity: sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==} + + '@vue/language-core@1.8.27': + resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/shared@3.5.21': + resolution: {integrity: sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==} + + '@yarnpkg/lockfile@1.1.0': + resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} + + '@yarnpkg/parsers@3.0.2': + resolution: {integrity: sha512-/HcYgtUSiJiot/XWGLOlGxPYUG65+/31V8oqk17vZLW1xlCoR4PampyePljOxY2n8/3jz9+tIFzICsyGujJZoA==} + engines: {node: '>=18.12.0'} + + '@zkochan/js-yaml@0.0.7': + resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.12.2: + resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chardet@2.1.0: + resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.6.1: + resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} + engines: {node: '>=6'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + computeds@0.0.1: + resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enquirer@2.3.6: + resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} + engines: {node: '>=8.6'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + front-matter@4.0.2: + resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + happy-dom@13.10.1: + resolution: {integrity: sha512-9GZLEFvQL5EgfJX2zcBgu1nsPUn98JF/EiJnSfQbdxI6YEQGqpd09lXXxOmYonRBIEFz9JlGCOiPflDzgS1p8w==} + engines: {node: '>=16.0.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + human-id@4.1.1: + resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==} + hasBin: true + + husky@8.0.3: + resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} + engines: {node: '>=14'} + hasBin: true + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jest-diff@30.1.2: + resolution: {integrity: sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + lines-and-columns@2.0.3: + resolution: {integrity: sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@3.0.8: + resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.3.1: + resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-machine-id@1.1.12: + resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nx@21.5.2: + resolution: {integrity: sha512-hvq3W6mWsNuXzO1VWXpVcbGuF3e4cx0PyPavy8RgZUinbnh3Gk+f+2DGXyjKEyAG3Ql0Nl3V4RJERZzXEVl7EA==} + hasBin: true + peerDependencies: + '@swc-node/register': ^1.8.0 + '@swc/core': ^1.3.85 + peerDependenciesMeta: + '@swc-node/register': + optional: true + '@swc/core': + optional: true + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + ora@5.3.0: + resolution: {integrity: sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==} + engines: {node: '>=10'} + + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + oxlint@1.16.0: + resolution: {integrity: sha512-o6z8s6QVw/d7QuxQ7QFfqDMrIcmHyU3J/MewxjqduJmy4vHt/s7OZISk8zEXjHXZzTWrcFakIrLqU/b9IKTcjg==} + engines: {node: '>=8.*'} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.2.0' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@30.0.5: + resolution: {integrity: sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.19.0: + resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.50.1: + resolution: {integrity: sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.4.2: + resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + validator@13.15.15: + resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} + engines: {node: '>= 0.10'} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite-plugin-dts@3.9.1: + resolution: {integrity: sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + typescript: '*' + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@5.4.20: + resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vue-template-compiler@2.7.16: + resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} + + vue-tsc@1.8.27: + resolution: {integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==} + hasBin: true + peerDependencies: + typescript: '*' + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + z-schema@5.0.5: + resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} + engines: {node: '>=8.0.0'} + hasBin: true + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/runtime@7.28.4': {} + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bcoe/v8-coverage@0.2.3': {} + + '@changesets/apply-release-plan@7.0.13': + dependencies: + '@changesets/config': 3.1.1 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.7.2 + + '@changesets/assemble-release-plan@6.0.9': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.7.2 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.29.7(@types/node@20.19.13)': + dependencies: + '@changesets/apply-release-plan': 7.0.13 + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.1 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-release-plan': 4.0.13 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.5 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.2(@types/node@20.19.13) + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + ci-info: 3.9.0 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + p-limit: 2.3.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.7.2 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.1': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/logger': 0.1.1 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.3': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.7.2 + + '@changesets/get-release-plan@4.0.13': + dependencies: + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/config': 3.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.5 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.1': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 3.14.1 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.5': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.1 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.1.1 + prettier: 2.8.8 + + '@emnapi/core@1.5.0': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.8.1 + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@inquirer/external-editor@1.0.2(@types/node@20.19.13)': + dependencies: + chardet: 2.1.0 + iconv-lite: 0.7.0 + optionalDependencies: + '@types/node': 20.19.13 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/diff-sequences@30.0.1': {} + + '@jest/get-type@30.1.0': {} + + '@jest/schemas@30.0.5': + dependencies: + '@sinclair/typebox': 0.34.41 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.28.4 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.28.4 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@microsoft/api-extractor-model@7.28.13(@types/node@20.19.13)': + dependencies: + '@microsoft/tsdoc': 0.14.2 + '@microsoft/tsdoc-config': 0.16.2 + '@rushstack/node-core-library': 4.0.2(@types/node@20.19.13) + transitivePeerDependencies: + - '@types/node' + + '@microsoft/api-extractor@7.43.0(@types/node@20.19.13)': + dependencies: + '@microsoft/api-extractor-model': 7.28.13(@types/node@20.19.13) + '@microsoft/tsdoc': 0.14.2 + '@microsoft/tsdoc-config': 0.16.2 + '@rushstack/node-core-library': 4.0.2(@types/node@20.19.13) + '@rushstack/rig-package': 0.5.2 + '@rushstack/terminal': 0.10.0(@types/node@20.19.13) + '@rushstack/ts-command-line': 4.19.1(@types/node@20.19.13) + lodash: 4.17.21 + minimatch: 3.0.8 + resolve: 1.22.10 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.4.2 + transitivePeerDependencies: + - '@types/node' + + '@microsoft/tsdoc-config@0.16.2': + dependencies: + '@microsoft/tsdoc': 0.14.2 + ajv: 6.12.6 + jju: 1.4.0 + resolve: 1.19.0 + + '@microsoft/tsdoc@0.14.2': {} + + '@napi-rs/wasm-runtime@0.2.4': + dependencies: + '@emnapi/core': 1.5.0 + '@emnapi/runtime': 1.5.0 + '@tybys/wasm-util': 0.9.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@nx/nx-darwin-arm64@21.5.2': + optional: true + + '@nx/nx-darwin-x64@21.5.2': + optional: true + + '@nx/nx-freebsd-x64@21.5.2': + optional: true + + '@nx/nx-linux-arm-gnueabihf@21.5.2': + optional: true + + '@nx/nx-linux-arm64-gnu@21.5.2': + optional: true + + '@nx/nx-linux-arm64-musl@21.5.2': + optional: true + + '@nx/nx-linux-x64-gnu@21.5.2': + optional: true + + '@nx/nx-linux-x64-musl@21.5.2': + optional: true + + '@nx/nx-win32-arm64-msvc@21.5.2': + optional: true + + '@nx/nx-win32-x64-msvc@21.5.2': + optional: true + + '@oxlint/darwin-arm64@1.16.0': + optional: true + + '@oxlint/darwin-x64@1.16.0': + optional: true + + '@oxlint/linux-arm64-gnu@1.16.0': + optional: true + + '@oxlint/linux-arm64-musl@1.16.0': + optional: true + + '@oxlint/linux-x64-gnu@1.16.0': + optional: true + + '@oxlint/linux-x64-musl@1.16.0': + optional: true + + '@oxlint/win32-arm64@1.16.0': + optional: true + + '@oxlint/win32-x64@1.16.0': + optional: true + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/pluginutils@5.3.0(rollup@4.50.1)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.50.1 + + '@rollup/rollup-android-arm-eabi@4.50.1': + optional: true + + '@rollup/rollup-android-arm64@4.50.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.50.1': + optional: true + + '@rollup/rollup-darwin-x64@4.50.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.50.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.50.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.50.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.50.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.50.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.50.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.50.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.50.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.50.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.50.1': + optional: true + + '@rushstack/node-core-library@4.0.2(@types/node@20.19.13)': + dependencies: + fs-extra: 7.0.1 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.10 + semver: 7.5.4 + z-schema: 5.0.5 + optionalDependencies: + '@types/node': 20.19.13 + + '@rushstack/rig-package@0.5.2': + dependencies: + resolve: 1.22.10 + strip-json-comments: 3.1.1 + + '@rushstack/terminal@0.10.0(@types/node@20.19.13)': + dependencies: + '@rushstack/node-core-library': 4.0.2(@types/node@20.19.13) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 20.19.13 + + '@rushstack/ts-command-line@4.19.1(@types/node@20.19.13)': + dependencies: + '@rushstack/terminal': 0.10.0(@types/node@20.19.13) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + + '@sinclair/typebox@0.34.41': {} + + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + + '@types/argparse@1.0.38': {} + + '@types/estree@1.0.8': {} + + '@types/node@12.20.55': {} + + '@types/node@20.19.13': + dependencies: + undici-types: 6.21.0 + + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@20.19.13)(happy-dom@13.10.1))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.19 + magicast: 0.3.5 + std-env: 3.9.0 + test-exclude: 7.0.1 + tinyrainbow: 1.2.0 + vitest: 2.1.9(@types/node@20.19.13)(happy-dom@13.10.1) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.20(@types/node@20.19.13))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 5.4.20(@types/node@20.19.13) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.19 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + '@volar/language-core@1.11.1': + dependencies: + '@volar/source-map': 1.11.1 + + '@volar/source-map@1.11.1': + dependencies: + muggle-string: 0.3.1 + + '@volar/typescript@1.11.1': + dependencies: + '@volar/language-core': 1.11.1 + path-browserify: 1.0.1 + + '@vue/compiler-core@3.5.21': + dependencies: + '@babel/parser': 7.28.4 + '@vue/shared': 3.5.21 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.21': + dependencies: + '@vue/compiler-core': 3.5.21 + '@vue/shared': 3.5.21 + + '@vue/language-core@1.8.27(typescript@5.9.2)': + dependencies: + '@volar/language-core': 1.11.1 + '@volar/source-map': 1.11.1 + '@vue/compiler-dom': 3.5.21 + '@vue/shared': 3.5.21 + computeds: 0.0.1 + minimatch: 9.0.5 + muggle-string: 0.3.1 + path-browserify: 1.0.1 + vue-template-compiler: 2.7.16 + optionalDependencies: + typescript: 5.9.2 + + '@vue/shared@3.5.21': {} + + '@yarnpkg/lockfile@1.1.0': {} + + '@yarnpkg/parsers@3.0.2': + dependencies: + js-yaml: 3.14.1 + tslib: 2.8.1 + + '@zkochan/js-yaml@0.0.7': + dependencies: + argparse: 2.0.1 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + assertion-error@2.0.1: {} + + asynckit@0.4.0: {} + + axios@1.12.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chardet@2.1.0: {} + + check-error@2.1.1: {} + + ci-info@3.9.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.6.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@9.5.0: + optional: true + + computeds@0.0.1: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + de-indent@1.0.2: {} + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-lazy-prop@2.0.0: {} + + delayed-stream@1.0.0: {} + + detect-indent@6.1.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.4.7 + + dotenv@16.4.7: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enquirer@2.3.6: + dependencies: + ansi-colors: 4.1.3 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + entities@4.5.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + esprima@4.0.1: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.2.2: {} + + extendable-error@0.1.7: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + flat@5.0.2: {} + + follow-redirects@1.15.11: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + front-matter@4.0.2: + dependencies: + js-yaml: 3.14.1 + + fs-constants@1.0.0: {} + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + 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 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + happy-dom@13.10.1: + dependencies: + entities: 4.5.0 + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + html-escaper@2.0.2: {} + + human-id@4.1.1: {} + + husky@8.0.3: {} + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + import-lazy@4.0.0: {} + + inherits@2.0.4: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-number@7.0.0: {} + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-unicode-supported@0.1.0: {} + + is-windows@1.0.2: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jest-diff@30.1.2: + dependencies: + '@jest/diff-sequences': 30.0.1 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.0.5 + + jju@1.4.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + json-schema-traverse@0.4.1: {} + + json5@2.2.3: {} + + jsonc-parser@3.2.0: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + kolorist@1.8.0: {} + + lines-and-columns@2.0.3: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.get@4.4.2: {} + + lodash.isequal@4.5.0: {} + + lodash.startcase@4.4.0: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.2 + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@2.1.0: {} + + minimatch@3.0.8: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.3: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + mri@1.2.0: {} + + ms@2.1.3: {} + + muggle-string@0.3.1: {} + + nanoid@3.3.11: {} + + node-machine-id@1.1.12: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nx@21.5.2: + dependencies: + '@napi-rs/wasm-runtime': 0.2.4 + '@yarnpkg/lockfile': 1.1.0 + '@yarnpkg/parsers': 3.0.2 + '@zkochan/js-yaml': 0.0.7 + axios: 1.12.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + cliui: 8.0.1 + dotenv: 16.4.7 + dotenv-expand: 11.0.7 + enquirer: 2.3.6 + figures: 3.2.0 + flat: 5.0.2 + front-matter: 4.0.2 + ignore: 5.3.2 + jest-diff: 30.1.2 + jsonc-parser: 3.2.0 + lines-and-columns: 2.0.3 + minimatch: 9.0.3 + node-machine-id: 1.1.12 + npm-run-path: 4.0.1 + open: 8.4.2 + ora: 5.3.0 + resolve.exports: 2.0.3 + semver: 7.7.2 + string-width: 4.2.3 + tar-stream: 2.2.0 + tmp: 0.2.5 + tree-kill: 1.2.2 + tsconfig-paths: 4.2.0 + tslib: 2.8.1 + yaml: 2.8.1 + yargs: 17.7.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@nx/nx-darwin-arm64': 21.5.2 + '@nx/nx-darwin-x64': 21.5.2 + '@nx/nx-freebsd-x64': 21.5.2 + '@nx/nx-linux-arm-gnueabihf': 21.5.2 + '@nx/nx-linux-arm64-gnu': 21.5.2 + '@nx/nx-linux-arm64-musl': 21.5.2 + '@nx/nx-linux-x64-gnu': 21.5.2 + '@nx/nx-linux-x64-musl': 21.5.2 + '@nx/nx-win32-arm64-msvc': 21.5.2 + '@nx/nx-win32-x64-msvc': 21.5.2 + transitivePeerDependencies: + - debug + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + ora@5.3.0: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + is-interactive: 1.0.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + outdent@0.5.0: {} + + oxlint@1.16.0: + optionalDependencies: + '@oxlint/darwin-arm64': 1.16.0 + '@oxlint/darwin-x64': 1.16.0 + '@oxlint/linux-arm64-gnu': 1.16.0 + '@oxlint/linux-arm64-musl': 1.16.0 + '@oxlint/linux-x64-gnu': 1.16.0 + '@oxlint/linux-x64-musl': 1.16.0 + '@oxlint/win32-arm64': 1.16.0 + '@oxlint/win32-x64': 1.16.0 + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-type@4.0.0: {} + + pathe@1.1.2: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@4.0.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier@2.8.8: {} + + prettier@3.6.2: {} + + pretty-format@30.0.5: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + react-is@18.3.1: {} + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + require-directory@2.1.1: {} + + resolve-from@5.0.0: {} + + resolve.exports@2.0.3: {} + + resolve@1.19.0: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + reusify@1.1.0: {} + + rollup@4.50.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.50.1 + '@rollup/rollup-android-arm64': 4.50.1 + '@rollup/rollup-darwin-arm64': 4.50.1 + '@rollup/rollup-darwin-x64': 4.50.1 + '@rollup/rollup-freebsd-arm64': 4.50.1 + '@rollup/rollup-freebsd-x64': 4.50.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.50.1 + '@rollup/rollup-linux-arm-musleabihf': 4.50.1 + '@rollup/rollup-linux-arm64-gnu': 4.50.1 + '@rollup/rollup-linux-arm64-musl': 4.50.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.50.1 + '@rollup/rollup-linux-ppc64-gnu': 4.50.1 + '@rollup/rollup-linux-riscv64-gnu': 4.50.1 + '@rollup/rollup-linux-riscv64-musl': 4.50.1 + '@rollup/rollup-linux-s390x-gnu': 4.50.1 + '@rollup/rollup-linux-x64-gnu': 4.50.1 + '@rollup/rollup-linux-x64-musl': 4.50.1 + '@rollup/rollup-openharmony-arm64': 4.50.1 + '@rollup/rollup-win32-arm64-msvc': 4.50.1 + '@rollup/rollup-win32-ia32-msvc': 4.50.1 + '@rollup/rollup-win32-x64-msvc': 4.50.1 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + + semver@7.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + source-map@0.6.1: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + sprintf-js@1.0.3: {} + + stackback@0.0.2: {} + + std-env@3.9.0: {} + + string-argv@0.3.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + term-size@2.2.1: {} + + test-exclude@7.0.1: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.4.5 + minimatch: 9.0.5 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tmp@0.2.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tree-kill@1.2.2: {} + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + typescript@5.4.2: {} + + typescript@5.9.2: {} + + undici-types@6.21.0: {} + + universalify@0.1.2: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + validator@13.15.15: {} + + vite-node@2.1.9(@types/node@20.19.13): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.20(@types/node@20.19.13) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-plugin-dts@3.9.1(@types/node@20.19.13)(rollup@4.50.1)(typescript@5.9.2)(vite@5.4.20(@types/node@20.19.13)): + dependencies: + '@microsoft/api-extractor': 7.43.0(@types/node@20.19.13) + '@rollup/pluginutils': 5.3.0(rollup@4.50.1) + '@vue/language-core': 1.8.27(typescript@5.9.2) + debug: 4.4.1 + kolorist: 1.8.0 + magic-string: 0.30.19 + typescript: 5.9.2 + vue-tsc: 1.8.27(typescript@5.9.2) + optionalDependencies: + vite: 5.4.20(@types/node@20.19.13) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + + vite@5.4.20(@types/node@20.19.13): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.50.1 + optionalDependencies: + '@types/node': 20.19.13 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@20.19.13)(happy-dom@13.10.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.20(@types/node@20.19.13)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.1 + expect-type: 1.2.2 + magic-string: 0.30.19 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.20(@types/node@20.19.13) + vite-node: 2.1.9(@types/node@20.19.13) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.13 + happy-dom: 13.10.1 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vue-template-compiler@2.7.16: + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + vue-tsc@1.8.27(typescript@5.9.2): + dependencies: + '@volar/typescript': 1.11.1 + '@vue/language-core': 1.8.27(typescript@5.9.2) + semver: 7.7.2 + typescript: 5.9.2 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webidl-conversions@7.0.0: {} + + whatwg-mimetype@3.0.0: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + y18n@5.0.8: {} + + yallist@4.0.0: {} + + yaml@2.8.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + z-schema@5.0.5: + dependencies: + lodash.get: 4.4.2 + lodash.isequal: 4.5.0 + validator: 13.15.15 + optionalDependencies: + commander: 9.5.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..25b1d97 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +packages: + - packages/* +onlyBuiltDependencies: + - esbuild + - nx