diff --git a/.env.example b/.env.example index a582588..27aa99d 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,4 @@ VITE_API_BASE_URL=https://localhost:8080 VITE_APP_BASE_URL=/ +VITE_OBSERVABILITY_URL=https://observability.example.com/events +VITE_RELEASE_ID=local diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..de4a16d --- /dev/null +++ b/.env.test @@ -0,0 +1,2 @@ +VITE_API_BASE_URL=http://localhost +VITE_APP_BASE_URL=/ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..530bbde --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto + +*.css text eol=lf +*.html text eol=lf +*.js text eol=lf +*.json text eol=lf +*.md text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +*.yml text eol=lf +*.yaml text eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..cc2e19c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,32 @@ +# Default reviewer for all repository changes. +* @sepaseh + +# Require explicit review for automation and dependency policy changes. +/.github/ @sepaseh +/package.json @sepaseh +/package-lock.json @sepaseh + +# Release policy, version state, and generated release notes. +/.release-please-manifest.json @sepaseh +/CHANGELOG.md @sepaseh +/docs/release-operations.md @sepaseh +/docs/releasing.md @sepaseh +/release-please-config.json @sepaseh + +# Production-like staging gate. +/.github/workflows/staging.yml @sepaseh +/docs/staging.md @sepaseh +/staging.config.ts @sepaseh +/staging/ @sepaseh + +# Security model, review evidence, and active scanning. +/.github/workflows/dast.yml @sepaseh +/docs/security/ @sepaseh +/SECURITY.md @sepaseh + +# Test-effectiveness and frontend/backend compatibility gates. +/.github/workflows/mutation.yml @sepaseh +/contract/ @sepaseh +/docs/contract-testing.md @sepaseh +/stryker.config.json @sepaseh +/vitest.contract.config.ts @sepaseh diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..66eecc1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,48 @@ +version: 2 + +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + day: monday + time: "09:00" + timezone: Asia/Tehran + open-pull-requests-limit: 10 + reviewers: + - sepaseh + commit-message: + prefix: chore + include: scope + groups: + production-minor-and-patch: + dependency-type: production + update-types: + - minor + - patch + development-minor-and-patch: + dependency-type: development + update-types: + - minor + - patch + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: "09:00" + timezone: Asia/Tehran + open-pull-requests-limit: 5 + reviewers: + - sepaseh + commit-message: + prefix: chore + include: scope + groups: + actions-minor-and-patch: + patterns: + - "*" + update-types: + - minor + - patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33e291a..1fa9486 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,71 +17,139 @@ permissions: contents: read env: - NODE_VERSION: 22 + NODE_VERSION: 24.18.0 jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + fetch-depth: 0 + persist-credentials: false - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_VERSION }} cache: npm - run: npm ci + - run: npm run audit + - run: npm run typecheck - run: npm run lint + - run: npm run format:check + - run: npm run test:coverage + - run: npm run test:contract + + - if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: consumer-contracts-${{ github.sha }} + path: pacts + retention-days: 14 + + - name: Validate SonarQube configuration + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository + env: + SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }} + SONAR_PROJECT_KEY: ${{ vars.SONAR_PROJECT_KEY }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + missing=() + + [[ -z "$SONAR_HOST_URL" ]] && missing+=("SONAR_HOST_URL") + [[ -z "$SONAR_PROJECT_KEY" ]] && missing+=("SONAR_PROJECT_KEY") + [[ -z "$SONAR_TOKEN" ]] && missing+=("SONAR_TOKEN") + + if (( ${#missing[@]} > 0 )); then + missing_list=$(IFS=,; echo "${missing[*]}") + echo "::error title=Missing SonarQube configuration::Configure these repository settings: $missing_list" + exit 1 + fi + + - name: SonarQube scan + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository + uses: SonarSource/sonarqube-scan-action@7006c4492b2e0ee0f816d36501671557c97f5995 # v8.1.0 + env: + SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_PROJECT_KEY: ${{ vars.SONAR_PROJECT_KEY }} + with: + args: -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} + - run: npm run knip build: needs: check runs-on: ubuntu-latest + env: + VITE_API_BASE_URL: https://api.example.com + VITE_APP_BASE_URL: / + VITE_RELEASE_ID: ${{ github.sha }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + persist-credentials: false - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_VERSION }} cache: npm - run: npm ci - - run: npm run build + - run: npm run performance - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: dist-${{ github.sha }} path: dist + - if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: lighthouse-${{ github.sha }} + path: .lighthouseci + retention-days: 14 + e2e: needs: check runs-on: ubuntu-latest + env: + VITE_API_BASE_URL: https://api.example.com + VITE_APP_BASE_URL: / + VITE_RELEASE_ID: ${{ github.sha }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + persist-credentials: false - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ env.NODE_VERSION }} cache: npm - run: npm ci - - run: npx playwright install --with-deps chromium + - run: npx playwright install --with-deps chromium firefox webkit - run: npm run test:e2e - if: ${{ !cancelled() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: playwright-report-${{ github.sha }} path: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..c002bf6 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,37 @@ +name: CodeQL + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "30 3 * * 1" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +permissions: + contents: read + +jobs: + analyze: + name: Analyze JavaScript and TypeScript + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - uses: github/codeql-action/init@f52b05f4acaaa234e44466e66d29050e135ea9ef # v4.36.0 + with: + languages: javascript-typescript + queries: security-extended + + - uses: github/codeql-action/analyze@f52b05f4acaaa234e44466e66d29050e135ea9ef # v4.36.0 diff --git a/.github/workflows/dast.yml b/.github/workflows/dast.yml new file mode 100644 index 0000000..a16532c --- /dev/null +++ b/.github/workflows/dast.yml @@ -0,0 +1,63 @@ +name: DAST + +on: + schedule: + - cron: "0 3 1 * *" + workflow_dispatch: + +concurrency: + cancel-in-progress: false + group: staging-dast + +permissions: + contents: read + +jobs: + zap: + environment: staging + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + DAST_ALLOWED_HOST: ${{ vars.STAGING_ALLOWED_HOST }} + DAST_PRODUCTION_HOST: ${{ vars.PRODUCTION_HOST }} + DAST_TARGET: ${{ vars.STAGING_BASE_URL }} + + steps: + - name: Validate authorized target + shell: bash + run: | + node -e ' + const value = process.env.DAST_TARGET; + const allowedHost = process.env.DAST_ALLOWED_HOST?.toLowerCase(); + const productionHost = process.env.DAST_PRODUCTION_HOST?.toLowerCase(); + if (!value) throw new Error("STAGING_BASE_URL is required"); + if (!allowedHost) throw new Error("STAGING_ALLOWED_HOST is required"); + if (!productionHost) throw new Error("PRODUCTION_HOST is required"); + const url = new URL(value); + if (url.protocol !== "https:") { + throw new Error("STAGING_BASE_URL must use HTTPS"); + } + if (url.username || url.password) { + throw new Error("STAGING_BASE_URL must not contain credentials"); + } + if (url.hostname.toLowerCase() !== allowedHost) { + throw new Error("STAGING_BASE_URL host is not explicitly allowed"); + } + if (url.hostname.toLowerCase() === productionHost) { + throw new Error("DAST must not target the production host"); + } + ' + + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + persist-credentials: false + + - name: Run authorized ZAP full scan + uses: zaproxy/action-full-scan@bee4e5be916301228d2b1b2dcad73aab6fa6a25b # v0.13.0 + with: + allow_issue_writing: false + artifact_name: staging-dast-${{ github.run_id }} + cmd_options: "-a" + docker_name: ghcr.io/zaproxy/zaproxy:2.17.0@sha256:c558ee87358911ab17278c70991e856f57793e115d9cd0f88ca475cf82907a1a + fail_action: true + target: ${{ env.DAST_TARGET }} diff --git a/.github/workflows/deployment-smoke.yml b/.github/workflows/deployment-smoke.yml new file mode 100644 index 0000000..c46aa80 --- /dev/null +++ b/.github/workflows/deployment-smoke.yml @@ -0,0 +1,67 @@ +name: Deployment smoke tests + +on: + deployment_status: + workflow_dispatch: + inputs: + application_url: + description: Deployed application base URL + required: true + type: string + api_health_url: + description: Public API health endpoint + required: true + type: string + +concurrency: + cancel-in-progress: true + group: deployment-smoke-${{ github.event.deployment.environment || inputs.application_url }} + +permissions: + contents: read + +jobs: + smoke: + if: >- + github.event_name == 'workflow_dispatch' || + github.event.deployment_status.state == 'success' + runs-on: ubuntu-latest + env: + NODE_VERSION: 24.18.0 + SMOKE_API_HEALTH_URL: >- + ${{ github.event_name == 'workflow_dispatch' && + inputs.api_health_url || vars.SMOKE_API_HEALTH_URL }} + SMOKE_BASE_URL: >- + ${{ github.event_name == 'workflow_dispatch' && + inputs.application_url || + github.event.deployment_status.environment_url }} + + steps: + - name: Validate smoke-test targets + run: | + if [[ -z "$SMOKE_BASE_URL" || -z "$SMOKE_API_HEALTH_URL" ]]; then + echo "SMOKE_BASE_URL and SMOKE_API_HEALTH_URL are required." + exit 1 + fi + + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + cache: npm + node-version: ${{ env.NODE_VERSION }} + + - run: npm ci + + - run: npx playwright install --with-deps chromium + + - run: npm run test:smoke + + - if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: deployment-smoke-${{ github.run_id }} + path: | + playwright-report/smoke + test-results/smoke + retention-days: 14 diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml new file mode 100644 index 0000000..13964fd --- /dev/null +++ b/.github/workflows/mutation.yml @@ -0,0 +1,49 @@ +name: Mutation testing + +on: + schedule: + - cron: "0 4 * * 1" + workflow_dispatch: + pull_request: + branches: + - main + paths: + - "src/api/token.ts" + - "src/api/token.test.ts" + - "src/storage/**" + - "src/utils/**" + - "stryker.config.json" + +concurrency: + cancel-in-progress: true + group: mutation-${{ github.ref }} + +permissions: + contents: read + +jobs: + mutation: + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + NODE_VERSION: 24.18.0 + VITE_API_BASE_URL: http://localhost + + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + cache: npm + node-version: ${{ env.NODE_VERSION }} + + - run: npm ci + + - run: npm run test:mutation + + - if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: mutation-report-${{ github.run_id }} + path: reports/mutation + retention-days: 14 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fec6310 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,23 @@ +name: Release + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Create or update release + uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml new file mode 100644 index 0000000..268ab10 --- /dev/null +++ b/.github/workflows/staging.yml @@ -0,0 +1,70 @@ +name: Staging validation + +on: + deployment_status: + workflow_dispatch: + inputs: + application_url: + description: Staging application base URL + required: true + type: string + api_health_url: + description: Staging API health endpoint + required: true + type: string + +concurrency: + cancel-in-progress: true + group: staging-${{ github.event.deployment.id || inputs.application_url }} + +permissions: + contents: read + +jobs: + validate: + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.deployment_status.state == 'success' && + github.event.deployment.environment == 'staging') + runs-on: ubuntu-latest + env: + NODE_VERSION: 24.18.0 + STAGING_API_HEALTH_URL: >- + ${{ github.event_name == 'workflow_dispatch' && + inputs.api_health_url || vars.STAGING_API_HEALTH_URL }} + STAGING_BASE_URL: >- + ${{ github.event_name == 'workflow_dispatch' && + inputs.application_url || + github.event.deployment_status.environment_url }} + + steps: + - name: Validate staging targets + run: | + if [[ -z "$STAGING_BASE_URL" || -z "$STAGING_API_HEALTH_URL" ]]; then + echo "STAGING_BASE_URL and STAGING_API_HEALTH_URL are required." + exit 1 + fi + + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + persist-credentials: false + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + cache: npm + node-version: ${{ env.NODE_VERSION }} + + - run: npm ci + + - run: npx playwright install --with-deps chromium firefox webkit + + - run: npm run test:staging + + - if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: staging-validation-${{ github.run_id }} + path: | + playwright-report/staging + test-results/staging + retention-days: 14 diff --git a/.gitignore b/.gitignore index baba8a2..a4e70ee 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,10 @@ node_modules dist dist-ssr coverage +.lighthouseci playwright-report +pacts +reports/mutation test-results blob-report *.local diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..ca5c350 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.18.0 diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..37fcefa --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "1.0.0" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..04576a4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +Notable changes to Kernel are recorded here. Release entries after the initial +stable baseline are generated from Conventional Commits by Release Please. + +## 1.0.0 (2026-07-30) + +- Established the first stable release baseline. +- Added production, security, quality, accessibility, and browser test gates. +- Added deployment smoke testing and structured observability. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f26c374 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,66 @@ +# Contributing to Kernel + +Thank you for helping improve Kernel. Keep changes focused, tested, and easy to +review. + +## Development setup + +Kernel uses Node.js 24.15.0 or newer in the Node.js 24 LTS line. If you use +`nvm`, select the repository version before installing dependencies: + +```bash +nvm use +npm ci +cp .env.example .env.local +npm run dev +``` + +On Windows, copy `.env.example` to `.env.local` using your preferred file +manager or shell. + +## Working on a change + +1. Create a branch from the latest `main`. +2. Keep each pull request limited to one coherent change. +3. Add or update tests for behavior changes. +4. Do not commit secrets, local environment files, generated reports, or build + output. +5. Update documentation when behavior, configuration, or public APIs change. + +Use Conventional Commits such as `feat:`, `fix:`, `test:`, `docs:`, and +`chore:`. These messages drive semantic versioning and automated release notes, +so pull request titles must also follow the format when squash merging. See +[docs/releasing.md](docs/releasing.md) for the versioning and release policy. + +## Required checks + +Run the same core checks used by continuous integration before opening a pull +request: + +```bash +npm run typecheck +npm run audit +npm run lint +npm run format:check +npm run test:coverage +npm run knip +npm run build +npm run test:e2e +``` + +Use `npm run format` and `npm run lint:fix` to apply safe automatic fixes. + +## Pull requests + +A pull request should: + +- Explain the problem and the chosen solution. +- Link related issues when applicable. +- Describe testing performed and any known limitations. +- Include screenshots or recordings for visible interface changes. +- Pass all required checks. +- Avoid unrelated formatting or refactoring. + +At least one code-owner review is expected before merging. Security +vulnerabilities must follow [SECURITY.md](SECURITY.md) instead of being +disclosed in a public issue. diff --git a/README.md b/README.md index bbb8938..5d1ed36 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ React starter kit upgraded as a dashboard-ready foundation under the `kernel` pr | UI framework | React 19 + TypeScript | | Build tool | Vite | | Component library | Ant Design 6 | -| Routing | `react-router-dom` 7 | +| Routing | `react-router` 8.3 | | Internationalization | `i18next` + `react-i18next` | | HTTP | Axios | | Date handling | Day.js + Jalaliday | @@ -44,6 +44,7 @@ npm run build # typecheck and build for production npm run preview # preview production build npm run lint # run ESLint npm run lint:fix # run ESLint with auto-fix +npm run audit # check dependencies for high-severity vulnerabilities npm run typecheck # typecheck only npm run knip # detect unused files, exports, and dependencies ``` @@ -134,7 +135,7 @@ Provider-neutral AI assistant guidance lives in `.agents/`. It documents project ## Icons -Icons are centralized in `src/components/Icon.tsx`. Add new icon names to the internal `iconMap`, then render them with: +Icons are centralized in `src/components/icon/Icon.tsx`. Add new icon names to the internal `iconMap`, then render them with: ```tsx diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a3b2277 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,48 @@ +# Security Policy + +## Supported versions + +Kernel is currently maintained on the `main` branch. Security fixes are applied +to the latest code; older commits and forks are not separately supported. + +## Reporting a vulnerability + +Do not disclose suspected vulnerabilities in a public issue, discussion, pull +request, or commit. + +Use the repository's +[private vulnerability reporting](https://github.com/sepaseh/kernel/security/advisories/new) +page to provide: + +- A description of the vulnerability and its potential impact. +- Reproduction steps or a minimal proof of concept. +- Affected routes, components, versions, or commits. +- Any suggested mitigation, if known. + +If private vulnerability reporting is unavailable, contact the repository +owner privately through their GitHub profile and request a secure communication +channel. Do not include sensitive vulnerability details in the initial public +contact. + +Reports will be acknowledged as soon as practical. The maintainer will validate +the issue, determine severity and scope, coordinate a fix, and disclose it after +users have had a reasonable opportunity to update. Please avoid public +disclosure until that process is complete. + +## Security expectations + +Contributors must not commit credentials, tokens, private keys, production +data, or populated environment files. Use `.env.example` for documented +configuration and rotate any secret that may have been exposed. + +## Security assurance + +The maintained security program includes: + +- The [application threat model](docs/security/threat-model.md). +- The [authentication and session review](docs/security/authentication-review.md). +- The [security testing and review schedule](docs/security/reviews.md). +- Monthly active DAST against an explicitly authorized staging environment. + +Security scan artifacts and unresolved vulnerability details are private. +Automated results require human triage and do not replace manual review. diff --git a/contract/api.contract.test.ts b/contract/api.contract.test.ts new file mode 100644 index 0000000..00ddad0 --- /dev/null +++ b/contract/api.contract.test.ts @@ -0,0 +1,100 @@ +import path from "node:path"; + +import { Pact } from "@pact-foundation/pact"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +const createPact = () => + new Pact({ + consumer: "kernel-web", + dir: path.resolve("pacts"), + provider: "kernel-api", + }); + +const loadApiFor = async (baseUrl: string) => { + vi.resetModules(); + vi.stubEnv("VITE_API_BASE_URL", baseUrl); + + return Promise.all([ + import("@/api/account"), + import("@/api/auth"), + import("@/api/token"), + ]); +}; + +afterEach(() => { + vi.unstubAllEnvs(); +}); + +describe("Kernel API consumer contract", () => { + it("logs in with the expected request and token response", async () => { + await createPact() + .addInteraction() + .given("a valid user exists") + .uponReceiving("a login request") + .withRequest("POST", "/auth/login", (request) => { + request.jsonBody({ + identifier: "ada", + password: "correct horse battery staple", + }); + }) + .willRespondWith(200, (response) => { + response.jsonBody({ + access_token: "access-token", + }); + }) + .executeTest(async ({ url }) => { + const [, { login }] = await loadApiFor(url); + + await expect( + login({ + identifier: "ada", + password: "correct horse battery staple", + }), + ).resolves.toEqual({ accessToken: "access-token" }); + }); + }); + + it("reads the authenticated account response", async () => { + await createPact() + .addInteraction() + .given("an authenticated account exists") + .uponReceiving("an account details request") + .withRequest("GET", "/account/me", (request) => { + request.headers({ + Authorization: "Bearer access-token", + }); + }) + .willRespondWith(200, (response) => { + response.jsonBody({ + email: "ada@example.com", + first_name: "Ada", + id: "user-1", + is_system_admin: false, + last_name: "Lovelace", + mobile: "09121111111", + permissions: ["users.read"], + personnel_code: "200", + status: "active", + username: "ada", + }); + }) + .executeTest(async ({ url }) => { + const [{ getAccount }, , { setAccessToken }] = await loadApiFor(url); + + setAccessToken("access-token"); + + await expect(getAccount()).resolves.toEqual({ + email: "ada@example.com", + firstName: "Ada", + id: "user-1", + isSystemAdmin: false, + lastName: "Lovelace", + mobile: "09121111111", + permissions: ["users.read"], + personnelCode: "200", + status: "active", + username: "ada", + }); + }); + }); +}); diff --git a/docs/architecture.md b/docs/architecture.md index 036d48e..079294f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -6,8 +6,8 @@ The main runtime pieces are: -- `src/providers/Core.tsx` for language, theme, current route, and authenticated user state. -- `src/providers/Antd.tsx` for Ant Design locale, direction, and theme tokens. +- `src/providers/core/Core.tsx` for language, theme, current route, and authenticated user state. +- `src/providers/antd/Antd.tsx` for Ant Design locale, direction, and theme tokens. - `src/Routes.tsx` for browser routing. ## Provider Responsibilities @@ -63,6 +63,9 @@ to JavaScript. ## Styling -Global styles live in `src/styles`. The app keeps `IRANSansX` first in the font stack, but font binaries are not committed because their public redistribution license is unclear. Copy licensed files into `public/fonts/iransans` locally and add matching `@font-face` rules in `src/styles/iransans.css` when needed. +Global styles live in `src/styles`. The application bundles the Vazirmatn +variable WOFF2 from `src/assets/fonts/vazirmatn` and uses weights 100–900 with +`font-display: swap`. Vite fingerprints the local asset during production +builds; the application does not depend on an external font provider. Prefer Ant Design components and theme tokens for new UI so layout, spacing, RTL behavior, and dark/light themes stay consistent. diff --git a/docs/branch-protection.md b/docs/branch-protection.md new file mode 100644 index 0000000..767a113 --- /dev/null +++ b/docs/branch-protection.md @@ -0,0 +1,33 @@ +# Main Branch Protection + +The `main` branch is protected on GitHub. Changes must be merged through a pull +request after the branch is current and these checks pass: + +| Required check | Coverage | +| -------------------------------------------- | ------------------------------------------------------- | +| `CI / check` | Audit, types, lint, format, tests, SonarQube, dead code | +| `CI / build` | Validated production build | +| `CI / e2e` | Critical browser journeys | +| `CodeQL / Analyze JavaScript and TypeScript` | CodeQL security analysis | + +Protection also: + +- Applies to repository administrators. +- Requires review conversations to be resolved. +- Prevents force-pushes. +- Prevents deletion of `main`. + +An approving review is not required for ordinary changes while the repository +has only one active maintainer. Release pull requests follow the explicit +approval process in the +[release operations runbook](release-operations.md). Enable at least one +approval and Code Owner review when another maintainer can review pull requests +without blocking all development. + +Configure the GitHub `production` environment separately with required +reviewers. Production deployment jobs must reference that environment so its +approval gate applies after merge and before deployment. + +The SonarQube scan is part of the required `check` job. Configure the repository +settings described in [Testing](testing.md#sonarqube) before treating the +quality gate as active. diff --git a/docs/contract-testing.md b/docs/contract-testing.md new file mode 100644 index 0000000..fbb818a --- /dev/null +++ b/docs/contract-testing.md @@ -0,0 +1,41 @@ +# Mutation and contract testing + +## Mutation testing + +`npm run test:mutation` uses Stryker to make controlled faults in the token, +storage, error, and data-transformation modules. The existing unit tests must +detect, or “kill,” at least 80% of generated mutants for the command to pass. +Scores below 90% remain visible for improvement. + +The focused scope covers small, security-sensitive modules and keeps feedback +practical. Expand it as dedicated unit coverage grows. Do not exclude a mutant +only to raise the score; add an assertion or document why the mutation is +equivalent to the original behavior. + +Mutation testing runs weekly and when its selected source or tests change. +Reports are retained as private workflow artifacts. + +## Consumer contracts + +`npm run test:contract` exercises the real frontend API modules against a Pact +mock provider. It verifies the request method, path, JSON body, wire-format +field names, response shape, and the frontend's snake-case to camel-case +transformation. + +The generated `pacts/kernel-web-kernel-api.json` contract is a CI artifact. The +API repository must verify that contract against the candidate API before +frontend or backend deployment. Publish it to the team's Pact Broker when one +is configured, using the frontend commit SHA as the consumer version and the +branch or release environment for deployment metadata. + +Changes are compatible only when: + +- Consumer contract generation passes. +- The provider verifies every interaction. +- The broker's deployment check reports that the frontend and API versions can + be deployed together. + +Add interactions when the frontend adopts a new endpoint or materially depends +on a new request or response field. Keep provider states deterministic and +free of production data. Pact files are generated evidence and are not +committed. diff --git a/docs/deployment.md b/docs/deployment.md index 17158e7..e5abe9e 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,6 +1,11 @@ # Deployment -This repository includes Docker, nginx, and Docker Compose example files for serving the built frontend. +This repository includes an nginx configuration for serving the built frontend +with client-side routing, long-lived asset caching, and required security +headers. + +Release candidates must pass the production-like +[staging environment](staging.md) before production promotion. ## Production Build @@ -20,7 +25,8 @@ npm run preview ## Environment Variables -Vite reads environment variables at build time. Make sure production values are present before building the image or static bundle. +Vite reads environment variables at build time. Production builds fail when +either required value is missing or invalid. Required values usually include: @@ -35,32 +41,75 @@ VITE_APP_BASE_URL=/ VITE_APP_BASE_URL=/app/ ``` -## Docker +`VITE_API_BASE_URL` must be an absolute HTTPS URL. `VITE_APP_BASE_URL` must +start and end with `/`; use `/` when the application is served at the domain +root. -Build the image: +## Observability -```bash -docker build -t kernel . -``` +Set `VITE_RELEASE_ID` to an immutable deployment identifier, such as the Git +commit SHA. This release is included with every error and performance event. -Run the image: +To deliver events, set `VITE_OBSERVABILITY_URL` to an HTTPS endpoint that +accepts JSON `POST` requests. If it is omitted, instrumentation remains active +but no events leave the browser. Reports include sanitized application errors, +unhandled failures, React component failures, largest contentful paint, +cumulative layout shift, and long tasks. Credential-like values, email +addresses, and URL query values are redacted before delivery. -```bash -docker run --rm -p 8080:80 kernel -``` +## nginx + +Build the application, copy `dist/` to `/usr/share/nginx/html`, and install +`nginx.conf` as the server configuration. It supports client-side routing by +falling back to `index.html`. + +When adding routes in React, no nginx route changes are normally required as long as the fallback remains in place. + +## Security headers -## Docker Compose +The nginx configuration applies: -Use the example compose file as a starting point: +- Content Security Policy restricted to same-origin application resources and + HTTPS API connections +- HTTP Strict Transport Security for one year, including subdomains +- Denial of framing through CSP and `X-Frame-Options` +- MIME sniffing protection +- Strict-origin-when-cross-origin referrer behavior +- Disabled camera, geolocation, microphone, payment, and USB browser features + +HSTS is honored by browsers only over HTTPS. When TLS terminates at a CDN or +load balancer, configure that edge to preserve these response headers. Run the +Playwright suite against the production build to verify the complete policy. + +## Post-deployment smoke tests + +The deployment smoke workflow runs after a successful GitHub deployment status +or can be started manually. It checks that: + +- The deployed HTML and application root load. +- Same-origin scripts, stylesheets, and fonts resolve successfully. +- Direct navigation to `/auth/register` reaches the client-side route. +- The configured public API health endpoint returns a successful status. + +Set the repository variable `SMOKE_API_HEALTH_URL` to the public HTTPS health +endpoint used by automatic deployment runs. The deployment provider must +include its application URL in the successful deployment status. For a manual +run, provide both URLs as workflow inputs. + +To run the same checks locally against a deployed environment: ```bash -docker compose -f docker-compose.example.yml up --build +SMOKE_BASE_URL=https://app.example.com/ \ +SMOKE_API_HEALTH_URL=https://api.example.com/health \ +npm run test:smoke ``` -Review environment values and service names before using it outside local development. - -## nginx +## Release control and rollback -`nginx.conf` serves the static build and supports client-side routing by falling back to `index.html`. +Production deployments must use the immutable artifact associated with the +approved GitHub Release. Keep at least the current and previous production +artifacts available so recovery does not require rebuilding old source. -When adding routes in React, no nginx route changes are normally required as long as the fallback remains in place. +Follow the [release operations runbook](release-operations.md) for approval +evidence, observation windows, rollback triggers, recovery steps, and incident +follow-up. diff --git a/docs/development.md b/docs/development.md index 08591a6..5ff73c8 100644 --- a/docs/development.md +++ b/docs/development.md @@ -4,7 +4,7 @@ This project is a Vite React application written in TypeScript. ## Prerequisites -- Node.js compatible with the versions required by the dependencies in `package-lock.json` +- Node.js 24.15.0 or newer in the Node.js 24 LTS line - npm - Access to the backend API @@ -57,6 +57,10 @@ Build the app: npm run build ``` +Production builds require `VITE_API_BASE_URL` and `VITE_APP_BASE_URL` from +`.env.local` or the build environment. See [Deployment](deployment.md) for +their validation rules. + Check unused files, exports, and dependencies: ```bash @@ -84,4 +88,4 @@ Keep feature code close to the existing structure: The application currently ships with English (`en`) and Persian (`fa`). Translation strings live in `src/locales/en.ts` and `src/locales/fa.ts`. -Ant Design direction and locale are selected in `src/providers/Antd.tsx`. Day.js locale/calendar setup is handled in `src/providers/Core.tsx`, using the Jalali calendar for Persian. +Ant Design direction and locale are selected in `src/providers/antd/Antd.tsx`. Day.js locale/calendar setup is handled in `src/providers/core/Core.tsx`, using the Jalali calendar for Persian. diff --git a/docs/quality-gate.md b/docs/quality-gate.md new file mode 100644 index 0000000..5ee1185 --- /dev/null +++ b/docs/quality-gate.md @@ -0,0 +1,37 @@ +# SonarQube Quality Gate + +Kernel follows Clean as You Code. The SonarQube project must use a quality gate +that fails when new code violates any of these conditions: + +| New-code condition | Required value | +| -------------------------- | -------------- | +| Issues | `0` | +| Security Hotspots reviewed | `100%` | +| Coverage | `>= 80%` | +| Duplicated lines | `<= 3%` | + +These conditions apply to new code only. Existing debt should be improved +incrementally instead of weakening the gate for new changes. + +## SonarQube setup + +An administrator must: + +1. Create a custom quality gate based on `Sonar way`. +2. Require no issues on new code. +3. Keep Security Hotspots reviewed at 100%. +4. Keep new-code coverage at 80% or higher. +5. Keep duplicated lines on new code at 3% or lower. +6. Assign the gate to the Kernel project. +7. Define new code using the previous-version or reference-branch strategy. + +The repository disables SonarQube's small-change exception so coverage and +duplication conditions also apply when a change contains fewer than 20 lines. +CI waits for the quality-gate result and fails when the gate fails. + +The remote gate cannot be configured from this repository. It requires +SonarQube project-administrator access and all three GitHub Actions settings: +the `SONAR_TOKEN` repository secret plus the `SONAR_PROJECT_KEY` and +`SONAR_HOST_URL` repository variables. CI fails before analysis when any +setting is missing. See [Testing](testing.md#sonarqube) for configuration +details. diff --git a/docs/release-operations.md b/docs/release-operations.md new file mode 100644 index 0000000..ef859ec --- /dev/null +++ b/docs/release-operations.md @@ -0,0 +1,112 @@ +# Release approval and rollback + +This runbook governs production releases of Kernel. It complements the +[versioning policy](releasing.md) and the +[deployment guide](deployment.md). + +## Ownership + +| Role | Owner | Responsibility | +| ------------------- | ------------------------------------ | ----------------------------------------------------------------------- | +| Release manager | `@sepaseh` | Reviews the release scope, records approval, and coordinates deployment | +| Deployment operator | Production environment administrator | Deploys the approved immutable artifact and performs rollback | +| Incident lead | On-call maintainer | Decides whether to halt or roll back and coordinates recovery | + +The release manager and deployment operator may be the same person while the +team has one maintainer. With two or more active maintainers, the release pull +request and production deployment should be approved by someone other than the +change author. + +Repository ownership is enforced for release configuration and version files +through `CODEOWNERS`. Configure the GitHub `production` environment with required +reviewers and prevent administrators from bypassing its protection rules. + +## Release approval + +The Release Please pull request is the release candidate. Before approving it, +the release manager must verify: + +- The version matches the intended SemVer impact. +- The changelog is complete, understandable, and contains no sensitive data. +- CI `check`, `build`, and `e2e` jobs pass for the exact candidate commit. +- CodeQL and the configured SonarQube quality gate pass. +- Dependency audit, performance budgets, and production security-header tests + pass. +- Required DAST and manual security reviews have no unresolved release-blocking + findings. +- Required configuration or data migrations have a tested rollback path. +- The previous production artifact and its configuration remain available. +- A deployment operator and incident lead are available for the release window. +- The exact candidate artifact passes the + [staging validation gate](staging.md). + +Approval is recorded by approving and merging the release pull request. Never +publish a tag or deploy from an unreviewed commit. The generated GitHub Release, +tag, built artifact, and deployment must all identify the same commit. + +## Deployment verification + +After deployment: + +1. Record the version, commit SHA, artifact identifier, operator, and start time + in the deployment record. +2. Run the automated deployment smoke tests. +3. Verify login, protected routing, and one read-only authenticated journey. +4. Confirm security headers and API health checks succeed. +5. Compare error rate, failed requests, latency, largest contentful paint, + layout shift, and long tasks with the previous release. +6. Announce success only after the observation window completes. + +Use a minimum 15-minute observation window for routine changes. Extend it for +authentication, authorization, configuration, infrastructure, or major-version +changes. + +## Rollback triggers + +The incident lead should halt rollout or begin rollback when any of these occur: + +- Smoke tests or API health checks fail. +- Users cannot authenticate, authorize, navigate, or complete a critical flow. +- Error rate, failed requests, or latency materially regress from the previous + release. +- Security controls are missing or a new vulnerability or data-exposure risk is + discovered. +- Assets fail to load, client routing breaks, or the deployment configuration + is invalid. +- A migration is incomplete or incompatible with the running frontend. + +When impact is uncertain, prefer stopping further rollout while evidence is +collected. Do not attempt an unrelated forward fix during an active rollback. + +## Rollback procedure + +1. Declare the release unhealthy, assign the incident lead, and pause further + deployments. +2. Capture the failing version, commit SHA, timestamps, symptoms, dashboards, + logs, and smoke-test artifacts. +3. Select the last known-good immutable artifact. Verify its checksum, release + identifier, configuration compatibility, and API compatibility. +4. Promote that artifact using the hosting provider's rollback or deployment + mechanism. Do not rebuild the old source because dependencies and output may + have changed. +5. Restore the matching environment configuration. Run only a separately tested + backward migration when data or schema changes require it. +6. Purge or invalidate cached HTML when necessary; fingerprinted static assets + may remain cached. +7. Re-run deployment smoke tests, critical authentication checks, API health + checks, and security-header verification. +8. Observe production until the rollback version returns to its prior baseline. +9. Record the recovery time and announce restoration. Keep the failed Git tag + and GitHub Release for traceability; mark them as withdrawn instead of + deleting or moving tags. + +If rollback cannot safely restore service, keep the rollout halted, activate +the service's incident-response process, and make the smallest reviewed forward +fix from the last known-good version. + +## Follow-up + +Within two business days, document the cause, customer impact, detection gap, +timeline, and corrective actions. Add regression coverage before retrying the +release. A retry receives a new patch version and repeats the complete approval +process; released versions and tags are never reused. diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..630d0b1 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,51 @@ +# Versioning and releases + +Kernel follows [Semantic Versioning](https://semver.org/). Versions use +`MAJOR.MINOR.PATCH`: + +- `MAJOR` for incompatible behavior or configuration changes. +- `MINOR` for backward-compatible features. +- `PATCH` for backward-compatible fixes. + +The current `1.0.0` version is the first stable baseline. Pre-release versions +use SemVer identifiers such as `2.0.0-rc.1`. + +## Commit policy + +Use Conventional Commits for changes merged into `main`. The release automation +uses these prefixes to determine the next version and build release notes: + +- `fix:` produces a patch release. +- `feat:` produces a minor release. +- A `BREAKING CHANGE:` footer or `!` after the type produces a major release. +- `docs:`, `test:`, `refactor:`, `perf:`, `build:`, `ci:`, and `chore:` do not + request a version bump on their own. + +Scopes are optional, for example `feat(auth): add passkey login`. Pull requests +should be squash-merged with a compliant title so each merged change has one +clear release-note entry. + +## Automated release flow + +After releasable commits reach `main`, the Release workflow creates or updates a +release pull request. That pull request updates: + +- `CHANGELOG.md` +- `package.json` and `package-lock.json` +- `.release-please-manifest.json` + +Review and merge the release pull request only after its required checks pass. +The next workflow run then creates the `vMAJOR.MINOR.PATCH` tag and GitHub +Release with generated notes. This application is private and is not published +to npm. + +Every production release must follow the approval, verification, rollback, and +follow-up steps in the [release operations runbook](release-operations.md). + +The workflow uses the repository `GITHUB_TOKEN` by default. If repository policy +requires release pull requests to trigger other workflows, configure a +fine-grained token or GitHub App token as the `RELEASE_PLEASE_TOKEN` repository +secret according to the Release Please documentation. + +Do not edit generated version or changelog entries outside a release pull +request, except when correcting the release configuration itself. diff --git a/docs/security/authentication-review.md b/docs/security/authentication-review.md new file mode 100644 index 0000000..8b4d59b --- /dev/null +++ b/docs/security/authentication-review.md @@ -0,0 +1,90 @@ +# Authentication and session review + +Complete this checklist quarterly and before any authentication, authorization, +cookie, token, account-recovery, or identity-provider change. + +## Client evidence + +- [x] Access tokens remain in memory only. +- [x] Login and registration replace the in-memory access token. +- [x] Logout clears the access token even when the API request fails. +- [x] A single refresh request is shared by concurrent unauthorized responses. +- [x] Each failed request is retried at most once. +- [x] Refresh failure clears authentication state and returns to public routes. +- [x] Public authentication failures do not start refresh loops. +- [ ] Credentials and account data are absent from local storage, session + storage, URLs, analytics, and observability reports. +- [ ] Redirect targets cannot send users to an untrusted origin. + +## API and cookie evidence + +- [ ] Refresh cookies use `HttpOnly`, `Secure`, an appropriate `SameSite` + setting, the narrowest `Path`, and no overly broad `Domain`. +- [ ] Refresh tokens are rotated on use, detect reuse, expire, and are revoked + on logout, password change, account disablement, and recovery. +- [ ] Credentialed state-changing requests have validated CSRF protection and + origin checks. +- [ ] CORS allows only explicit application origins and credential requirements. +- [ ] Login, OTP, registration, recovery, refresh, and verification endpoints + have rate limits and abuse monitoring. +- [ ] Password and OTP reset artifacts are single-use, short-lived, and never + exposed in logs or URLs beyond the minimum required flow. +- [ ] Authorization is enforced server-side for every object and operation, + including object ownership and tenant/workspace boundaries. +- [ ] Authentication responses and errors do not enable account enumeration. +- [ ] Session creation, refresh, revocation, privilege changes, and suspicious + failures create protected audit events. + +## Browser verification + +- [ ] Cookies and cache headers are inspected in browser developer tools. +- [ ] Cross-origin requests are tested from an untrusted origin. +- [ ] Back-button, refresh, duplicate-tab, expiry, and clock-skew behavior are + tested. +- [ ] Concurrent `401` responses, offline recovery, and API timeouts are tested. +- [ ] Logout is verified in every tab and the old refresh token cannot be used. +- [ ] Role removal takes effect without relying on stale client permissions. + +Record the reviewed release, frontend and API commit SHAs, reviewer, date, +evidence links, findings, owners, severity, and due dates. Put sensitive +findings in a private security advisory rather than a public issue. + +## Initial frontend review — 2026-07-30 + +- Reviewed release: `1.0.0` +- Frontend commit: `c8aa8a63fe2ca8a89f09898e3c95f6015f643e8f` +- API commit: Not supplied; API controls remain unverified and block release + approval. +- Reviewer: Sepaseh +- Evidence: + - [Token storage implementation](../../src/api/token.ts) + - [Authentication operations](../../src/api/auth.ts) + - [Refresh and retry implementation](../../src/api/instance.ts) + - [Authentication operation tests](../../src/api/auth.test.ts) + - [Refresh, retry, and concurrency tests](../../src/api/instance.test.ts) + - [Authenticated-layout cleanup tests](../../src/layouts/default/Default.test.tsx) + +The frontend review confirmed: + +- Access tokens are held in module memory and are not persisted. +- Login and registration set the token; logout clears it in a `finally` block. +- Unauthorized protected requests share one refresh operation, retry once, and + clear authentication after terminal failure. +- Refresh and logout use credentialed HTTPS API requests. +- Unit tests cover concurrency, successful retry, refresh failure, retry-loop + prevention, and cleanup. + +Release approval still requires API-owner evidence for refresh-cookie flags, +rotation and reuse detection, CSRF and CORS enforcement, revocation, endpoint +rate limits, account-enumeration resistance, audit events, and server-side +authorization. These controls cannot be established from frontend source. + +| Finding | Severity | Owner | Due date | Status | +| ----------------------------------------------------------------------------------------------- | -------- | ---------------------------- | ------------------------------- | --------------------------------------------- | +| API authentication and session evidence, including the reviewed API SHA, has not been supplied. | Major | API owner | Before `1.0.0` release approval | Open | +| Same-origin script injection can read the in-memory access token and act as the user. | Major | Frontend and security owners | Reassess by 2026-10-30 | Risk accepted pending defense-in-depth review | + +The residual client risk is that any successful same-origin script injection +can read the in-memory access token and act as the user. CSP, dependency +controls, output encoding, short token lifetime, and server-side authorization +remain required defense in depth. diff --git a/docs/security/reviews.md b/docs/security/reviews.md new file mode 100644 index 0000000..36bd4a1 --- /dev/null +++ b/docs/security/reviews.md @@ -0,0 +1,57 @@ +# Security testing and review schedule + +Security checks are defense in depth and do not replace review by a qualified +person. + +| Activity | Frequency | Owner | Evidence | +| --------------------------------------------- | ------------------------------------------- | ------------------------------ | -------------------------------------- | +| Dependency audit, CodeQL, and secret scanning | Every change | Maintainer | Required CI checks | +| Staging DAST full scan | Monthly and before major releases | Security reviewer | Private workflow artifact | +| Threat-model review | Quarterly and on trust-boundary changes | Security reviewer | Reviewed document change | +| Authentication/session review | Quarterly and on auth changes | Frontend and API owners | Completed checklist | +| Manual application security review | At least annually and before major releases | Independent qualified reviewer | Private report and tracked remediation | +| Rollback exercise | At least twice yearly | Release manager | Exercise record | + +## DAST rules + +The `DAST` workflow runs an active OWASP ZAP full scan against the repository +variable `STAGING_BASE_URL`. The target must be an explicitly authorized, +disposable staging environment. Active scans may submit forms, create data, +trigger email or OTP delivery, and place load on the API. + +Configure a protected GitHub environment named `staging`. Store +`STAGING_BASE_URL`, `STAGING_ALLOWED_HOST`, and `PRODUCTION_HOST` as +environment-scoped variables so repository-level values cannot redirect the +scan. Require approval from a staging security owner before the job can start. + +Before enabling the schedule: + +1. Obtain written authorization from the application, API, hosting, CDN, and + security owners. +2. Seed non-sensitive test data and disable real notifications or downstream + side effects. +3. Confirm backups, rate limits, monitoring, and a cleanup procedure. +4. Set `STAGING_BASE_URL` to the exact HTTPS application origin, + `STAGING_ALLOWED_HOST` to that origin's hostname, and `PRODUCTION_HOST` to the + production hostname. The workflow requires an exact staging-host match and + rejects the production host. +5. Run the workflow manually and review the private artifact. + +The job fails on reported alerts. Do not suppress a rule merely to make the +scan pass. Record a suppression only after review, with the rule ID, affected +URL, evidence, owner, expiry date, and justification. + +Do not run active DAST against production. Authenticated scanning requires a +separate least-privileged test account, explicit scope approval, and secure +injection of short-lived credentials; never commit scan credentials. + +## Finding handling + +Triage every finding for exploitability, affected versions, data exposure, and +release impact. Handle sensitive details through GitHub private vulnerability +reporting. Assign an owner and due date, add regression coverage, and rerun the +relevant scan after remediation. + +Critical or actively exploited findings block deployment and trigger the +incident process. High-severity findings block the affected release unless the +security owner documents a time-limited exception with compensating controls. diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md new file mode 100644 index 0000000..87a1460 --- /dev/null +++ b/docs/security/threat-model.md @@ -0,0 +1,81 @@ +# Threat model + +## Scope + +This model covers the Kernel browser application, its static hosting layer, the +configured API origin, and the authentication/session exchange between them. +The backend implementation, identity provider, CI platform, and hosting +provider are dependencies and must maintain their own threat models. + +Review this document for authentication, authorization, data-flow, +infrastructure, or trust-boundary changes and at least quarterly. + +## Assets + +- Access and refresh credentials. +- User identity, profile, role, permission, and account data. +- Administrative operations for users and roles. +- Build artifacts, release identifiers, source code, and CI credentials. +- Application availability, integrity, audit evidence, and observability data. + +## Trust boundaries and data flow + +1. The browser downloads immutable static assets through the CDN or web server. +2. The browser sends HTTPS API requests to `VITE_API_BASE_URL`. +3. The access token exists only in JavaScript memory and is sent as a bearer + header. It is not intentionally persisted in browser storage. +4. Refresh and logout requests include credentials, so the API controls the + refresh cookie and its lifecycle. +5. Language and theme preferences cross the browser-storage boundary but must + never contain credentials or sensitive account data. +6. CI builds artifacts and security reports using repository variables and + secrets. Staging and production are separate deployment boundaries. +7. Sanitized browser errors and performance events may cross into the configured + observability service. + +## Primary threats and controls + +| Threat | Impact | Current controls | Required verification | +| ------------------------------------------ | ----------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| Script injection or compromised dependency | Credential theft and actions as the user | CSP, dependency audit, CodeQL, no token persistence | Review CSP exceptions and dependency provenance | +| Stolen or replayed refresh cookie | Session takeover | Credentialed refresh endpoint and server logout | Verify `HttpOnly`, `Secure`, `SameSite`, rotation, expiry, and revocation | +| Cross-site request forgery | Unauthorized refresh, logout, or mutation | Same-origin deployment assumptions and API controls | Verify origin checks or CSRF tokens on every credentialed state change | +| Broken access control | Unauthorized user or role administration | Client route/action filtering | Enforce every permission on the API; client checks are not security boundaries | +| Sensitive data exposure | Privacy or credential compromise | HTTPS validation, redacted observability, memory-only access token | Inspect logs, errors, caches, source maps, and browser storage | +| Malicious or vulnerable build input | Compromised production artifact | Pinned Actions, audit, CodeQL, SBOM/license checks, protected branch | Review workflow permissions and artifact provenance | +| Unsafe deployment or rollback | Extended outage or vulnerable release | Staging gate, immutable releases, smoke tests, rollback runbook | Exercise rollback and compare release, artifact, and deployed SHA | +| Denial of service or automated abuse | Unavailable authentication/API | Client request cancellation where applicable | Verify API rate limits, quotas, timeouts, and alerting | +| Clickjacking or content-type confusion | Deceptive UI or script execution | Frame denial, MIME sniffing protection, CSP | Validate headers on CDN and direct routes | +| Observability leakage | Secrets in monitoring systems | Client-side redaction and optional endpoint | Test server-side scrubbing and access/retention policy | + +## Abuse cases + +- An unauthenticated user directly requests an administrative route or API. +- A user edits client state or permissions to reveal a hidden action. +- Multiple expired requests race to refresh a session or replay a token. +- An attacker submits oversized, malformed, or script-bearing form values. +- A malicious site causes credentialed cross-origin requests. +- A compromised dependency reads memory, DOM content, or browser storage. +- A release serves stale HTML that references missing or incompatible assets. +- A scanner or attacker repeatedly triggers OTP, login, registration, or + password-recovery endpoints. + +## Security invariants + +- Authorization decisions are enforced by the API, never only by React. +- Production and security-test traffic use HTTPS. +- Access tokens are not written to local or session storage. +- Refresh cookies are inaccessible to JavaScript and narrowly scoped. +- Logout and terminal refresh failure clear client and server session state. +- Error reports, logs, build artifacts, and test evidence contain no secrets. +- Released tags are immutable and production can return to a known-good + artifact. +- Active security testing targets only explicitly authorized, disposable + staging systems. + +## Residual risks + +The frontend cannot verify API authorization, cookie flags, refresh-token +rotation, revocation, rate limiting, audit logging, database isolation, or +backend input validation. These remain release blockers until confirmed through +backend evidence, integration tests, or a documented security review. diff --git a/docs/staging.md b/docs/staging.md new file mode 100644 index 0000000..1ca8821 --- /dev/null +++ b/docs/staging.md @@ -0,0 +1,73 @@ +# Staging environment + +Staging is the production-like release gate for Kernel. It must use the same +build artifact, web-server configuration, routing rules, security headers, and +API contract intended for production. + +## Environment requirements + +Provision a GitHub environment named exactly `staging` and configure: + +- A stable HTTPS application URL. +- Non-production API, identity, and data stores with production-compatible + schemas. +- The same CDN, reverse-proxy, cache, and security-header behavior as + production. +- Sanitized seed data and test accounts. Never copy production credentials, + tokens, or personal data. +- Access controls that still allow GitHub-hosted test runners to reach the + application and health endpoint. + +Set the repository Actions variable `STAGING_API_HEALTH_URL` to the public HTTPS +health endpoint. The validation job intentionally does not attach itself to a +GitHub environment, because doing so would create another deployment status and +could retrigger the deployment-status workflow. + +Keep staging configuration separate from production secrets. Build once with an +immutable release identifier and promote that artifact between environments; +do not rebuild source specifically for production. + +## Automated validation + +The `Staging validation` workflow runs when GitHub receives a successful +deployment status for the `staging` environment. It can also be started +manually with application and API health URLs. + +The live suite verifies: + +- Application HTML and same-origin assets load successfully. +- Direct client-side routes work. +- Public authentication journeys pass automated accessibility checks. +- CSP, HSTS, framing, MIME-sniffing, referrer, and permissions policies are + present. +- The staging API health endpoint succeeds. +- The checks pass in Chromium, Firefox, WebKit, and a mobile Chromium viewport. + +The workflow rejects missing, non-HTTPS, or credential-bearing target URLs and +retains traces, screenshots, video, and HTML reports when tests fail. + +To run the same gate locally: + +```bash +STAGING_BASE_URL=https://staging.example.com/ \ +STAGING_API_HEALTH_URL=https://staging-api.example.com/health \ +npm run test:staging +``` + +## Promotion + +A release is eligible for production only when: + +1. The ordinary CI, CodeQL, and quality-gate checks pass. +2. The exact candidate artifact is deployed to staging. +3. The Staging validation workflow passes for that deployment. +4. Any required manual acceptance checks complete. +5. The release manager approves promotion using the + [release operations runbook](release-operations.md). + +Failed staging checks block promotion. Fix the issue through a reviewed change, +create a new candidate artifact, and repeat the complete staging gate. + +The separate active DAST workflow also targets staging on the +[security review schedule](security/reviews.md). Its authorization, isolation, +and side-effect controls must be satisfied before enabling scheduled scans. diff --git a/docs/testing.md b/docs/testing.md index e97e0d1..2ef987a 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -10,7 +10,7 @@ fast and maintainable. | Unit | Vitest | Pure functions, hooks, reducers, and isolated logic | | Component | React Testing Library and user-event | User-visible rendering and interactions | | API integration | MSW | Request serialization, response handling, and error states | -| End-to-end | Playwright | A small set of critical journeys in a real browser | +| End-to-end | Playwright and axe-core | Critical journeys and automated accessibility checks | Vitest is the best unit runner for this project because it shares Vite's TypeScript and module resolution model. React Testing Library encourages tests @@ -50,15 +50,44 @@ separate because they run against the built application. - `npm run test:e2e` builds the application and runs Playwright. - `npm run test:e2e:ui` opens Playwright's interactive UI. - `npm run test:e2e:report` opens the most recent HTML report. +- `npm run test:e2e:update-snapshots` reviews and updates visual baselines. +- `npm run performance` builds the app, checks bundle budgets, and runs + Lighthouse. -Local E2E runs use the installed stable Chrome channel. GitHub CI installs and -uses Playwright's pinned Chromium build for reproducibility. If you change the -local project configuration to use bundled Chromium, install it once with: +Vitest loads the tracked `.env.test` file, which provides deterministic local +API and application base URLs for unit, component, and API integration tests. +These values take precedence over developer-specific `.env.local` settings, so +tests never depend on or contact a configured development backend. + +Local Chromium and mobile E2E runs use the installed stable Chrome channel. +Firefox and WebKit use Playwright's pinned browser builds. GitHub CI installs +all three pinned engines for reproducibility. Install the required local +browsers once with: ```sh -npx playwright install chromium +npx playwright install firefox webkit ``` +Visual regression tests run in a dedicated Chromium project with stable mocked +API responses. As with the functional Chromium project, local runs use stable +Chrome and CI uses the pinned Playwright build. Review every changed image +before accepting a baseline update. Generated diffs and actual screenshots +remain available in `test-results/` when a comparison fails. + +## Performance budgets + +The production build enforces two JavaScript size limits: + +- No individual JavaScript chunk may exceed 450 KB. +- All emitted JavaScript chunks combined may not exceed 1.6 MB. + +Lighthouse audits the production login page and requires a performance score of +at least 80, first contentful paint within 3.5 seconds, largest contentful paint +within 4 seconds, time to interactive within 4 seconds, total blocking time +below 300 milliseconds, and cumulative layout shift no greater than 0.1. +Reports are written to `.lighthouseci/` and uploaded by CI for 14 days, +including when the performance job fails. + ## Conventions - Test behavior users can observe; avoid component state and implementation @@ -77,17 +106,51 @@ npx playwright install chromium automatically after each test. - Use Playwright locators and web-first assertions. Do not select by CSS class or XPath. +- Run axe against public forms, authenticated pages, and open dialogs. Keep + color-contrast checks enabled and pair automated scans with keyboard and + focus assertions. +- Run critical browser journeys in English/LTR and Persian/RTL, asserting the + document language and direction as well as translated controls. +- Keep visual snapshots deterministic: use fixed test data, wait for visible + page content, and disable animations and carets during capture. - Add E2E tests only for high-value journeys. Cover permutations and error states lower in the test pyramid. ## Coverage Coverage uses Vitest's V8 provider and includes untested source files. Reports -are written to `coverage/`, which is ignored by Git. The initial global -threshold is intentionally low because Kernel is introducing coverage to an -existing codebase. Raise thresholds as coverage grows; never lower them to make -a change pass. +are written to `coverage/`, which is ignored by Git. Global thresholds preserve +the established baseline across statements, branches, functions, and lines. +Raise thresholds as coverage grows; never lower them to make a change pass. +SonarQube separately requires at least 80% coverage on new code through the +mandatory quality gate. Coverage is a guardrail, not a quality score. Critical authentication, token refresh, authorization, and account-management branches should receive direct behavioral tests even when the global threshold is already satisfied. + +## Test effectiveness and API compatibility + +Mutation testing measures whether focused unit tests detect injected faults, +while Pact consumer contracts protect the request and response boundary with +the backend. See [Mutation and contract testing](contract-testing.md) for local +commands, CI behavior, thresholds, and provider verification requirements. + +## SonarQube + +CI sends the existing LCOV report to SonarQube and requires all of these GitHub +repository settings: + +| Setting | GitHub type | Value | +| ------------------- | ------------------- | ------------------------------------------- | +| `SONAR_HOST_URL` | Repository variable | Base URL of the SonarQube service | +| `SONAR_PROJECT_KEY` | Repository variable | Key of the Kernel project in SonarQube | +| `SONAR_TOKEN` | Repository secret | Project analysis token created in SonarQube | + +Configure them under **Settings → Secrets and variables → Actions**. Store the +token as a secret, never as a variable or committed file. CI fails with the +names of missing settings before analysis starts; it never prints their values. + +The scan waits for the configured quality gate, so a failed gate fails CI. See +[SonarQube Quality Gate](quality-gate.md) for the required new-code conditions +and project setup. diff --git a/e2e/accessibility.spec.ts b/e2e/accessibility.spec.ts new file mode 100644 index 0000000..44fe84d --- /dev/null +++ b/e2e/accessibility.spec.ts @@ -0,0 +1,138 @@ +import AxeBuilder from "@axe-core/playwright"; +import { expect, Page, test } from "@playwright/test"; + +const expectNoAxeViolations = async (page: Page) => { + const results = await new AxeBuilder({ page }).analyze(); + const violations = results.violations.map(({ help, id, impact, nodes }) => ({ + help, + id, + impact, + targets: nodes.map(({ target }) => target), + })); + + expect(violations).toEqual([]); +}; + +const mockAccount = async (page: Page, permissions: string[]) => { + await page.route("https://api.example.com/account/me", async (route) => { + await route.fulfill({ + contentType: "application/json", + json: { + email: "manager@example.com", + first_name: "Test", + id: "manager-1", + is_system_admin: false, + last_name: "Manager", + mobile: "09120000000", + permissions, + personnel_code: "100", + status: "active", + username: "manager", + }, + }); + }); +}; + +test.describe("accessibility", () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + localStorage.setItem("language", "en"); + localStorage.setItem("theme", "light"); + }); + }); + + test("public authentication pages have no detectable violations", async ({ + page, + }) => { + for (const path of ["/auth", "/auth/forgot-password", "/auth/register"]) { + await page.goto(path); + await expect(page.locator("main, form").first()).toBeVisible(); + await expectNoAxeViolations(page); + } + }); + + test("login controls follow a usable keyboard focus order", async ({ + page, + }) => { + await page.goto("/auth"); + + for (let attempt = 0; attempt < 5; attempt += 1) { + if ( + await page + .getByLabel("Username, email, or mobile") + .evaluate((element) => element === document.activeElement) + ) { + break; + } + + await page.keyboard.press("Tab"); + } + + await expect(page.getByLabel("Username, email, or mobile")).toBeFocused(); + await page.keyboard.press("Tab"); + await expect(page.getByLabel("Password")).toBeFocused(); + }); + + test("role dialog is labeled, traps focus, and passes axe", async ({ + page, + }) => { + await mockAccount(page, ["roles.create", "roles.read"]); + await page.route("https://api.example.com/roles", async (route) => { + await route.fulfill({ contentType: "application/json", json: [] }); + }); + await page.route("https://api.example.com/permissions", async (route) => { + await route.fulfill({ + contentType: "application/json", + json: [ + { + name: "users", + permissions: [{ name: "users.read", title: "Read users" }], + title: "Users", + }, + ], + }); + }); + await page.goto("/roles"); + await page.getByRole("button", { name: "Create" }).click(); + const dialog = page.getByRole("dialog", { name: "Create" }); + + await expect(dialog).toBeVisible(); + await expect(dialog.getByLabel("Name")).toBeFocused(); + await expect(dialog.getByLabel("Read users")).toBeVisible(); + await expectNoAxeViolations(page); + }); + + test("authenticated user management has no detectable violations", async ({ + page, + }) => { + await mockAccount(page, ["users.read"]); + await page.route( + /https:\/\/api\.example\.com\/users(?:\?.*)?$/, + async (route) => { + await route.fulfill({ + contentType: "application/json", + json: { + items: [ + { + email: "ada@example.com", + first_name: "Ada", + id: "user-1", + is_system_admin: false, + last_name: "Lovelace", + mobile: "09121111111", + personnel_code: "200", + status: "active", + username: "ada", + }, + ], + total: 1, + }, + }); + }, + ); + await page.goto("/users"); + await expect(page.getByText("Ada Lovelace")).toBeVisible(); + + await expectNoAxeViolations(page); + }); +}); diff --git a/e2e/auth.spec.ts b/e2e/auth.spec.ts index 09665eb..f173227 100644 --- a/e2e/auth.spec.ts +++ b/e2e/auth.spec.ts @@ -1,12 +1,38 @@ -import { expect, test } from "@playwright/test"; +import { expect, Page, test } from "@playwright/test"; + +import { syntheticCredential } from "./fixtures"; + +const accountResponse = { + email: "ada@example.com", + first_name: "Ada", + id: "user-1", + is_system_admin: true, + last_name: "Lovelace", + mobile: "09120000000", + permissions: [], + personnel_code: "100", + status: "active", + username: "ada", +}; + +const fillOtp = async (page: Page, otp: string) => { + for (const [index, digit] of [...otp].entries()) { + await page + .getByRole("textbox", { name: `OTP Input ${index + 1}` }) + .fill(digit); + } +}; test.describe("public authentication", () => { - test("shows the login form and public account recovery links", async ({ - page, - }) => { + test.beforeEach(async ({ page }) => { await page.addInitScript(() => { localStorage.setItem("language", "en"); }); + }); + + test("shows the login form and public account recovery links", async ({ + page, + }) => { await page.goto("/auth"); await expect(page.getByRole("heading", { name: "Login" })).toBeVisible(); @@ -19,4 +45,210 @@ test.describe("public authentication", () => { page.getByRole("link", { name: "Forgot password?" }), ).toHaveAttribute("href", "/auth/forgot-password"); }); + + test("shows the API error when credentials are rejected", async ({ + page, + }) => { + await page.route("https://api.example.com/auth/login", async (route) => { + expect(route.request().postDataJSON()).toEqual({ + identifier: "ada", + password: syntheticCredential, + }); + + await route.fulfill({ + contentType: "application/json", + json: { message: "Invalid credentials" }, + status: 401, + }); + }); + await page.goto("/auth"); + + await page.getByLabel("Username, email, or mobile").fill("ada"); + await page.getByLabel("Password").fill(syntheticCredential); + await page.getByRole("button", { name: "Enter" }).click(); + + await expect(page.getByText("Invalid credentials")).toBeVisible(); + await expect(page).toHaveURL(/\/auth$/); + }); + + test("logs in and loads the authenticated account", async ({ page }) => { + await page.route("https://api.example.com/auth/login", async (route) => { + await route.fulfill({ + contentType: "application/json", + json: { access_token: "access-token" }, + }); + }); + await page.route("https://api.example.com/account/me", async (route) => { + expect(route.request().headers().authorization).toBe( + "Bearer access-token", + ); + + await route.fulfill({ + contentType: "application/json", + json: accountResponse, + }); + }); + await page.goto("/auth"); + + await page.getByLabel("Username, email, or mobile").fill("ada"); + await page.getByLabel("Password").fill(syntheticCredential); + await page.getByRole("button", { name: "Enter" }).click(); + + await expect(page).toHaveURL(/\/$/); + await expect(page.getByRole("link", { name: "kernel" })).toBeVisible(); + }); + + test("recovers a forgotten password and returns to login", async ({ + page, + }) => { + await page.route( + "https://api.example.com/auth/otp-request", + async (route) => { + expect(route.request().postDataJSON()).toEqual({ + mobile: "09120000000", + purpose: "forgot_password", + }); + await route.fulfill({ + contentType: "application/json", + json: { expires_in: 300, remaining_seconds: 60 }, + }); + }, + ); + await page.route( + "https://api.example.com/auth/forgot-password", + async (route) => { + expect(route.request().postDataJSON()).toEqual({ + mobile: "09120000000", + otp: "123456", + password: syntheticCredential, + }); + await route.fulfill({ status: 204 }); + }, + ); + await page.goto("/auth/forgot-password"); + + await page.getByLabel("Mobile").fill("09120000000"); + await page.getByRole("button", { name: "Send code" }).click(); + await expect(page.getByText("Verification code sent.")).toBeVisible(); + await fillOtp(page, "123456"); + await page.getByLabel("New password").fill(syntheticCredential); + await page.getByLabel("Confirm password").fill(syntheticCredential); + await page.getByRole("button", { name: "Submit" }).click(); + + await expect(page).toHaveURL(/\/auth$/); + }); + + test("registers a new account and enters the application", async ({ + page, + }) => { + await page.route( + "https://api.example.com/auth/otp-request", + async (route) => { + expect(route.request().postDataJSON()).toEqual({ + mobile: "09120000000", + purpose: "register", + }); + await route.fulfill({ + contentType: "application/json", + json: { expires_in: 300, remaining_seconds: 60 }, + }); + }, + ); + await page.route("https://api.example.com/auth/register", async (route) => { + expect(route.request().postDataJSON()).toEqual({ + first_name: "Ada", + last_name: "Lovelace", + mobile: "09120000000", + otp: "123456", + password: syntheticCredential, + }); + await route.fulfill({ + contentType: "application/json", + json: { access_token: "registered-token" }, + }); + }); + await page.route("https://api.example.com/account/me", async (route) => { + expect(route.request().headers().authorization).toBe( + "Bearer registered-token", + ); + await route.fulfill({ + contentType: "application/json", + json: accountResponse, + }); + }); + await page.goto("/auth/register"); + + await page.getByLabel("First name").fill("Ada"); + await page.getByLabel("Last name").fill("Lovelace"); + await page.getByLabel("Mobile").fill("09120000000"); + await page.getByRole("button", { name: "Send code" }).click(); + await fillOtp(page, "123456"); + await page + .getByLabel("Password", { exact: true }) + .fill(syntheticCredential); + await page.getByLabel("Confirm password").fill(syntheticCredential); + await page.getByRole("button", { name: "Register" }).click(); + + await expect(page).toHaveURL(/\/$/); + await expect(page.getByRole("link", { name: "kernel" })).toBeVisible(); + }); + + test("logs out and returns to login", async ({ page }) => { + await page.route("https://api.example.com/auth/login", async (route) => { + await route.fulfill({ + contentType: "application/json", + json: { access_token: "access-token" }, + }); + }); + await page.route("https://api.example.com/account/me", async (route) => { + await route.fulfill({ + contentType: "application/json", + json: accountResponse, + }); + }); + await page.route("https://api.example.com/auth/logout", async (route) => { + expect(route.request().headers().authorization).toBe( + "Bearer access-token", + ); + await route.fulfill({ status: 204 }); + }); + await page.goto("/auth"); + await page.getByLabel("Username, email, or mobile").fill("ada"); + await page.getByLabel("Password").fill(syntheticCredential); + await page.getByRole("button", { name: "Enter" }).click(); + await expect(page.getByRole("link", { name: "kernel" })).toBeVisible(); + + await page.getByRole("button", { name: "Account" }).click(); + await page.getByRole("menuitem", { name: "Logout" }).click(); + + await expect(page).toHaveURL(/\/auth$/); + await expect(page.getByRole("heading", { name: "Login" })).toBeVisible(); + }); + + test("redirects unauthenticated users away from protected routes", async ({ + page, + }) => { + await page.route("https://api.example.com/account/me", async (route) => { + await route.fulfill({ + contentType: "application/json", + json: { message: "Unauthorized" }, + status: 401, + }); + }); + await page.route( + "https://api.example.com/auth/refresh-token", + async (route) => { + await route.fulfill({ + contentType: "application/json", + json: { message: "Unauthorized" }, + status: 401, + }); + }, + ); + + await page.goto("/users"); + + await expect(page).toHaveURL(/\/auth$/); + await expect(page.getByRole("heading", { name: "Login" })).toBeVisible(); + }); }); diff --git a/e2e/fixtures.ts b/e2e/fixtures.ts new file mode 100644 index 0000000..6f64f34 --- /dev/null +++ b/e2e/fixtures.ts @@ -0,0 +1 @@ +export const syntheticCredential = "synthetic-test-credential"; diff --git a/e2e/font.spec.ts b/e2e/font.spec.ts new file mode 100644 index 0000000..01e2433 --- /dev/null +++ b/e2e/font.spec.ts @@ -0,0 +1,37 @@ +import { expect, test } from "@playwright/test"; + +test("loads Vazirmatn from the application origin", async ({ page }) => { + const fontRequests: string[] = []; + + page.on("request", (request) => { + if (request.resourceType() === "font") { + fontRequests.push(request.url()); + } + }); + + await page.goto("/auth"); + + const typography = await page.evaluate(async () => { + await document.fonts.load("400 16px Vazirmatn"); + await document.fonts.ready; + + const vazirmatnFace = [...document.fonts].find( + ({ family }) => family.replaceAll('"', "") === "Vazirmatn", + ); + + return { + family: getComputedStyle(document.documentElement).fontFamily, + fontStatus: vazirmatnFace?.status, + }; + }); + + expect(typography.family).toBe("Vazirmatn, system-ui, sans-serif"); + expect(typography.fontStatus).toBe("loaded"); + expect(fontRequests).toHaveLength(1); + + const applicationOrigin = new URL(page.url()).origin; + const fontUrl = new URL(fontRequests[0]); + + expect(fontUrl.origin).toBe(applicationOrigin); + expect(fontUrl.pathname).toMatch(/Vazirmatn-Variable-.*\.woff2$/); +}); diff --git a/e2e/localization.spec.ts b/e2e/localization.spec.ts new file mode 100644 index 0000000..e4d35a7 --- /dev/null +++ b/e2e/localization.spec.ts @@ -0,0 +1,85 @@ +import { expect, test } from "@playwright/test"; + +import { syntheticCredential } from "./fixtures"; + +const locales = [ + { + direction: "ltr", + enter: "Enter", + identifier: "Username, email, or mobile", + language: "en", + login: "Login", + password: "Password", + }, + { + direction: "rtl", + enter: "ورود", + identifier: "نام کاربری، ایمیل یا موبایل", + language: "fa", + login: "ورود به سیستم", + password: "رمز عبور", + }, +] as const; + +test.describe("localized critical journeys", () => { + for (const locale of locales) { + test(`logs in using ${locale.language.toUpperCase()}/${locale.direction.toUpperCase()}`, async ({ + page, + }) => { + await page.addInitScript((language) => { + localStorage.setItem("language", language); + localStorage.setItem("theme", "light"); + }, locale.language); + await page.route("https://api.example.com/auth/login", async (route) => { + expect(route.request().postDataJSON()).toEqual({ + identifier: "ada", + password: syntheticCredential, + }); + await route.fulfill({ + contentType: "application/json", + json: { access_token: "access-token" }, + }); + }); + await page.route("https://api.example.com/account/me", async (route) => { + await route.fulfill({ + contentType: "application/json", + json: { + email: "ada@example.com", + first_name: "Ada", + id: "user-1", + is_system_admin: false, + last_name: "Lovelace", + mobile: "09120000000", + permissions: [], + personnel_code: "100", + status: "active", + username: "ada", + }, + }); + }); + await page.goto("/auth"); + + await expect(page.locator("html")).toHaveAttribute( + "dir", + locale.direction, + ); + await expect(page.locator("html")).toHaveAttribute( + "lang", + locale.language, + ); + await expect( + page.getByRole("heading", { name: locale.login }), + ).toBeVisible(); + await page.getByLabel(locale.identifier).fill("ada"); + await page.getByLabel(locale.password).fill(syntheticCredential); + await page.getByRole("button", { name: locale.enter }).click(); + + await expect(page).toHaveURL(/\/$/); + await expect(page.getByRole("link", { name: "kernel" })).toBeVisible(); + await expect(page.locator("html")).toHaveAttribute( + "dir", + locale.direction, + ); + }); + } +}); diff --git a/e2e/management.spec.ts b/e2e/management.spec.ts new file mode 100644 index 0000000..b991ca7 --- /dev/null +++ b/e2e/management.spec.ts @@ -0,0 +1,337 @@ +import { expect, Page, test } from "@playwright/test"; + +import { syntheticCredential } from "./fixtures"; + +type RoleRecord = { + id: string; + name: string; + permissions: string[]; +}; + +type UserRecord = { + email: string | null; + first_name: string; + id: string; + is_system_admin: boolean; + last_name: string; + mobile: string; + personnel_code: string; + status: "active" | "inactive"; + username: string | null; +}; + +const mockAccount = async ( + page: Page, + permissions: string[], + isSystemAdmin = false, +) => { + await page.route("https://api.example.com/account/me", async (route) => { + await route.fulfill({ + contentType: "application/json", + json: { + email: "manager@example.com", + first_name: "Test", + id: "manager-1", + is_system_admin: isSystemAdmin, + last_name: "Manager", + mobile: "09120000000", + permissions, + personnel_code: "100", + status: "active", + username: "manager", + }, + }); + }); +}; + +test.describe("role and user management", () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + localStorage.setItem("language", "en"); + }); + }); + + test("creates a role after validation and assigns permissions", async ({ + page, + }) => { + const roles: RoleRecord[] = []; + let createRequestCount = 0; + + await mockAccount(page, ["roles.create", "roles.read"]); + await page.route("https://api.example.com/permissions", async (route) => { + await route.fulfill({ + contentType: "application/json", + json: [ + { + name: "users", + permissions: [ + { name: "users.read", title: "Read users" }, + { name: "users.update", title: "Update users" }, + ], + title: "Users", + }, + ], + }); + }); + await page.route("https://api.example.com/roles", async (route) => { + if (route.request().method() === "POST") { + createRequestCount += 1; + expect(route.request().postDataJSON()).toEqual({ + name: "Operators", + permissions: ["users.read"], + }); + roles.push({ + id: "role-1", + name: "Operators", + permissions: ["users.read"], + }); + await route.fulfill({ + contentType: "application/json", + json: roles[0], + }); + return; + } + + await route.fulfill({ contentType: "application/json", json: roles }); + }); + await page.goto("/roles"); + await expect(page.getByRole("link", { name: "kernel" })).toBeVisible(); + + await page.getByRole("button", { name: "Create" }).click(); + await page.getByRole("button", { name: "Submit" }).click(); + await expect(page.getByText("Please enter Name")).toBeVisible(); + expect(createRequestCount).toBe(0); + + await page.getByLabel("Name").fill("Operators"); + await page.getByLabel("Read users").click(); + await expect(page.getByLabel("Read users")).toBeChecked(); + await page.getByRole("button", { name: "Submit" }).click(); + + await expect(page.getByText("Role created successfully.")).toBeVisible(); + await expect(page.getByText("Operators")).toBeVisible(); + expect(createRequestCount).toBe(1); + }); + + test("updates and deletes a role after confirmation", async ({ page }) => { + const roles: RoleRecord[] = [ + { + id: "role-1", + name: "Operators", + permissions: ["users.read"], + }, + ]; + + await mockAccount(page, ["roles.delete", "roles.read", "roles.update"]); + await page.route("https://api.example.com/permissions", async (route) => { + await route.fulfill({ contentType: "application/json", json: [] }); + }); + await page.route("https://api.example.com/roles", async (route) => { + await route.fulfill({ contentType: "application/json", json: roles }); + }); + await page.route("https://api.example.com/roles/role-1", async (route) => { + if (route.request().method() === "PATCH") { + expect(route.request().postDataJSON()).toEqual({ + name: "Support", + permissions: ["users.read"], + }); + roles[0] = { ...roles[0], name: "Support" }; + await route.fulfill({ + contentType: "application/json", + json: roles[0], + }); + return; + } + + if (route.request().method() === "DELETE") { + roles.splice(0, roles.length); + await route.fulfill({ status: 204 }); + return; + } + + await route.fulfill({ + contentType: "application/json", + json: roles[0], + }); + }); + await page.goto("/roles"); + const roleRow = page.getByRole("row", { name: /Operators/ }); + + await roleRow.getByRole("button", { name: "Update" }).click(); + await page.getByLabel("Name").fill("Support"); + await page.getByRole("button", { name: "Submit" }).click(); + await expect(page.getByText("Role updated successfully.")).toBeVisible(); + await expect(page.getByText("Support")).toBeVisible(); + + await page + .getByRole("row", { name: /Support/ }) + .getByRole("button", { name: "Delete" }) + .click(); + await expect( + page.getByRole("dialog", { + name: "Are you sure you want to delete this role?", + }), + ).toBeVisible(); + await page.getByRole("button", { name: "Yes" }).click(); + + await expect(page.getByText("Role deleted successfully.")).toBeVisible(); + await expect(page.getByText("Support")).not.toBeVisible(); + }); + + test("creates and updates a user, then displays a delete error", async ({ + page, + }) => { + const users: UserRecord[] = []; + + await mockAccount(page, [ + "users.create", + "users.delete", + "users.read", + "users.update", + ]); + await page.route("https://api.example.com/roles", async (route) => { + await route.fulfill({ contentType: "application/json", json: [] }); + }); + await page.route("https://api.example.com/workspaces", async (route) => { + await route.fulfill({ contentType: "application/json", json: [] }); + }); + await page.route( + /https:\/\/api\.example\.com\/users(?:\?.*)?$/, + async (route) => { + if (route.request().method() === "POST") { + expect(route.request().postDataJSON()).toEqual({ + first_name: "Ada", + last_name: "Lovelace", + mobile: "09121111111", + password: syntheticCredential, + personnel_code: "200", + }); + users.push({ + email: null, + first_name: "Ada", + id: "user-1", + is_system_admin: false, + last_name: "Lovelace", + mobile: "09121111111", + personnel_code: "200", + status: "active", + username: null, + }); + await route.fulfill({ + contentType: "application/json", + json: { ...users[0], role_ids: [], workspace_ids: [] }, + }); + return; + } + + await route.fulfill({ + contentType: "application/json", + json: { items: users, total: users.length }, + }); + }, + ); + await page.route("https://api.example.com/users/user-1", async (route) => { + if (route.request().method() === "PATCH") { + expect(route.request().postDataJSON()).toEqual({ + first_name: "Augusta", + last_name: "Lovelace", + mobile: "09121111111", + personnel_code: "200", + }); + users[0] = { ...users[0], first_name: "Augusta" }; + await route.fulfill({ + contentType: "application/json", + json: { ...users[0], role_ids: [], workspace_ids: [] }, + }); + return; + } + + if (route.request().method() === "DELETE") { + await route.fulfill({ + contentType: "application/json", + json: { message: "User cannot be deleted" }, + status: 409, + }); + return; + } + + await route.fulfill({ + contentType: "application/json", + json: { ...users[0], role_ids: [], workspace_ids: [] }, + }); + }); + await page.goto("/users"); + + await page.getByRole("button", { name: "Create" }).click(); + await page.getByRole("button", { name: "Submit" }).click(); + await expect(page.getByText("Please enter First name")).toBeVisible(); + const createDialog = page.getByRole("dialog", { name: "Create" }); + await createDialog.getByLabel("First name").fill("Ada"); + await createDialog.getByLabel("Last name").fill("Lovelace"); + await createDialog.getByRole("textbox").nth(2).fill("09121111111"); + await createDialog.getByLabel("Personnel code").fill("200"); + await createDialog + .getByLabel("Password", { exact: true }) + .fill(syntheticCredential); + await createDialog.getByLabel("Confirm password").fill(syntheticCredential); + await createDialog.getByRole("button", { name: "Submit" }).click(); + await expect(page.getByText("User created successfully.")).toBeVisible(); + await expect(page.getByText("Ada Lovelace")).toBeVisible(); + + await page + .getByRole("row", { name: /Ada Lovelace/ }) + .getByRole("button", { name: "Update" }) + .click(); + const updateDialog = page.getByRole("dialog", { name: "Update" }); + await updateDialog.getByLabel("First name").fill("Augusta"); + await updateDialog.getByRole("button", { name: "Submit" }).click(); + await expect(page.getByText("User updated successfully.")).toBeVisible(); + await expect(page.getByText("Augusta Lovelace")).toBeVisible(); + + await page + .getByRole("row", { name: /Augusta Lovelace/ }) + .getByRole("button", { name: "Delete" }) + .click(); + await expect( + page.getByRole("dialog", { + name: "Are you sure you want to delete this user?", + }), + ).toBeVisible(); + await page.getByRole("button", { name: "Yes" }).click(); + await expect(page.getByText("User cannot be deleted")).toBeVisible(); + }); + + test("hides mutation controls from a read-only user", async ({ page }) => { + await mockAccount(page, ["users.read"]); + await page.route( + /https:\/\/api\.example\.com\/users(?:\?.*)?$/, + async (route) => { + await route.fulfill({ + contentType: "application/json", + json: { + items: [ + { + email: "ada@example.com", + first_name: "Ada", + id: "user-1", + is_system_admin: false, + last_name: "Lovelace", + mobile: "09121111111", + personnel_code: "200", + status: "active", + username: "ada", + }, + ], + total: 1, + }, + }); + }, + ); + await page.goto("/users"); + await expect(page.getByText("Ada Lovelace")).toBeVisible(); + + await expect(page.getByRole("button", { name: "Create" })).toHaveCount(0); + await expect(page.getByRole("button", { name: "Update" })).toHaveCount(0); + await expect(page.getByRole("button", { name: "Delete" })).toHaveCount(0); + await expect(page.getByRole("button", { name: "Active" })).toHaveCount(0); + }); +}); diff --git a/e2e/security-headers.spec.ts b/e2e/security-headers.spec.ts new file mode 100644 index 0000000..4088723 --- /dev/null +++ b/e2e/security-headers.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from "@playwright/test"; + +test.describe("production security headers", () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + localStorage.setItem("language", "en"); + }); + }); + + test("protects the built application response", async ({ page }) => { + const response = await page.goto("/auth"); + + expect(response).not.toBeNull(); + await expect(page.getByRole("heading", { name: "Login" })).toBeVisible(); + + const headers = response!.headers(); + const contentSecurityPolicy = headers["content-security-policy"]; + + expect(contentSecurityPolicy).toContain("default-src 'self'"); + expect(contentSecurityPolicy).toContain("object-src 'none'"); + expect(contentSecurityPolicy).toContain("frame-ancestors 'none'"); + expect(contentSecurityPolicy).toContain("upgrade-insecure-requests"); + expect(headers["strict-transport-security"]).toBe( + "max-age=31536000; includeSubDomains; preload", + ); + expect(headers["x-frame-options"]).toBe("DENY"); + expect(headers["x-content-type-options"]).toBe("nosniff"); + expect(headers["referrer-policy"]).toBe("strict-origin-when-cross-origin"); + expect(headers["permissions-policy"]).toBe( + "camera=(), geolocation=(), microphone=(), payment=(), usb=()", + ); + }); +}); diff --git a/e2e/visual.spec.ts b/e2e/visual.spec.ts new file mode 100644 index 0000000..4b6a5cf --- /dev/null +++ b/e2e/visual.spec.ts @@ -0,0 +1,122 @@ +import { expect, Page, test } from "@playwright/test"; + +const account = { + email: "ada@example.com", + first_name: "Ada", + id: "user-1", + is_system_admin: true, + last_name: "Lovelace", + mobile: "09120000000", + permissions: [], + personnel_code: "100", + status: "active", + username: "ada", +}; + +const preparePage = async (page: Page) => { + await page.addInitScript(() => { + localStorage.setItem("language", "en"); + localStorage.setItem("theme", "light"); + }); +}; + +const mockAuthenticatedPage = async (page: Page) => { + await page.route("https://api.example.com/account/me", async (route) => { + await route.fulfill({ + contentType: "application/json", + json: account, + }); + }); +}; + +test.describe("visual regression", () => { + test.beforeEach(async ({ page }) => { + await preparePage(page); + }); + + test("login page", async ({ page }) => { + await page.goto("/auth"); + await expect(page.getByRole("heading", { name: "Login" })).toBeVisible(); + await expect(page).toHaveScreenshot("login.png", { fullPage: true }); + }); + + test("forgot password page", async ({ page }) => { + await page.goto("/auth/forgot-password"); + await expect( + page.getByRole("heading", { name: "Forgot password" }), + ).toBeVisible(); + await expect(page).toHaveScreenshot("forgot-password.png", { + fullPage: true, + }); + }); + + test("registration page", async ({ page }) => { + await page.goto("/auth/register"); + await expect(page.getByRole("heading", { name: "Register" })).toBeVisible(); + await expect(page).toHaveScreenshot("register.png", { fullPage: true }); + }); + + test("authenticated layout and users table", async ({ page }) => { + await mockAuthenticatedPage(page); + await page.route( + /https:\/\/api\.example\.com\/users(?:\?.*)?$/, + async (route) => { + await route.fulfill({ + contentType: "application/json", + json: { + items: [ + { + ...account, + is_system_admin: false, + }, + { + email: "grace@example.com", + first_name: "Grace", + id: "user-2", + is_system_admin: false, + last_name: "Hopper", + mobile: "09121111111", + personnel_code: "101", + status: "inactive", + username: "grace", + }, + ], + total: 2, + }, + }); + }, + ); + + await page.goto("/users"); + await expect(page.getByText("Ada Lovelace")).toBeVisible(); + await expect(page.getByText("Grace Hopper")).toBeVisible(); + await expect(page).toHaveScreenshot("users-table.png", { fullPage: true }); + }); + + test("create role form", async ({ page }) => { + await mockAuthenticatedPage(page); + await page.route("https://api.example.com/permissions", async (route) => { + await route.fulfill({ + contentType: "application/json", + json: [ + { + name: "users", + permissions: [ + { name: "users.read", title: "Read users" }, + { name: "users.update", title: "Update users" }, + ], + title: "Users", + }, + ], + }); + }); + await page.route("https://api.example.com/roles", async (route) => { + await route.fulfill({ contentType: "application/json", json: [] }); + }); + + await page.goto("/roles"); + await page.getByRole("button", { name: "Create" }).click(); + await expect(page.getByRole("dialog", { name: "Create" })).toBeVisible(); + await expect(page).toHaveScreenshot("create-role.png", { fullPage: true }); + }); +}); diff --git a/e2e/visual.spec.ts-snapshots/create-role-visual-chromium.png b/e2e/visual.spec.ts-snapshots/create-role-visual-chromium.png new file mode 100644 index 0000000..a74f066 Binary files /dev/null and b/e2e/visual.spec.ts-snapshots/create-role-visual-chromium.png differ diff --git a/e2e/visual.spec.ts-snapshots/forgot-password-visual-chromium.png b/e2e/visual.spec.ts-snapshots/forgot-password-visual-chromium.png new file mode 100644 index 0000000..290e6cd Binary files /dev/null and b/e2e/visual.spec.ts-snapshots/forgot-password-visual-chromium.png differ diff --git a/e2e/visual.spec.ts-snapshots/login-visual-chromium.png b/e2e/visual.spec.ts-snapshots/login-visual-chromium.png new file mode 100644 index 0000000..fd5efa2 Binary files /dev/null and b/e2e/visual.spec.ts-snapshots/login-visual-chromium.png differ diff --git a/e2e/visual.spec.ts-snapshots/register-visual-chromium.png b/e2e/visual.spec.ts-snapshots/register-visual-chromium.png new file mode 100644 index 0000000..8802312 Binary files /dev/null and b/e2e/visual.spec.ts-snapshots/register-visual-chromium.png differ diff --git a/e2e/visual.spec.ts-snapshots/users-table-visual-chromium.png b/e2e/visual.spec.ts-snapshots/users-table-visual-chromium.png new file mode 100644 index 0000000..3cf6d65 Binary files /dev/null and b/e2e/visual.spec.ts-snapshots/users-table-visual-chromium.png differ diff --git a/index.html b/index.html index f2c9f7a..150a382 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ kernel - +
diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..4be38be --- /dev/null +++ b/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; connect-src 'self' https:; font-src 'self' data:; form-action 'self'; frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; upgrade-insecure-requests" always; + add_header Permissions-Policy "camera=(), geolocation=(), microphone=(), payment=(), usb=()" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "DENY" always; + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(?:css|js|svg|woff2?)$ { + expires 1y; + try_files $uri =404; + } +} diff --git a/package-lock.json b/package-lock.json index dd42f7e..47ca970 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,53 +1,62 @@ { "name": "kernel", - "version": "0.0.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kernel", - "version": "0.0.0", + "version": "1.0.0", "dependencies": { - "antd": "^6.5.0", + "antd": "^6.5.2", "antd-style": "^4.1.0", - "axios": "^1.18.1", + "axios": "^1.19.0", "dayjs": "^1.11.21", - "i18next": "^26.3.3", + "i18next": "^26.3.6", "jalaliday": "^3.1.1", "lodash": "^4.18.1", - "react": "^19.2.7", - "react-dom": "^19.2.7", - "react-i18next": "^17.0.8", - "react-router-dom": "^7.18.0" + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-i18next": "^17.0.11", + "react-router": "^8.3.0" }, "devDependencies": { + "@axe-core/playwright": "4.12.1", "@eslint/js": "^10.0.1", + "@pact-foundation/pact": "^17.0.1", "@playwright/test": "^1.62.0", + "@stryker-mutator/core": "^9.6.1", + "@stryker-mutator/vitest-runner": "^9.6.1", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/lodash": "^4.17.24", - "@types/node": "^26.0.1", + "@types/node": "^24.13.3", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "@typescript-eslint/types": "^8.62.0", + "@typescript-eslint/types": "^8.65.0", "@vitejs/plugin-basic-ssl": "^2.3.0", - "@vitejs/plugin-react": "^6.0.3", + "@vitejs/plugin-react": "^6.0.5", "@vitest/coverage-v8": "^4.1.10", - "eslint": "^10.6.0", + "chrome-launcher": "^1.2.1", + "eslint": "^10.8.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", - "eslint-plugin-simple-import-sort": "^13.0.0", - "globals": "^17.7.0", - "jsdom": "^29.1.1", - "knip": "^6.22.0", + "eslint-plugin-simple-import-sort": "^14.0.0", + "globals": "^17.8.0", + "jsdom": "^30.0.1", + "knip": "^6.29.0", + "lighthouse": "^13.4.1", "msw": "^2.15.0", "prettier": "^3.9.6", "typescript": "~6.0.3", - "typescript-eslint": "^8.62.0", - "vite": "^8.1.0", + "typescript-eslint": "^8.65.0", + "vite": "^8.2.0", "vitest": "^4.1.10" + }, + "engines": { + "node": ">=24.15.0 <25" } }, "node_modules/@adobe/css-tools": { @@ -150,56 +159,127 @@ "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@apm-js-collab/code-transformer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer/-/code-transformer-0.18.1.tgz", + "integrity": "sha512-u1Hb6bHjWtkSpiprwVP6YaHC1DTN4RAU3zYkUDUe7WMnJwdyU1pwTL9dFKiSJB9IiLue/EQovmyx6xhU7FFtAQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/estree": "^1.0.8", + "astring": "^1.9.0", + "esquery": "^1.7.0", + "meriyah": "^6.1.4", + "semifies": "^1.0.0", + "source-map": "^0.6.0" + }, + "bin": { + "code-transformer": "cli.js" + } + }, + "node_modules/@apm-js-collab/code-transformer-bundler-plugins": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer-bundler-plugins/-/code-transformer-bundler-plugins-0.7.3.tgz", + "integrity": "sha512-qNbPwuMZ8f5ZuGj/ttPeB7a6C/S1bB6tNYaEL5vNiRKydSAxa4AU0gxCWgaP4fVju+AuwhcumSFjrEcGF9Dv7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.18.0", + "es-module-lexer": "^2.1.0", + "magic-string": "^0.30.21", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@apm-js-collab/code-transformer/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@apm-js-collab/tracing-hooks": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.13.0.tgz", + "integrity": "sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.18.0", + "debug": "^4.4.1", + "module-details-from-path": "^1.0.4" + } + }, "node_modules/@asamuzakjp/css-color": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.5.tgz", + "integrity": "sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@csstools/css-calc": "^3.2.0", - "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-calc": "^3.2.1", + "@csstools/css-color-parser": "^4.1.9", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", - "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.0.tgz", + "integrity": "sha512-UJLfKXBhrc8i1vH2eJXuYQMwlsLKWFw3O+CPqXSuVEiikeAim3UgrfWX0k4tA/X8cRFM8iZ7OaqBokFGbYusdg==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1" + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.13.0 || >=24.0.0" } }, - "node_modules/@asamuzakjp/generational-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "20 || >=22" } }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "node_modules/@axe-core/playwright": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.12.1.tgz", + "integrity": "sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==", "dev": true, - "license": "MIT" + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.12.1" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } }, "node_modules/@babel/code-frame": { "version": "7.29.7", @@ -272,6 +352,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", @@ -289,6 +382,28 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", @@ -298,6 +413,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", @@ -329,6 +458,61 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -386,6 +570,163 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.7.tgz", + "integrity": "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-decorators": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz", + "integrity": "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", @@ -604,9 +945,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", "dev": true, "license": "MIT", "optional": true, @@ -616,9 +957,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", "dev": true, "license": "MIT", "optional": true, @@ -858,9 +1199,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", - "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -946,29 +1287,85 @@ } } }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.2", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" } }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-skeleton-parser": "1.8.16", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" @@ -1022,6 +1419,30 @@ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", + "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@inquirer/confirm": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.1.tgz", @@ -1071,6 +1492,73 @@ } } }, + "node_modules/@inquirer/editor": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", + "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/external-editor": "^3.0.3", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", + "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@inquirer/figures": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", @@ -1081,6 +1569,172 @@ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, + "node_modules/@inquirer/input": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", + "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", + "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", + "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.5.2.tgz", + "integrity": "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.1", + "@inquirer/confirm": "^6.1.1", + "@inquirer/editor": "^5.2.2", + "@inquirer/expand": "^5.1.1", + "@inquirer/input": "^5.1.2", + "@inquirer/number": "^4.1.1", + "@inquirer/password": "^5.1.1", + "@inquirer/rawlist": "^5.3.1", + "@inquirer/search": "^4.2.1", + "@inquirer/select": "^5.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", + "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", + "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", + "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@inquirer/type": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", @@ -1171,22 +1825,25 @@ "license": "MIT" }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.1.tgz", + "integrity": "sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "@tybys/wasm-util": "^0.10.3" }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" } }, "node_modules/@open-draft/deferred-promise": { @@ -1214,27 +1871,148 @@ "dev": true, "license": "MIT" }, - "node_modules/@oxc-parser/binding-android-arm-eabi": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.137.0.tgz", - "integrity": "sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA==", - "cpu": [ - "arm" - ], + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz", + "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz", + "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.140.0.tgz", + "integrity": "sha512-ZfjDZ422mo7eo3b3VltqNsV9kmv1qt/sPEAMSl64iOSwhVfd0eIZ9LB79Mbs1xYXJnk7WSROwzBCKDIiVxPTvQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@oxc-parser/binding-android-arm64": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.137.0.tgz", - "integrity": "sha512-WhALNzfy3x/RfC6bsqX+csavuUY0yHHE7XfgPE5M542uhoBZUUoGTPG+nkMbGoG4+gcfss5s7urMyn5QBHu0sw==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.140.0.tgz", + "integrity": "sha512-Ia8jSvikUX6Sf+Ht+KOCUF/k1HpR0VlmqIYymubmWDebOEGtsyliHDR6JxsZ4IX3/c/GbrB1uh09aVGQv/LQmQ==", "cpu": [ "arm64" ], @@ -1249,9 +2027,9 @@ } }, "node_modules/@oxc-parser/binding-darwin-arm64": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.137.0.tgz", - "integrity": "sha512-bFPr5hgmNMOMoyPTGtdsK4Ug21RovIPojRMgDDhSp1LtCnc/DkLwGONKjgRjszg677RlGnkYSviQ8hHaUPOVYA==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.140.0.tgz", + "integrity": "sha512-G6VK0nK61pH0d0mBjUqSZbVxGqqO5uzeginLDQj+gOO6ObfJjXRwgkD/ol0w1INcnFeAb6YGGO7qc3ueGHaycQ==", "cpu": [ "arm64" ], @@ -1266,9 +2044,9 @@ } }, "node_modules/@oxc-parser/binding-darwin-x64": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.137.0.tgz", - "integrity": "sha512-CL5dMm1asqXIDZHg14FLxj3Mc36w8PI7xCWh1uA4is6z8g2XrIILoTcQYOxDbwzuk34RDPX5IAGUxZr6LA9KAg==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.140.0.tgz", + "integrity": "sha512-HazBOuZzd2pO1C2uMmp8Gv7mhzMHqKSKDS1OZfcLEvpIcgA+48J92HEtNanVHDIzRD9PRPCV6aS6fkZIWOVl8Q==", "cpu": [ "x64" ], @@ -1283,9 +2061,9 @@ } }, "node_modules/@oxc-parser/binding-freebsd-x64": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.137.0.tgz", - "integrity": "sha512-79h8rYGnSlKPGWo7mHr2ixO6ea7aW8B0CT965SZ8SLbNnCOH5aOYBTeVXUY6eMvEaiLyWr8Skuiugr5pDYgLGw==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.140.0.tgz", + "integrity": "sha512-9hSUU+HmTUyOe4JzMHxNGgLWNY7rrO+6ShicZwImNJacEAACDMIkuEQQkvXSL+WJN50jaNtLYJv8s4OcBdpyUQ==", "cpu": [ "x64" ], @@ -1300,9 +2078,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.137.0.tgz", - "integrity": "sha512-ASgmlSimhGyr0lksgVIo6hibz1obnDq4qJbiMX/AzltfgPnanRrzG1Q+23g8ljOHOjv6dsznkUuCYL3gg0sY1Q==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.140.0.tgz", + "integrity": "sha512-RAEuQsYtS0KcDFqN0ABTjyyNlokS91JeuDuoW9tEbG0JTbRNXnpQUdbYc/16JoA6Z/2ALbNrE3KmxtqDiuIjCQ==", "cpu": [ "arm" ], @@ -1317,9 +2095,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.137.0.tgz", - "integrity": "sha512-AU2J9aa22Sx32wRGnDjybOU9TQXXQUud5sdUi+ZB0XxwM8aToWLweV+yA0wlQm0yIUVqljquqoHCYEq9II8gJQ==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.140.0.tgz", + "integrity": "sha512-c4CkHvPvqfojouredJ0w3e6+jiBq0SbFyhH61kr/zPb/7XsaYTNKQ54vmlSsopfdQbNDX40ZeK9Abs2Qet6wcw==", "cpu": [ "arm" ], @@ -1334,9 +2112,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm64-gnu": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.137.0.tgz", - "integrity": "sha512-GdEtiG89yMr7XkUGxifgodXEEm2f+xW2f9CpDjlgAnBOwhTmrpQMvhOGobLVKUyzf/qHBXW16smk5zbF3nZU6w==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.140.0.tgz", + "integrity": "sha512-yrjmLj8ixPB25yqvPGr28meGjb+keed7m1GqqY/0uqkhZIoT4t9zmfwUgFEtC33C7dtE+UQ7TU0IaVxf97SWJg==", "cpu": [ "arm64" ], @@ -1351,9 +2129,9 @@ } }, "node_modules/@oxc-parser/binding-linux-arm64-musl": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.137.0.tgz", - "integrity": "sha512-EGJ+Bs8iXx8KBH8DQ5BLoEm5lnHaYjlh4/8j8vFhrr/6z4tqONy5BZDzLpKmmNWlN6Hlc5r8YOuBVHqZ9vRFEQ==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.140.0.tgz", + "integrity": "sha512-ggGMQTN8Agwxp2WiLMpdY671dt0qTDJWiWlJeig3HnUwTnerRl0J2JdGVghWBeDcss2D9S2V2Js6dZHEiVabVA==", "cpu": [ "arm64" ], @@ -1368,9 +2146,9 @@ } }, "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.137.0.tgz", - "integrity": "sha512-vzFUQENy/fnbSe5DZWovq6tIBc1uhuMztanSW6rz1e9WdQE4gHwYuD7ZII6JnrJifd1R3RSoqiZbgRFlVL2tYQ==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.140.0.tgz", + "integrity": "sha512-IgTs8xYAFgAUGNmR65tIqjlJ8vKgrfXzC515e9goSdfMyKQV4aJpd2pUUudU4u51G64H0/DSEJEXKOraxm9ZCA==", "cpu": [ "ppc64" ], @@ -1385,9 +2163,9 @@ } }, "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.137.0.tgz", - "integrity": "sha512-SfVI14HBQs9gtLcUD5hTt5hsNbdrqSUNg9S8muN+LhVQ5nf1WwH3hAoK6B9NKgdYgWAQSXFXGiiBedQ4r/BKuw==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.140.0.tgz", + "integrity": "sha512-A1x+PMWZmSGaFVOx2YeNTFau8uD+QO14/vLP4GrcuvUPs3+nBkUOjy9Lus86ftHsDojjYMbvBelmKc3F7Rv08g==", "cpu": [ "riscv64" ], @@ -1402,9 +2180,9 @@ } }, "node_modules/@oxc-parser/binding-linux-riscv64-musl": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.137.0.tgz", - "integrity": "sha512-e7Ppy4FCIFNQxT/ikSeIWFoQ0l+N9vgtRBtLcyZXeolTzApyVoPqEXsYPrcdM/9i0Bwk8knvYd37vaEMxHyi6g==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.140.0.tgz", + "integrity": "sha512-zBqpfRo2myWPrPo5xUjeZqlnPXPXsX8BcWtWff66/eGRQdbPjhzPgXa/F+AtxT2afUViPxbuDlwscMKzQ5tg+g==", "cpu": [ "riscv64" ], @@ -1419,9 +2197,9 @@ } }, "node_modules/@oxc-parser/binding-linux-s390x-gnu": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.137.0.tgz", - "integrity": "sha512-Bho5qFwdhqsIFR7gipYEUlqvi3SRrY8sugxXig380MIaakBB1PyU9+7dBiBVScfImTNWhijUxdBwqrprGdq5WA==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.140.0.tgz", + "integrity": "sha512-2M1DPm/8w9I//YzFlFC9qXw+r2tJFh5CYwRlYTq2vUJQS7qoQftEDeCZ8EnN7KHtvSiXvYj8mZI5pR7DpXmcEw==", "cpu": [ "s390x" ], @@ -1436,9 +2214,9 @@ } }, "node_modules/@oxc-parser/binding-linux-x64-gnu": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.137.0.tgz", - "integrity": "sha512-36mGWtg7PyFzjJwGDkH6/F4o2nIDEoKXLPr/X/lwqklkomQwJJt1I5GJVmGhovUEmgPK5WAeAZMqlFCehwiy9Q==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.140.0.tgz", + "integrity": "sha512-8aRDbZ/U/jO8N7go1MO72jtbpb4uswV8d7vOkMvt/BPgZiyEYvl1VIWK4ESxZZhnJ4tqwVldgX7dNiP/eB1Jdg==", "cpu": [ "x64" ], @@ -1453,9 +2231,9 @@ } }, "node_modules/@oxc-parser/binding-linux-x64-musl": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.137.0.tgz", - "integrity": "sha512-/Jqx6+N7A44n2BdvUr7pXhVr2vFjs6WGH3unZRczwrfiH0H1zY0QwKQMG/dtRiTlKGDKGukznPT8lx84/oEsZg==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.140.0.tgz", + "integrity": "sha512-xRqpeI8U2sQQS1W5BMWRyMTxtagkuLG2dEWruet5lFsWHTvBth11/TpSaJatHdqVVwHN0q3uuoS9zRsGinq8hg==", "cpu": [ "x64" ], @@ -1470,9 +2248,9 @@ } }, "node_modules/@oxc-parser/binding-openharmony-arm64": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.137.0.tgz", - "integrity": "sha512-9Uj0qHNNl+OgT1UTGwF7ixIXU6T1u2SbMidmgPy/h1h/fl2gRS6YpAxxY1gwHofcWjoTwkoMFd8xs5Vuj6GOFA==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.140.0.tgz", + "integrity": "sha512-GbGRe26MqAKciFRvXeHNQJ6VAHYs9R4miP89sEAncysM3n+f4lnyLWgsa9kklJNpfnxdq2yRoNYHFqwBckVimw==", "cpu": [ "arm64" ], @@ -1487,9 +2265,9 @@ } }, "node_modules/@oxc-parser/binding-wasm32-wasi": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.137.0.tgz", - "integrity": "sha512-gW2vfkytNGgMVADiuzdvOfw0mWG9za20F/1fCJsif5aBMAvWJTSbpIXbIe0XkOe0VENk+PadpQ7cZgUy2sUJcA==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.140.0.tgz", + "integrity": "sha512-vFiC1hqys+hkX1GnQkIoiTQJNiUm43Z0lO35ETKXTw0YtpW7+cN58YRRXFAQQ+TgpkIi3lrhcxdlnqz+Oi3ptQ==", "cpu": [ "wasm32" ], @@ -1497,18 +2275,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.5" + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@oxc-parser/binding-win32-arm64-msvc": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.137.0.tgz", - "integrity": "sha512-x+pFANF0yL5uK/6T7lu6SlR5qid6sp//eZXKLq5iNsIE+EQg6EaS8/wsW7E91nXXjpnPhSoMOHXShSVhGRdn8w==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.140.0.tgz", + "integrity": "sha512-fGSQldwEYKhM+H8uLt76Op8hh5+FYaR6lvvQ1Txw3Mhn86DyQXLcI0fi1EkFlTK7F+46OCk/j0AJMzZQm6g5Xg==", "cpu": [ "arm64" ], @@ -1523,9 +2301,9 @@ } }, "node_modules/@oxc-parser/binding-win32-ia32-msvc": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.137.0.tgz", - "integrity": "sha512-sQUqym80PFi6McRsIqfJrSu2JrSClEZIXXD+/FjAFoULEKzOPsldIdFBG96xdX8aVMzCNQ9792FPx3MfkEIrFA==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.140.0.tgz", + "integrity": "sha512-sDS2Bai+g3ZWYwfZqmosiSuFDBcVnZ3Ta6pszzsiJoLMqsJEWKcxXXbGa7b7yXr++W2lQNPb3ZRJ8czseqL7RA==", "cpu": [ "ia32" ], @@ -1540,9 +2318,9 @@ } }, "node_modules/@oxc-parser/binding-win32-x64-msvc": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.137.0.tgz", - "integrity": "sha512-2AsevxlvNN4WKxpEn3RtqD5zbqMaXF+T7JXblsP4gVuY+vC9dXS4ED/PwfRCliFqoeisYS3Iro4DHzxr0TEvVA==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.140.0.tgz", + "integrity": "sha512-kHbE1zWyb5OQgJA6/5P4WjiuB01sYdQwtZnSSyE58FQEXDAMnyeeq4vj7KgN75i5SlBzOs8A5MrtlD3gOlDKqQ==", "cpu": [ "x64" ], @@ -1557,9 +2335,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", - "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.140.0.tgz", + "integrity": "sha512-h5LUOzGArYemnW1NMz/DuuQhBi96J6JL2Bk8zE4kvqxB5Sg3jxmCiH4uyOWHDkiKSt5vWlG4FIwCR/DbstcNRQ==", "dev": true, "license": "MIT", "funding": { @@ -1567,9 +2345,9 @@ } }, "node_modules/@oxc-resolver/binding-android-arm-eabi": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.3.tgz", - "integrity": "sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.24.2.tgz", + "integrity": "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==", "cpu": [ "arm" ], @@ -1581,9 +2359,9 @@ ] }, "node_modules/@oxc-resolver/binding-android-arm64": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.3.tgz", - "integrity": "sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.24.2.tgz", + "integrity": "sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==", "cpu": [ "arm64" ], @@ -1595,9 +2373,9 @@ ] }, "node_modules/@oxc-resolver/binding-darwin-arm64": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.3.tgz", - "integrity": "sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.24.2.tgz", + "integrity": "sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==", "cpu": [ "arm64" ], @@ -1609,9 +2387,9 @@ ] }, "node_modules/@oxc-resolver/binding-darwin-x64": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.3.tgz", - "integrity": "sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.24.2.tgz", + "integrity": "sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==", "cpu": [ "x64" ], @@ -1623,9 +2401,9 @@ ] }, "node_modules/@oxc-resolver/binding-freebsd-x64": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.3.tgz", - "integrity": "sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.24.2.tgz", + "integrity": "sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==", "cpu": [ "x64" ], @@ -1637,9 +2415,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.3.tgz", - "integrity": "sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.24.2.tgz", + "integrity": "sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==", "cpu": [ "arm" ], @@ -1651,9 +2429,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.3.tgz", - "integrity": "sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.24.2.tgz", + "integrity": "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==", "cpu": [ "arm" ], @@ -1665,9 +2443,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.3.tgz", - "integrity": "sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.24.2.tgz", + "integrity": "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==", "cpu": [ "arm64" ], @@ -1679,9 +2457,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm64-musl": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.3.tgz", - "integrity": "sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.24.2.tgz", + "integrity": "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==", "cpu": [ "arm64" ], @@ -1693,9 +2471,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.3.tgz", - "integrity": "sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.24.2.tgz", + "integrity": "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==", "cpu": [ "ppc64" ], @@ -1707,9 +2485,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.3.tgz", - "integrity": "sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.24.2.tgz", + "integrity": "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==", "cpu": [ "riscv64" ], @@ -1721,9 +2499,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.3.tgz", - "integrity": "sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.24.2.tgz", + "integrity": "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==", "cpu": [ "riscv64" ], @@ -1735,9 +2513,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.3.tgz", - "integrity": "sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.24.2.tgz", + "integrity": "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==", "cpu": [ "s390x" ], @@ -1749,9 +2527,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-x64-gnu": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.3.tgz", - "integrity": "sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.24.2.tgz", + "integrity": "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==", "cpu": [ "x64" ], @@ -1763,9 +2541,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-x64-musl": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.3.tgz", - "integrity": "sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.24.2.tgz", + "integrity": "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==", "cpu": [ "x64" ], @@ -1777,9 +2555,9 @@ ] }, "node_modules/@oxc-resolver/binding-openharmony-arm64": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.3.tgz", - "integrity": "sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.24.2.tgz", + "integrity": "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==", "cpu": [ "arm64" ], @@ -1791,9 +2569,9 @@ ] }, "node_modules/@oxc-resolver/binding-wasm32-wasi": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.3.tgz", - "integrity": "sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.24.2.tgz", + "integrity": "sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==", "cpu": [ "wasm32" ], @@ -1801,55 +2579,195 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.11.0", - "@emnapi/runtime": "1.11.0", - "@napi-rs/wasm-runtime": "^1.1.5" + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", - "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.24.2.tgz", + "integrity": "sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } + "os": [ + "win32" + ] }, - "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", - "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.24.2.tgz", + "integrity": "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@pact-foundation/pact": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact/-/pact-17.0.1.tgz", + "integrity": "sha512-Gnzy9fMK0Wu6UB8u0fYcso5fiEZk+Tu2lG9zR5DoJiwWBanb/9/pOmRrBWtOHn5EdgvEvzIF1huB39sZIc27Rw==", + "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^2.4.0" + "@pact-foundation/pact-core": "^19.2.0", + "@scarf/scarf": "^1.4.0", + "axios": "^1.12.2", + "body-parser": "^2.2.0", + "chalk": "4.1.2", + "express": "^5.1.0", + "graphql": "^16.11.0", + "graphql-tag": "^2.12.6", + "http-proxy": "^1.18.1", + "https-proxy-agent": "^7.0.6", + "js-base64": "^3.7.8", + "lodash": "^4.17.21", + "ramda": "^0.32.0", + "randexp": "^0.5.3", + "router": "^2.2.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": ">=22" } }, - "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.3.tgz", - "integrity": "sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==", + "node_modules/@pact-foundation/pact-core": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core/-/pact-core-19.2.0.tgz", + "integrity": "sha512-7EB2e850hmBzGnwOYTRj4TCFCJQa9zaOy53bK+ybzEDx801olf0gVlYqMYHt1EsHUZzmtS5uDybpr9UMcZQxgg==", "cpu": [ + "x64", + "ia32", "arm64" ], "dev": true, "license": "MIT", - "optional": true, "os": [ + "darwin", + "linux", "win32" + ], + "dependencies": { + "check-types": "11.2.3", + "detect-libc": "^2.0.3", + "node-gyp-build": "^4.6.0", + "pino": "^10.0.0", + "pino-pretty": "^13.1.1", + "underscore": "1.13.8" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@pact-foundation/pact-core-darwin-arm64": "19.2.0", + "@pact-foundation/pact-core-darwin-x64": "19.2.0", + "@pact-foundation/pact-core-linux-arm64-glibc": "19.2.0", + "@pact-foundation/pact-core-linux-arm64-musl": "19.2.0", + "@pact-foundation/pact-core-linux-x64-glibc": "19.2.0", + "@pact-foundation/pact-core-linux-x64-musl": "19.2.0", + "@pact-foundation/pact-core-windows-x64": "19.2.0" + } + }, + "node_modules/@pact-foundation/pact-core-darwin-arm64": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-darwin-arm64/-/pact-core-darwin-arm64-19.2.0.tgz", + "integrity": "sha512-cQvvWfJKS7iMzkunzh/kdgmyVLhAxyr/BQ8fOnMco2M/1+GHR+sOXvhrR1tes+NAYd1xjE3fbg1K2Flno8aDBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" ] }, - "node_modules/@oxc-resolver/binding-win32-x64-msvc": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.3.tgz", - "integrity": "sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==", + "node_modules/@pact-foundation/pact-core-darwin-x64": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-darwin-x64/-/pact-core-darwin-x64-19.2.0.tgz", + "integrity": "sha512-UltPXDZ+h1r3JqgIpEBP16bQzB90otd1QbcoeM3XakF9khCrYhW4y9llSuaosyqPFYwCk+mLaZTl/4iitJJaow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pact-foundation/pact-core-linux-arm64-glibc": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-arm64-glibc/-/pact-core-linux-arm64-glibc-19.2.0.tgz", + "integrity": "sha512-amiv4COurH1FRa7DSH3ks4UPQCb5J3x4GKrArf8UsTOkNhVGFUWVl83LOELj9Gtk67GwJ96iF7/fQps8I/iFIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pact-foundation/pact-core-linux-arm64-musl": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-arm64-musl/-/pact-core-linux-arm64-musl-19.2.0.tgz", + "integrity": "sha512-tGWF7dP72Ho1A1PApyUqUMRI/e+gfbPxfaXxwWMXNh8JLK2jQQnP4bWymGwHQ4vxL7wn8ayx8I5y6x1vk/2+rA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pact-foundation/pact-core-linux-x64-glibc": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-x64-glibc/-/pact-core-linux-x64-glibc-19.2.0.tgz", + "integrity": "sha512-/ZNuWKuFJSBRZH09t7FXqKLy5koCaMIZ4RdbockLYlNo8uWF2ALfRCllj+SbXPLK+T+lVnBrs5s94X2cu4Rc7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pact-foundation/pact-core-linux-x64-musl": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-x64-musl/-/pact-core-linux-x64-musl-19.2.0.tgz", + "integrity": "sha512-n0m39CzVkq8J2alir1redm0rAdUVQzkKJqYBQdRmhEIa+QeGjUrJq+gmakUysoJxfSY4UthnJ9jS8SoyiZ+k6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pact-foundation/pact-core-windows-x64": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-windows-x64/-/pact-core-windows-x64-19.2.0.tgz", + "integrity": "sha512-WZSDBL1Sn8y5+YJTK+HDiS/Fn9th1M4QJW2dflQ8UFwoaIh6yb0TrJjtuWbDGIwNeD4Sv8CJ5jJOf1nyNjFLfg==", "cpu": [ "x64" ], @@ -1860,6 +2778,48 @@ "win32" ] }, + "node_modules/@pact-foundation/pact/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@pact-foundation/pact/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@paulirish/trace_engine": { + "version": "0.0.65", + "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.65.tgz", + "integrity": "sha512-Qsm6F5C8xf6ZzQXbQc2+wcpe6sggfs/gvc/ytqSurdvYg3kyW0ECHCqE0CWBKZpqgjVfPNX9c7SCS3r2nEIRGg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "legacy-javascript": "latest", + "third-party-web": "latest" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "dev": true, + "license": "MIT" + }, "node_modules/@playwright/test": { "version": "1.62.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0.tgz", @@ -1876,57 +2836,249 @@ "node": ">=20" } }, - "node_modules/@rc-component/async-validator": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-6.0.0.tgz", - "integrity": "sha512-D3AGQwdyE58gmvx6waVSXJ80JGO+IY5L2O8HDnSOex7JNlzB3GuN/4hyHNTdhy2qtOhkpbIjmeAN3tL993wKbA==", - "license": "MIT", + "node_modules/@puppeteer/browsers": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.6.tgz", + "integrity": "sha512-B/gKoqlFkzhvzsI6jo9K1cZz9o5ypviVv/xu8CwA4grZzyVwN+XfkT+tu8T1zrauuEXv6VhS2oGX+6NL95WcKA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@babel/runtime": "^7.24.4" + "modern-tar": "^0.7.6", + "yargs": "^18.0.0" + }, + "bin": { + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=14.x" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@rc-component/cascader": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.17.0.tgz", - "integrity": "sha512-3cVNG0zrQF1PoXq262L3wGCU+/YLEC1mGSVHDl577dQmA0ZKkXFbY6nwyXo+beCcM7buo49t24jkr+QZdL7O8w==", + "node_modules/@puppeteer/browsers/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", - "dependencies": { - "@rc-component/select": "~1.8.0", - "@rc-component/tree": "~1.3.2", - "@rc-component/util": "^1.11.1", - "clsx": "^2.1.1" + "engines": { + "node": ">=12" }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@rc-component/checkbox": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-2.0.0.tgz", - "integrity": "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==", + "node_modules/@puppeteer/browsers/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@puppeteer/browsers/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", "dependencies": { - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" + "engines": { + "node": ">=20" } }, - "node_modules/@rc-component/collapse": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.2.0.tgz", - "integrity": "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw==", + "node_modules/@puppeteer/browsers/node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/motion": "^1.1.4", - "@rc-component/util": "^1.3.0", + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@puppeteer/browsers/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@puppeteer/browsers/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@puppeteer/browsers/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@puppeteer/browsers/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@puppeteer/browsers/node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/@rc-component/async-validator": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-6.0.0.tgz", + "integrity": "sha512-D3AGQwdyE58gmvx6waVSXJ80JGO+IY5L2O8HDnSOex7JNlzB3GuN/4hyHNTdhy2qtOhkpbIjmeAN3tL993wKbA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/cascader": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.17.0.tgz", + "integrity": "sha512-3cVNG0zrQF1PoXq262L3wGCU+/YLEC1mGSVHDl577dQmA0ZKkXFbY6nwyXo+beCcM7buo49t24jkr+QZdL7O8w==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.8.0", + "@rc-component/tree": "~1.3.2", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/checkbox": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-2.0.0.tgz", + "integrity": "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/collapse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.2.0.tgz", + "integrity": "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { @@ -1995,13 +3147,13 @@ } }, "node_modules/@rc-component/dropdown": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.2.tgz", - "integrity": "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.3.tgz", + "integrity": "sha512-YTST/N6kpqpDz3IMuM/PSSZnrDpSOA6dgHv12gPA90ZTSLv2CoqkZ0+9NtwTY6BeO7dstPblSic2QJg7dSFy/g==", "license": "MIT", "dependencies": { "@rc-component/trigger": "^3.0.0", - "@rc-component/util": "^1.2.1", + "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { @@ -2397,9 +3549,9 @@ } }, "node_modules/@rc-component/table": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.10.2.tgz", - "integrity": "sha512-b3PjqB9Gp25p5t/zq+9QrbXbodkptT8/zvLmwgd2FNPUUtaYyDnQqfxeD5a7ao8E8lpinLHsi2u2vdfPhyNvAw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.10.4.tgz", + "integrity": "sha512-HwoTnrwc29zeoXkXGhWqzJh8FIibGUxi1jM4LtoSzmR9d5Vv5osUQpZxnXKBP8iOCvyD6BQzZm1nXJRcnrxpAg==", "license": "MIT", "dependencies": { "@rc-component/context": "^2.0.1", @@ -2507,15 +3659,15 @@ } }, "node_modules/@rc-component/trigger": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.9.1.tgz", - "integrity": "sha512-LNsYvz60mrLJ/kRvKcHE7boUvcQfVMCfRqZ71x3Fo9AOiZ1KKIEqkzMA8DNvz2V3Bcvir/vwQNn7JF1NPODQ7Q==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.10.1.tgz", + "integrity": "sha512-mXlDN0IXdtV8Yqqm8195ECCyrbmfvvfKvwVvSlH0+qvKD6BUF8gRhEjSy0FOcD1+CcDRHgTiX99LoxfQrmh3Cw==", "license": "MIT", "dependencies": { - "@rc-component/motion": "^1.1.4", - "@rc-component/portal": "^2.2.0", - "@rc-component/resize-observer": "^1.1.1", - "@rc-component/util": "^1.2.1", + "@rc-component/motion": "^1.3.3", + "@rc-component/portal": "^2.2.1", + "@rc-component/resize-observer": "^1.1.2", + "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "engines": { @@ -2541,13 +3693,13 @@ } }, "node_modules/@rc-component/util": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.11.1.tgz", - "integrity": "sha512-awVlI3ub2vqfqkYxOBc/uQ0efm3jw0wcrhtO/YWLyZfxiKXczKwNbVuhlnyxytDt7H9pbbVQiqr+O6MLATtRYg==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.12.0.tgz", + "integrity": "sha512-AEjPL8JVdohIITaiXokyjL9WQ6tKWWjAYK9QU16tGNE9JaQABBQy+hA4H2Lup5MgXy9yY3iLrbZJheuU13hTdQ==", "license": "MIT", "dependencies": { "is-mobile": "^5.0.0", - "react-is": "^18.2.0" + "react-is": "^19.2.7" }, "peerDependencies": { "react": ">=18.0.0", @@ -2574,9 +3726,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", - "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz", + "integrity": "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==", "cpu": [ "arm64" ], @@ -2591,9 +3743,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", - "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.1.tgz", + "integrity": "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==", "cpu": [ "arm64" ], @@ -2608,9 +3760,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", - "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.1.tgz", + "integrity": "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==", "cpu": [ "x64" ], @@ -2625,9 +3777,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", - "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.1.tgz", + "integrity": "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==", "cpu": [ "x64" ], @@ -2642,9 +3794,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", - "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.1.tgz", + "integrity": "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==", "cpu": [ "arm" ], @@ -2659,9 +3811,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", - "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.1.tgz", + "integrity": "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==", "cpu": [ "arm64" ], @@ -2676,9 +3828,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", - "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.1.tgz", + "integrity": "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==", "cpu": [ "arm64" ], @@ -2693,9 +3845,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", - "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.1.tgz", + "integrity": "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==", "cpu": [ "ppc64" ], @@ -2710,9 +3862,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", - "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.1.tgz", + "integrity": "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==", "cpu": [ "s390x" ], @@ -2727,9 +3879,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", - "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.1.tgz", + "integrity": "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==", "cpu": [ "x64" ], @@ -2744,9 +3896,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", - "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.1.tgz", + "integrity": "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==", "cpu": [ "x64" ], @@ -2761,9 +3913,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", - "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.1.tgz", + "integrity": "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==", "cpu": [ "arm64" ], @@ -2778,28 +3930,59 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", - "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", - "cpu": [ - "wasm32" - ], + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.1.tgz", + "integrity": "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" + "@emnapi/core": "2.0.0-alpha.3", + "@emnapi/runtime": "2.0.0-alpha.3", + "@napi-rs/wasm-runtime": "^1.2.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "2.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", - "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.1.tgz", + "integrity": "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==", "cpu": [ "arm64" ], @@ -2814,9 +3997,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", - "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.1.tgz", + "integrity": "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==", "cpu": [ "x64" ], @@ -2837,100 +4020,449 @@ "dev": true, "license": "MIT" }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", "dev": true, "license": "MIT" }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "node_modules/@sentry/conventions": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.16.0.tgz", + "integrity": "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@sentry/core": { + "version": "10.69.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.69.0.tgz", + "integrity": "sha512-+uuqVEeiDzYuAKjZLqsROKXvRTbl/QeH0gfGRtpYib1cud4rAFWRIkFmcR7Jb7JGFYwmReyQotiTj/hcDszTZg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" + "@sentry/conventions": "^0.16.0" }, "engines": { "node": ">=18" } }, - "node_modules/@testing-library/jest-dom": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz", - "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==", + "node_modules/@sentry/node": { + "version": "10.69.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.69.0.tgz", + "integrity": "sha512-xEXA1YGIiTZbrW6MWV34uS6JGQuQg2ijTI0zed+FsJb9JZKPYel/GZK8Km26vfTVb+yCXFmWZNBesKegNcVdzg==", "dev": true, "license": "MIT", "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.69.0", + "@sentry/node-core": "10.69.0", + "@sentry/opentelemetry": "10.69.0", + "@sentry/server-utils": "10.69.0", + "import-in-the-middle": "^3.0.0" }, "engines": { - "node": ">=22", - "npm": ">=6", - "yarn": ">=1" - }, - "peerDependencies": { - "@testing-library/dom": ">=10 <11" + "node": ">=18" } }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "node_modules/@sentry/node-core": { + "version": "10.69.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.69.0.tgz", + "integrity": "sha512-IgArHczrZJxkgxoffHscj0NxQrG6kCazgmGQnlf3j58J1ec21YaUu8Tu+7G4Lo5tCiW3teQnwlKW1ttMXSqWRw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.5" + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.69.0", + "@sentry/opentelemetry": "10.69.0", + "import-in-the-middle": "^3.0.0" }, "engines": { "node": ">=18" }, "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" }, "peerDependenciesMeta": { - "@types/react": { + "@opentelemetry/api": { "optional": true }, - "@types/react-dom": { + "@opentelemetry/core": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-http": { + "optional": true + }, + "@opentelemetry/instrumentation": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { "optional": true } } }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "node_modules/@sentry/opentelemetry": { + "version": "10.69.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.69.0.tgz", + "integrity": "sha512-3FyWV6YcEJuvLrlaKGE1dHXCI+1YO0a62w7PkwlRg8yp6K6YXkmdwu9GjqaYD+Ju4tm7uC7mHIsGFQMm0M7pqQ==", "dev": true, "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.69.0" + }, "engines": { - "node": ">=12", - "npm": ">=6" + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + } + }, + "node_modules/@sentry/server-utils": { + "version": "10.69.0", + "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.69.0.tgz", + "integrity": "sha512-0MwHrA8+nNvMIsqf8m3cXwCBlUjr6AS7N6CZvHJtY1DkqEvQqEbD5VIrhzEyHN/KMZIgQ8XeDCQRhjnXFQGRhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apm-js-collab/code-transformer-bundler-plugins": "^0.7.3", + "@apm-js-collab/tracing-hooks": "^0.13.0", + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.69.0", + "meriyah": "^6.1.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stryker-mutator/api": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-9.6.1.tgz", + "integrity": "sha512-g8VNoFWQWbx0pdal3Vt8jVCZW+v3sc3gi94iI0GVtVgUGTqphAjJF6EAruPTx0lqvtonsaAxn5TD36hcG1d6Wg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-metrics": "3.7.3", + "mutation-testing-report-schema": "3.7.3", + "tslib": "~2.8.0", + "typed-inject": "~5.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/core": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-9.6.1.tgz", + "integrity": "sha512-WMgnvf+Wyh/yiruhNZwc8w8DlzmmjXhPjSn5MR8RhAXzlnWji8TQrUYgBUkHk9bEgSaIlB3KZHm37iiU5Q2cLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inquirer/prompts": "^8.0.0", + "@stryker-mutator/api": "9.6.1", + "@stryker-mutator/instrumenter": "9.6.1", + "@stryker-mutator/util": "9.6.1", + "ajv": "~8.18.0", + "chalk": "~5.6.0", + "commander": "~14.0.0", + "diff-match-patch": "1.0.5", + "emoji-regex": "~10.6.0", + "execa": "~9.6.0", + "json-rpc-2.0": "^1.7.0", + "lodash.groupby": "~4.6.0", + "minimatch": "~10.2.4", + "mutation-server-protocol": "~0.4.0", + "mutation-testing-elements": "3.7.3", + "mutation-testing-metrics": "3.7.3", + "mutation-testing-report-schema": "3.7.3", + "npm-run-path": "~6.0.0", + "progress": "~2.0.3", + "rxjs": "~7.8.1", + "semver": "^7.6.3", + "source-map": "~0.7.4", + "tree-kill": "~1.2.2", + "tslib": "2.8.1", + "typed-inject": "~5.0.0", + "typed-rest-client": "~2.3.0" + }, + "bin": { + "stryker": "bin/stryker.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stryker-mutator/core/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@stryker-mutator/core/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stryker-mutator/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stryker-mutator/core/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@stryker-mutator/core/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@stryker-mutator/instrumenter": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-9.6.1.tgz", + "integrity": "sha512-5K8wH4Pthly25c2uKKik4Dfcoeou7sbJdFS6u3QIYHlulgFVDJwtEMWTZGkZfs7IiUEXIDNa0keRACq5jn5AvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "~7.29.0", + "@babel/generator": "~7.29.0", + "@babel/parser": "~7.29.0", + "@babel/plugin-proposal-decorators": "~7.29.0", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/preset-typescript": "~7.28.0", + "@stryker-mutator/api": "9.6.1", + "@stryker-mutator/util": "9.6.1", + "angular-html-parser": "~10.4.0", + "semver": "~7.7.0", + "tslib": "2.8.1", + "weapon-regex": "~1.3.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/instrumenter/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@stryker-mutator/util": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-9.6.1.tgz", + "integrity": "sha512-Lk/ALVctJjFv1vvwR+CFoKzDCWvsBlq7flDUnmnpuwTrGbm156EdZD1Jjq4o8KdOap0ezUZqQNE9OAI1m2+pUQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@stryker-mutator/vitest-runner": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/vitest-runner/-/vitest-runner-9.6.1.tgz", + "integrity": "sha512-eyUHTCf3Ui+SUn/tpFJwzw6MV391kyBLZk/cDHFUfKFELqKMLbvd7e81axArlApKqO6cOnLfrxlwED+2SRN0ow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stryker-mutator/api": "9.6.1", + "@stryker-mutator/util": "9.6.1", + "semver": "^7.7.4", + "tslib": "~2.8.0" + }, + "engines": { + "node": ">=14.18.0" + }, + "peerDependencies": { + "@stryker-mutator/core": "9.6.1", + "vitest": ">=2.0.0" + } + }, + "node_modules/@stryker-mutator/vitest-runner/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz", + "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" }, "peerDependencies": { "@testing-library/dom": ">=7.21.4" @@ -3001,13 +4533,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.0.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", - "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~8.3.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/parse-json": { @@ -3054,17 +4586,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", - "integrity": "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.62.0", - "@typescript-eslint/type-utils": "8.62.0", - "@typescript-eslint/utils": "8.62.0", - "@typescript-eslint/visitor-keys": "8.62.0", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -3077,15 +4609,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.62.0", + "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", "dev": true, "license": "MIT", "engines": { @@ -3093,16 +4625,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.0.tgz", - "integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.62.0", - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/typescript-estree": "8.62.0", - "@typescript-eslint/visitor-keys": "8.62.0", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3" }, "engines": { @@ -3118,14 +4650,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz", - "integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.62.0", - "@typescript-eslint/types": "^8.62.0", + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", "debug": "^4.4.3" }, "engines": { @@ -3140,14 +4672,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz", - "integrity": "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/visitor-keys": "8.62.0" + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3158,9 +4690,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz", - "integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", "dev": true, "license": "MIT", "engines": { @@ -3175,15 +4707,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.0.tgz", - "integrity": "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/typescript-estree": "8.62.0", - "@typescript-eslint/utils": "8.62.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -3200,9 +4732,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz", - "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", "dev": true, "license": "MIT", "engines": { @@ -3214,16 +4746,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz", - "integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.62.0", - "@typescript-eslint/tsconfig-utils": "8.62.0", - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/visitor-keys": "8.62.0", + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -3255,16 +4787,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.0.tgz", - "integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.62.0", - "@typescript-eslint/types": "8.62.0", - "@typescript-eslint/typescript-estree": "8.62.0" + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3279,13 +4811,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz", - "integrity": "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/types": "8.65.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3310,9 +4842,9 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", - "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", "dev": true, "license": "MIT", "dependencies": { @@ -3479,6 +5011,47 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -3531,18 +5104,38 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/angular-html-parser": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-10.4.0.tgz", + "integrity": "sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 14" } }, - "node_modules/ansi-styles": { - "version": "5.2.0", + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, @@ -3555,16 +5148,16 @@ } }, "node_modules/antd": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/antd/-/antd-6.5.0.tgz", - "integrity": "sha512-9zbVc9UukfGuqCvIAov01nlpDQWfARNmZQyt21ZhqLX7ilXmi4cdkp12xA48WEmXRXwZvno8A03qQuGE9JG8fg==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.5.2.tgz", + "integrity": "sha512-ntYx0lr4Jq192QnBkDWkDqEeoberXZ34vSE9SgiP/0J6DY8O0pzR3bVZLBsdpCSguVkwjtEAP+QNeMN7LNAvgw==", "license": "MIT", "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", - "@ant-design/icons": "^6.3.1", + "@ant-design/icons": "^6.3.2", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.29.2", "@rc-component/cascader": "~1.17.0", @@ -3573,7 +5166,7 @@ "@rc-component/color-picker": "~3.1.1", "@rc-component/dialog": "~1.10.0", "@rc-component/drawer": "~1.4.2", - "@rc-component/dropdown": "~1.0.2", + "@rc-component/dropdown": "~1.0.3", "@rc-component/form": "~1.8.5", "@rc-component/image": "~1.9.0", "@rc-component/input": "~1.3.1", @@ -3594,15 +5187,15 @@ "@rc-component/slider": "~1.1.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", - "@rc-component/table": "~1.10.2", + "@rc-component/table": "~1.10.4", "@rc-component/tabs": "~1.11.0", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.4.0", "@rc-component/tree": "~1.3.2", "@rc-component/tree-select": "~1.11.0", - "@rc-component/trigger": "^3.9.1", + "@rc-component/trigger": "^3.10.1", "@rc-component/upload": "~1.1.1", - "@rc-component/util": "^1.11.1", + "@rc-component/util": "^1.12.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", @@ -3676,20 +5269,61 @@ "dev": true, "license": "MIT" }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "dev": true, + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/atomically": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", + "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" + } + }, + "node_modules/axe-core": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/axios": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", - "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } @@ -3742,17 +5376,42 @@ "require-from-string": "^2.0.2" } }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/browserslist": { @@ -3789,6 +5448,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -3802,6 +5471,23 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3842,6 +5528,106 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/check-types": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", + "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==", + "dev": true, + "license": "MIT" + }, + "node_modules/chrome-launcher": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.1.tgz", + "integrity": "sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^2.0.1" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.cjs" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-bidi": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -3896,6 +5682,13 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3908,12 +5701,69 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/compute-scroll-into-view": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", "license": "MIT" }, + "node_modules/configstore": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.1.0.tgz", + "integrity": "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "atomically": "^2.0.3", + "dot-prop": "^9.0.0", + "graceful-fs": "^4.2.11", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -3925,6 +5775,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -3934,6 +5785,22 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -3974,6 +5841,13 @@ "node": ">= 8" } }, + "node_modules/csp_evaluator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.8.tgz", + "integrity": "sha512-EwOnfYuNbTytvbMKsLixTrRgnjOa0WZCxGy8A9nnSYAicrdwn+T/epU/yjgymmOxlgKnvH+8wXt+7p/8ak5Feg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/css-tree": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", @@ -4015,6 +5889,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/dayjs": { "version": "1.11.21", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", @@ -4052,6 +5936,16 @@ "dev": true, "license": "MIT" }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4061,6 +5955,16 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4071,6 +5975,17 @@ "node": ">=6" } }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -4081,6 +5996,20 @@ "node": ">=8" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1663043", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1663043.tgz", + "integrity": "sha512-33aOY3ZnBP1dgZsshgaL+/XlsQleiFZgyUaDtdZkEa1nbZhVY1MoDeWjk+wxg25fU924l1ZJfoGNmjjeA/5s1w==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -4088,6 +6017,45 @@ "dev": true, "license": "MIT" }, + "node_modules/dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/drange": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", + "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4102,6 +6070,13 @@ "node": ">= 0.4" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.380", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.380.tgz", @@ -4116,6 +6091,40 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/entities": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", @@ -4200,6 +6209,13 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4213,9 +6229,9 @@ } }, "node_modules/eslint": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", - "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", "dev": true, "license": "MIT", "workspaces": [ @@ -4225,7 +6241,7 @@ "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.6.0", + "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", @@ -4249,7 +6265,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -4302,9 +6318,9 @@ } }, "node_modules/eslint-plugin-simple-import-sort": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-13.0.0.tgz", - "integrity": "sha512-McAc+/Nlvcg4byY/CABGH8kqnefWBj8s3JA2okEtz8ixbECQgU46p0HkTUKa4YS7wvgGceimlc34p1nXqbWqtA==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-14.0.0.tgz", + "integrity": "sha512-NUJO0+XFCkk+o5EsAJruTgnfMEpeWrPWeJS15UVF60GgXmqz1BJ9/3hzlvG7lkL8Bubzos5cCLptThbFfPnSMQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4417,6 +6433,50 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -4427,6 +6487,104 @@ "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-copy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.4.tgz", + "integrity": "sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4448,6 +6606,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-string-truncated-width": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", @@ -4465,6 +6630,23 @@ "fast-string-truncated-width": "^3.0.2" } }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-wrap-ansi": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", @@ -4503,6 +6685,22 @@ } } }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4516,6 +6714,28 @@ "node": ">=16.0.0" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -4612,10 +6832,30 @@ "node": ">=18.3.0" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4656,6 +6896,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4693,6 +6946,23 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { "version": "4.14.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", @@ -4720,9 +6990,9 @@ } }, "node_modules/globals": { - "version": "17.7.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", - "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "version": "17.8.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.8.0.tgz", + "integrity": "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==", "dev": true, "license": "MIT", "engines": { @@ -4744,6 +7014,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/graphql": { "version": "16.14.2", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", @@ -4754,6 +7031,22 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/graphql-tag": { + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.7.tgz", + "integrity": "sha512-xnE/NFzy+0eIesvAsREJZ284zTl/wYuBAvpsFSDhRGRdRHdnE90M21Q3xAWyYInb0J756c6x0pIQ62+vtvOs1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4821,6 +7114,13 @@ "dev": true, "license": "MIT" }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -4874,12 +7174,58 @@ "license": "MIT" }, "node_modules/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-4.0.1.tgz", + "integrity": "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==", + "license": "MIT", + "funding": { + "url": "https://locize.com" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-link-header": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.4.tgz", + "integrity": "sha512-xT3GPW6/ZbGuw4UvwHqErSCEjNUlwbQJuZn9/q5U4WEKfp2kENVCAlousG1zLxHeaQ/ffOHUNpWamvkbBW0eNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, "license": "MIT", "dependencies": { - "void-elements": "3.1.0" + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" } }, "node_modules/https-proxy-agent": { @@ -4895,10 +7241,20 @@ "node": ">= 6" } }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/i18next": { - "version": "26.3.3", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.3.tgz", - "integrity": "sha512-aYVegyBdXSO93CMMihvr47jI7GHSOcIahMpJX+qzUXDzW4xDJf2uenIA+45vDU+YhiVdcfsql70AC9RVdMNrHg==", + "version": "26.3.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz", + "integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==", "funding": [ { "type": "individual", @@ -4915,7 +7271,7 @@ ], "license": "MIT", "peerDependencies": { - "typescript": "^5 || ^6" + "typescript": "^5 || ^6 || ^7" }, "peerDependenciesMeta": { "typescript": { @@ -4923,6 +7279,23 @@ } } }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4933,6 +7306,13 @@ "node": ">= 4" } }, + "node_modules/image-ssim": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", + "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", + "dev": true, + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4949,6 +7329,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-in-the-middle": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.2.tgz", + "integrity": "sha512-jTd2FfOgOWOdgjkHuk/1Ms8VKFXkPs15ymYBETw1sAOrO/dY3XeGVRWir9qBbw7pXr0T2eTFwfCZ+N02HmiNGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -4969,9 +7364,39 @@ "node": ">=8" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/intl-messageformat": { + "version": "10.7.18", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "tslib": "^2.8.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, @@ -4990,6 +7415,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -5036,6 +7477,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -5043,6 +7497,52 @@ "dev": true, "license": "MIT" }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5108,6 +7608,47 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/js-base64": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.9.2.tgz", + "integrity": "sha512-6zayE8QlUdiweYI6cETD/XBSqFcoCUlufn/29PJR99r82x1yDnIprRca0YvAYpAW+ez0GuQkVBC6xG5QkD7OjA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/js-library-detector": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.7.0.tgz", + "integrity": "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5115,39 +7656,39 @@ "license": "MIT" }, "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", + "lru-cache": "^11.5.2", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", + "whatwg-url": "^17.1.0", "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "canvas": "^3.0.0" + "canvas": "^3.2.3" }, "peerDependenciesMeta": { "canvas": { @@ -5165,6 +7706,21 @@ "node": "20 || >=22" } }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", + "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5190,6 +7746,13 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, + "node_modules/json-rpc-2.0": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.7.1.tgz", + "integrity": "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5237,9 +7800,9 @@ } }, "node_modules/knip": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/knip/-/knip-6.22.0.tgz", - "integrity": "sha512-HTqtalzPNERFiyTj/3pERGss8oBzgxL9ZWiEQ29N9/+cTMZ1guIIEtcczrYpaoE76//XSHO11uuc66O9feY8wA==", + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.29.0.tgz", + "integrity": "sha512-A3kXqSBky1tWBAqiU9srdtu0Larhzkuyor0aD/gg+ToiqyBncCCs2Q60sLsxmcKhV0OsKss9LV0hMPpwLv711Q==", "dev": true, "funding": [ { @@ -5257,15 +7820,15 @@ "formatly": "^0.3.0", "get-tsconfig": "4.14.0", "jiti": "^2.7.0", - "oxc-parser": "^0.137.0", - "oxc-resolver": "11.21.3", - "picomatch": "^4.0.4", - "smol-toml": "^1.6.1", + "oxc-parser": "^0.140.0", + "oxc-resolver": "11.24.2", + "picomatch": "^4.0.5", + "smol-toml": "^1.7.0", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", - "unbash": "^4.0.1", + "unbash": "^4.0.3", "yaml": "^2.9.0", - "zod": "^4.1.11" + "zod": "^4.4.3" }, "bin": { "knip": "bin/knip.js", @@ -5275,6 +7838,13 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/legacy-javascript": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/legacy-javascript/-/legacy-javascript-0.0.1.tgz", + "integrity": "sha512-lPyntS4/aS7jpuvOlitZDFifBCb4W8L/3QU0PLbUTUj+zYah8rfVjYic88yG7ZKTxhS5h9iz7duT8oUXKszLhg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5289,10 +7859,71 @@ "node": ">= 0.8.0" } }, + "node_modules/lighthouse": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-13.4.1.tgz", + "integrity": "sha512-fDu8lt3QLK/lTqIxtp1HkzQNJ32rsFHhbadYOepcMZFLgA8oINhxutMbMv8XXnpTOvZ0TXCo4JCk1LDTWaRLnA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@paulirish/trace_engine": "0.0.65", + "@sentry/node": "^10.0.0", + "axe-core": "^4.12.1", + "chrome-launcher": "^1.2.1", + "configstore": "^7.0.0", + "csp_evaluator": "1.1.8", + "devtools-protocol": "0.0.1663043", + "enquirer": "^2.3.6", + "http-link-header": "^1.1.1", + "intl-messageformat": "^10.5.3", + "jpeg-js": "^0.4.4", + "js-library-detector": "^6.7.0", + "lighthouse-logger": "^2.0.2", + "lighthouse-stack-packs": "1.12.3", + "lodash-es": "^4.17.21", + "lookup-closest-locale": "6.2.0", + "open": "^8.4.0", + "puppeteer-core": "^25.3.0", + "robots-parser": "^3.0.1", + "speedline-core": "^1.4.3", + "third-party-web": "^0.29.2", + "tldts-icann": "^7.4.9", + "web-features": "^3.34.0", + "ws": "^7.0.0", + "yargs": "^17.3.1", + "yargs-parser": "^21.0.0" + }, + "bin": { + "chrome-debug": "core/scripts/manual-chrome-launcher.js", + "lighthouse": "cli/index.js", + "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js" + }, + "engines": { + "node": ">=22.19" + } + }, + "node_modules/lighthouse-logger": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.2.tgz", + "integrity": "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.1", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-stack-packs": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/lighthouse-stack-packs/-/lighthouse-stack-packs-1.12.3.tgz", + "integrity": "sha512-d8IsOpE83kbANgnM+Tp8+x6HcMpX9o2ITBiUERssgzAIFdZCQzs/f4k6D0DLQTE59enml9mbAOU52Wu35exWtg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -5306,23 +7937,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -5341,9 +7972,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], @@ -5362,9 +7993,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], @@ -5383,9 +8014,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], @@ -5404,9 +8035,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], @@ -5425,9 +8056,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ "arm64" ], @@ -5446,9 +8077,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], @@ -5467,9 +8098,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ "x64" ], @@ -5488,9 +8119,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ "x64" ], @@ -5509,9 +8140,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], @@ -5530,9 +8161,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], @@ -5578,6 +8209,27 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lookup-closest-locale": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", + "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5649,6 +8301,13 @@ "node": ">=10" } }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5665,6 +8324,43 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meriyah": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-6.1.4.tgz", + "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -5696,6 +8392,13 @@ "node": ">=4" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -5712,6 +8415,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/modern-tar": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.7.tgz", + "integrity": "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5763,6 +8500,43 @@ } } }, + "node_modules/mutation-server-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mutation-server-protocol/-/mutation-server-protocol-0.4.1.tgz", + "integrity": "sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.1.12" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mutation-testing-elements": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-3.7.3.tgz", + "integrity": "sha512-SMeIPxngJpfjfNYctFpYQQtlBlZaVO0aoB3FKdwrI8Ee/2bkyUuCZzAOCLv1U9fnmfA37dPFq0Owduoxs2XgGQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/mutation-testing-metrics": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-3.7.3.tgz", + "integrity": "sha512-B8QrP0ZomErzTPNlhrzKWPNBln+3afwBZPHv0Q7N8wZZTYxMptzb/Gdm3ExXVmioVYrtZAtsDs7W/T/b2AixOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-report-schema": "3.7.3" + } + }, + "node_modules/mutation-testing-report-schema": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-3.7.3.tgz", + "integrity": "sha512-BHm3MYq+ckO+t5CtlG8zpqxc75rdJCkxVlE+fGuGJM3F7tNCQ/OW2N+TQVHN3BHsYa84+BFc6g3AwDYkUsw2MA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/mute-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", @@ -5774,9 +8548,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -5799,6 +8573,28 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "2.0.50", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", @@ -5809,6 +8605,49 @@ "node": ">=18" } }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obug": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", @@ -5823,15 +8662,66 @@ "node": ">=12.20.0" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", "dev": true, "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", @@ -5849,13 +8739,13 @@ "license": "MIT" }, "node_modules/oxc-parser": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.137.0.tgz", - "integrity": "sha512-yFImD+WLElJpLKy8llG1qe4DCmMsL18peRp8XP1JKfig/gISbJkglnpDtX2aTmAn10kZF7164HbN2H8QPsXxGg==", + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.140.0.tgz", + "integrity": "sha512-h6QFWd6lBMfjESqgQ27GjzrSDb0qbznp7VDQqp2zvgsrWut4vcchyMIzOVXvGQ2GMZgKw9RWrFNWv9WqGL0p7Q==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "^0.137.0" + "@oxc-project/types": "^0.140.0" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -5864,57 +8754,57 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxc-parser/binding-android-arm-eabi": "0.137.0", - "@oxc-parser/binding-android-arm64": "0.137.0", - "@oxc-parser/binding-darwin-arm64": "0.137.0", - "@oxc-parser/binding-darwin-x64": "0.137.0", - "@oxc-parser/binding-freebsd-x64": "0.137.0", - "@oxc-parser/binding-linux-arm-gnueabihf": "0.137.0", - "@oxc-parser/binding-linux-arm-musleabihf": "0.137.0", - "@oxc-parser/binding-linux-arm64-gnu": "0.137.0", - "@oxc-parser/binding-linux-arm64-musl": "0.137.0", - "@oxc-parser/binding-linux-ppc64-gnu": "0.137.0", - "@oxc-parser/binding-linux-riscv64-gnu": "0.137.0", - "@oxc-parser/binding-linux-riscv64-musl": "0.137.0", - "@oxc-parser/binding-linux-s390x-gnu": "0.137.0", - "@oxc-parser/binding-linux-x64-gnu": "0.137.0", - "@oxc-parser/binding-linux-x64-musl": "0.137.0", - "@oxc-parser/binding-openharmony-arm64": "0.137.0", - "@oxc-parser/binding-wasm32-wasi": "0.137.0", - "@oxc-parser/binding-win32-arm64-msvc": "0.137.0", - "@oxc-parser/binding-win32-ia32-msvc": "0.137.0", - "@oxc-parser/binding-win32-x64-msvc": "0.137.0" + "@oxc-parser/binding-android-arm-eabi": "0.140.0", + "@oxc-parser/binding-android-arm64": "0.140.0", + "@oxc-parser/binding-darwin-arm64": "0.140.0", + "@oxc-parser/binding-darwin-x64": "0.140.0", + "@oxc-parser/binding-freebsd-x64": "0.140.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.140.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.140.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.140.0", + "@oxc-parser/binding-linux-arm64-musl": "0.140.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.140.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.140.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.140.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.140.0", + "@oxc-parser/binding-linux-x64-gnu": "0.140.0", + "@oxc-parser/binding-linux-x64-musl": "0.140.0", + "@oxc-parser/binding-openharmony-arm64": "0.140.0", + "@oxc-parser/binding-wasm32-wasi": "0.140.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.140.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.140.0", + "@oxc-parser/binding-win32-x64-msvc": "0.140.0" } }, "node_modules/oxc-resolver": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.3.tgz", - "integrity": "sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==", + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.24.2.tgz", + "integrity": "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxc-resolver/binding-android-arm-eabi": "11.21.3", - "@oxc-resolver/binding-android-arm64": "11.21.3", - "@oxc-resolver/binding-darwin-arm64": "11.21.3", - "@oxc-resolver/binding-darwin-x64": "11.21.3", - "@oxc-resolver/binding-freebsd-x64": "11.21.3", - "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.3", - "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.3", - "@oxc-resolver/binding-linux-arm64-gnu": "11.21.3", - "@oxc-resolver/binding-linux-arm64-musl": "11.21.3", - "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.3", - "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.3", - "@oxc-resolver/binding-linux-riscv64-musl": "11.21.3", - "@oxc-resolver/binding-linux-s390x-gnu": "11.21.3", - "@oxc-resolver/binding-linux-x64-gnu": "11.21.3", - "@oxc-resolver/binding-linux-x64-musl": "11.21.3", - "@oxc-resolver/binding-openharmony-arm64": "11.21.3", - "@oxc-resolver/binding-wasm32-wasi": "11.21.3", - "@oxc-resolver/binding-win32-arm64-msvc": "11.21.3", - "@oxc-resolver/binding-win32-x64-msvc": "11.21.3" + "@oxc-resolver/binding-android-arm-eabi": "11.24.2", + "@oxc-resolver/binding-android-arm64": "11.24.2", + "@oxc-resolver/binding-darwin-arm64": "11.24.2", + "@oxc-resolver/binding-darwin-x64": "11.24.2", + "@oxc-resolver/binding-freebsd-x64": "11.24.2", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", + "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", + "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", + "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-x64-musl": "11.24.2", + "@oxc-resolver/binding-openharmony-arm64": "11.24.2", + "@oxc-resolver/binding-wasm32-wasi": "11.24.2", + "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", + "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, "node_modules/p-limit": { @@ -5979,6 +8869,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -5992,6 +8895,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6048,9 +8961,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -6060,6 +8973,71 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "dev": true, + "license": "MIT" + }, "node_modules/playwright": { "version": "1.62.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", @@ -6092,25 +9070,10 @@ "node": ">=20" } }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -6128,7 +9091,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6184,6 +9147,63 @@ "dev": true, "license": "MIT" }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -6193,6 +9213,17 @@ "node": ">=10" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6203,41 +9234,167 @@ "node": ">=6" } }, + "node_modules/puppeteer-core": { + "version": "25.4.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.4.0.tgz", + "integrity": "sha512-K1plkLOdeoUnGeT1OvdqF3qxl33v+Ra/uH5VyPEhXdMcpvGiEskHzxxEU3fgpccJpJLIipB/rPUsvkZRWeKqOA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.0.6", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1653615", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.2", + "ws": "^8.21.1" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/puppeteer-core/node_modules/devtools-protocol": { + "version": "0.0.1653615", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1653615.tgz", + "integrity": "sha512-pGVkY3T/qXxAp2nFPodwYqOevk6ncNMSmvL8QfRCx5ZWGd6Vor7AFNmyaA8Zs6uJyP1QAfjuLandCgvSix1BNA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ramda": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.32.0.tgz", + "integrity": "sha512-GQWAHhxhxWBWA8oIBr1XahFVjQ9Fic6MK9ikijfd4TZHfE2+urfk+irVlR5VOn48uwMgM+loRRBJd6Yjsbc0zQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ramda" + } + }, + "node_modules/randexp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", + "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "drange": "^1.0.2", + "ret": "^0.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.7" + "react": "^19.2.8" } }, "node_modules/react-i18next": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", - "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", + "version": "17.0.11", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.11.tgz", + "integrity": "sha512-cDtkXgxjuFTWUH6V+aQn1Ve5vDiUztCNPWW5GtSHDccsgRXO1nE6QFWCEmc1KAutrb3OUv87wFShJL5RhUwPXg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", - "html-parse-stringify": "^3.0.1", + "html-parse-stringify": "^4.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.2.0", "react": ">= 16.8.0", - "typescript": "^5 || ^6" + "typescript": "^5 || ^6 || ^7" }, "peerDependenciesMeta": { "react-dom": { @@ -6252,26 +9409,25 @@ } }, "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", "license": "MIT" }, "node_modules/react-router": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", - "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.3.0.tgz", + "integrity": "sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ==", "license": "MIT", "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" + "cookie-es": "^3.1.1" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.22.0" }, "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" + "react": ">=19.2.7", + "react-dom": ">=19.2.7" }, "peerDependenciesMeta": { "react-dom": { @@ -6279,20 +9435,14 @@ } } }, - "node_modules/react-router-dom": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", - "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, "license": "MIT", - "dependencies": { - "react-router": "7.18.0" - }, "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" + "node": ">= 12.13.0" } }, "node_modules/redent": { @@ -6329,6 +9479,27 @@ "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -6369,6 +9540,16 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/rettime": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz", @@ -6376,14 +9557,24 @@ "dev": true, "license": "MIT" }, + "node_modules/robots-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz", + "integrity": "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/rolldown": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", - "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz", + "integrity": "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.137.0", + "@oxc-project/types": "=0.142.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -6393,23 +9584,88 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.3", - "@rolldown/binding-darwin-arm64": "1.1.3", - "@rolldown/binding-darwin-x64": "1.1.3", - "@rolldown/binding-freebsd-x64": "1.1.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", - "@rolldown/binding-linux-arm64-gnu": "1.1.3", - "@rolldown/binding-linux-arm64-musl": "1.1.3", - "@rolldown/binding-linux-ppc64-gnu": "1.1.3", - "@rolldown/binding-linux-s390x-gnu": "1.1.3", - "@rolldown/binding-linux-x64-gnu": "1.1.3", - "@rolldown/binding-linux-x64-musl": "1.1.3", - "@rolldown/binding-openharmony-arm64": "1.1.3", - "@rolldown/binding-wasm32-wasi": "1.1.3", - "@rolldown/binding-win32-arm64-msvc": "1.1.3", - "@rolldown/binding-win32-x64-msvc": "1.1.3" + "@rolldown/binding-android-arm64": "1.2.1", + "@rolldown/binding-darwin-arm64": "1.2.1", + "@rolldown/binding-darwin-x64": "1.2.1", + "@rolldown/binding-freebsd-x64": "1.2.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", + "@rolldown/binding-linux-arm64-gnu": "1.2.1", + "@rolldown/binding-linux-arm64-musl": "1.2.1", + "@rolldown/binding-linux-ppc64-gnu": "1.2.1", + "@rolldown/binding-linux-s390x-gnu": "1.2.1", + "@rolldown/binding-linux-x64-gnu": "1.2.1", + "@rolldown/binding-linux-x64-musl": "1.2.1", + "@rolldown/binding-openharmony-arm64": "1.2.1", + "@rolldown/binding-wasm32-wasi": "1.2.1", + "@rolldown/binding-win32-arm64-msvc": "1.2.1", + "@rolldown/binding-win32-x64-msvc": "1.2.1" + } + }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", + "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -6438,6 +9694,30 @@ "compute-scroll-into-view": "^3.0.2" } }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semifies": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semifies/-/semifies-1.0.0.tgz", + "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -6448,11 +9728,86 @@ "semver": "bin/semver.js" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" }, "node_modules/shebang-command": { "version": "2.0.0", @@ -6467,14 +9822,90 @@ "node": ">=8" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/siginfo": { @@ -6510,6 +9941,16 @@ "url": "https://github.com/sponsors/cyyynthia" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -6529,6 +9970,54 @@ "node": ">=0.10.0" } }, + "node_modules/speedline-core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz", + "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "image-ssim": "^0.2.0", + "jpeg-js": "^0.4.1" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -6594,6 +10083,19 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -6620,6 +10122,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "dev": true, + "license": "MIT" + }, "node_modules/stylis": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", @@ -6671,6 +10190,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/third-party-web": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.29.2.tgz", + "integrity": "sha512-fegtha91tq2DHphyoiBXVHjVi2YG9zFaRnboT9C28tO1en9Y3wJsfspuy40F+u5wl3hHVbw7cnd1b67kEGHb8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "dev": true, + "license": "MIT" + }, "node_modules/throttle-debounce": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", @@ -6744,6 +10290,26 @@ "dev": true, "license": "MIT" }, + "node_modules/tldts-icann": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-7.4.9.tgz", + "integrity": "sha512-q6gyxCkcFPu5OZd2hi2quysxCG3ejIi53EACesbCEZHhH7FO3HnliqwO5zUs0TV6dA54SxhCrTGAc4ugl7rTiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.9" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tough-cookie": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", @@ -6770,6 +10336,16 @@ "node": ">=20" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6788,8 +10364,17 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } }, "node_modules/type-check": { "version": "0.4.0", @@ -6820,6 +10405,86 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typed-inject": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/typed-inject/-/typed-inject-5.0.0.tgz", + "integrity": "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/typed-rest-client": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.3.1.tgz", + "integrity": "sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "des.js": "^1.1.0", + "js-md4": "^0.3.2", + "qs": "6.15.1", + "tunnel": "0.0.6", + "underscore": "^1.13.8" + }, + "engines": { + "node": ">= 16.0.0" + } + }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -6835,16 +10500,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.62.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.0.tgz", - "integrity": "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.62.0", - "@typescript-eslint/parser": "8.62.0", - "@typescript-eslint/typescript-estree": "8.62.0", - "@typescript-eslint/utils": "8.62.0" + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6859,32 +10524,62 @@ } }, "node_modules/unbash": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.1.tgz", - "integrity": "sha512-1ajSo3813sDoVIHx4inJdUS4l5L2ic5cFiddemPiyjb/PZEoBAhFwHtbaEdRDFxbAKy7FCG7s5ww3/uCFawuIA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.4.tgz", + "integrity": "sha512-60m9IVGbavD6jholbxt0jVBXZkEB/HsMZq7Tyaghseve2/Sf0zQRAIfWsD34sde+DKP2tBxJS2wP88ZM0D1FhA==", "dev": true, "license": "ISC", "engines": { "node": ">=14" } }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, "node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", + "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==", "dev": true, "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">=22.19.0" } }, "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/until-async": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", @@ -6954,17 +10649,27 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz", - "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "~1.1.2", + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "bin": { @@ -6981,7 +10686,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", + "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -7032,6 +10737,21 @@ } } }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/vitest": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", @@ -7122,15 +10842,6 @@ } } }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -7154,6 +10865,27 @@ "node": "20 || >=22" } }, + "node_modules/weapon-regex": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/weapon-regex/-/weapon-regex-1.3.6.tgz", + "integrity": "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/web-features": { + "version": "3.34.2", + "resolved": "https://registry.npmjs.org/web-features/-/web-features-3.34.2.tgz", + "integrity": "sha512-1/prthzNwl/ITxBgFuKUCJ1ezWxZDM8rhz/VIBCD6zMEv5STMDQxgf05IfYxmJvFBTTLONaTDUk7umPkhUHmpw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", + "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -7189,6 +10921,13 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7266,6 +11005,48 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -7358,6 +11139,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/package.json b/package.json index 272e478..58fd117 100644 --- a/package.json +++ b/package.json @@ -1,39 +1,58 @@ { "name": "kernel", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", + "engines": { + "node": ">=24.15.0 <25" + }, + "overrides": { + "qs": "^6.15.3" + }, "scripts": { + "audit": "npm audit --audit-level=high", + "bundle:size": "node scripts/check-bundle-size.mjs", "dev": "vite --host", - "build": "tsc -b && vite build", + "build": "node scripts/validate-production-env.mjs && tsc -b && vite build", "format": "prettier . --write", "format:check": "prettier . --check", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "eslint . --ext .ts,.tsx --fix", + "lighthouse": "node scripts/run-lighthouse.mjs", "knip": "knip", + "performance": "npm run build && npm run bundle:size && npm run lighthouse", "preview": "vite preview", "test": "vitest run", + "test:contract": "vitest run --config=vitest.contract.config.ts", "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", "test:e2e:report": "playwright show-report", "test:e2e:ui": "playwright test --ui", + "test:e2e:update-snapshots": "playwright test --project=visual-chromium --update-snapshots", + "test:smoke": "playwright test --config=smoke.config.ts", + "test:staging": "playwright test --config=staging.config.ts", + "test:mutation": "stryker run", "test:watch": "vitest", "typecheck": "tsc --noEmit" }, "dependencies": { - "antd": "^6.5.0", + "antd": "^6.5.2", "antd-style": "^4.1.0", - "axios": "^1.18.1", + "axios": "^1.19.0", "dayjs": "^1.11.21", - "i18next": "^26.3.3", + "i18next": "^26.3.6", "jalaliday": "^3.1.1", "lodash": "^4.18.1", - "react": "^19.2.7", - "react-dom": "^19.2.7", - "react-i18next": "^17.0.8", - "react-router-dom": "^7.18.0" + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-i18next": "^17.0.11", + "react-router": "^8.3.0" }, "devDependencies": { + "@axe-core/playwright": "4.12.1", + "@pact-foundation/pact": "^17.0.1", + "@stryker-mutator/core": "^9.6.1", + "@stryker-mutator/vitest-runner": "^9.6.1", "@eslint/js": "^10.0.1", "@playwright/test": "^1.62.0", "@testing-library/dom": "^10.4.1", @@ -41,25 +60,27 @@ "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/lodash": "^4.17.24", - "@types/node": "^26.0.1", + "@types/node": "^24.13.3", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "@typescript-eslint/types": "^8.62.0", + "@typescript-eslint/types": "^8.65.0", "@vitejs/plugin-basic-ssl": "^2.3.0", - "@vitejs/plugin-react": "^6.0.3", + "@vitejs/plugin-react": "^6.0.5", "@vitest/coverage-v8": "^4.1.10", - "eslint": "^10.6.0", + "chrome-launcher": "^1.2.1", + "eslint": "^10.8.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", - "eslint-plugin-simple-import-sort": "^13.0.0", - "globals": "^17.7.0", - "jsdom": "^29.1.1", - "knip": "^6.22.0", + "eslint-plugin-simple-import-sort": "^14.0.0", + "globals": "^17.8.0", + "jsdom": "^30.0.1", + "knip": "^6.29.0", + "lighthouse": "^13.4.1", "msw": "^2.15.0", "prettier": "^3.9.6", "typescript": "~6.0.3", - "typescript-eslint": "^8.62.0", - "vite": "^8.1.0", + "typescript-eslint": "^8.65.0", + "vite": "^8.2.0", "vitest": "^4.1.10" } } diff --git a/playwright.config.ts b/playwright.config.ts index 5a76495..7fc754a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -4,6 +4,11 @@ const baseURL = "https://127.0.0.1:4173"; export default defineConfig({ expect: { + toHaveScreenshot: { + animations: "disabled", + caret: "hide", + maxDiffPixelRatio: 0.01, + }, timeout: 5_000, }, forbidOnly: !!process.env.CI, @@ -12,6 +17,33 @@ export default defineConfig({ projects: [ { name: "chromium", + testIgnore: /visual\.spec\.ts/, + use: { + ...devices["Desktop Chrome"], + ...(process.env.CI ? {} : { channel: "chrome" as const }), + }, + }, + { + name: "firefox", + testIgnore: /visual\.spec\.ts/, + use: devices["Desktop Firefox"], + }, + { + name: "webkit", + testIgnore: /visual\.spec\.ts/, + use: devices["Desktop Safari"], + }, + { + name: "mobile-chromium", + testIgnore: /visual\.spec\.ts/, + use: { + ...devices["Pixel 7"], + ...(process.env.CI ? {} : { channel: "chrome" as const }), + }, + }, + { + name: "visual-chromium", + testMatch: /visual\.spec\.ts/, use: { ...devices["Desktop Chrome"], ...(process.env.CI ? {} : { channel: "chrome" as const }), @@ -22,6 +54,8 @@ export default defineConfig({ ? [["line"], ["html", { open: "never" }]] : [["list"], ["html", { open: "never" }]], retries: process.env.CI ? 2 : 0, + snapshotPathTemplate: + "{testDir}/{testFilePath}-snapshots/{arg}-{projectName}{ext}", testDir: "./e2e", use: { baseURL, diff --git a/public/fonts/iransans/README.md b/public/fonts/iransans/README.md deleted file mode 100644 index 422932c..0000000 --- a/public/fonts/iransans/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Local IRANSansX fonts - -IRANSansX font binaries are not committed because their public redistribution license is unclear. - -To enable the font locally: - -1. Obtain a valid IRANSansX license. -2. Copy the licensed `.woff2` or `.woff` files into this directory. -3. Add matching `@font-face` rules in `src/styles/iransans.css`. - -The app already keeps `"IRANSansX"` first in the global font stack, so the UI falls back safely until local font files are configured. diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..5fab763 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "packages": { + ".": { + "package-name": "kernel", + "release-type": "node" + } + } +} diff --git a/scripts/check-bundle-size.mjs b/scripts/check-bundle-size.mjs new file mode 100644 index 0000000..9faa794 --- /dev/null +++ b/scripts/check-bundle-size.mjs @@ -0,0 +1,45 @@ +import console from "node:console"; +import { readdir, stat } from "node:fs/promises"; +import path from "node:path"; + +const assetsDirectory = path.resolve("dist/assets"); +const maximumChunkBytes = 450_000; +const maximumTotalBytes = 1_600_000; + +const assetNames = await readdir(assetsDirectory); +const javascriptAssets = assetNames.filter((name) => name.endsWith(".js")); + +if (javascriptAssets.length === 0) { + throw new Error(`No JavaScript assets found in ${assetsDirectory}`); +} + +const assets = await Promise.all( + javascriptAssets.map(async (name) => ({ + bytes: (await stat(path.join(assetsDirectory, name))).size, + name, + })), +); +const oversizedAssets = assets.filter(({ bytes }) => bytes > maximumChunkBytes); +const totalBytes = assets.reduce((total, { bytes }) => total + bytes, 0); + +if (oversizedAssets.length > 0 || totalBytes > maximumTotalBytes) { + const failures = oversizedAssets.map( + ({ bytes, name }) => + `${name} is ${bytes.toLocaleString()} bytes (limit ${maximumChunkBytes.toLocaleString()})`, + ); + + if (totalBytes > maximumTotalBytes) { + failures.push( + `Total JavaScript is ${totalBytes.toLocaleString()} bytes (limit ${maximumTotalBytes.toLocaleString()})`, + ); + } + + throw new Error(`Bundle size budget exceeded:\n${failures.join("\n")}`); +} + +const largestAsset = assets.toSorted((a, b) => b.bytes - a.bytes)[0]; + +console.log( + `Bundle budgets passed: ${assets.length} chunks, ${totalBytes.toLocaleString()} bytes total, ` + + `${largestAsset.name} largest at ${largestAsset.bytes.toLocaleString()} bytes.`, +); diff --git a/scripts/run-lighthouse.mjs b/scripts/run-lighthouse.mjs new file mode 100644 index 0000000..eebe1b7 --- /dev/null +++ b/scripts/run-lighthouse.mjs @@ -0,0 +1,97 @@ +import console from "node:console"; +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; +import { URL } from "node:url"; + +import * as chromeLauncher from "chrome-launcher"; +import lighthouse from "lighthouse"; +import { preview } from "vite"; + +const reportDirectory = path.resolve(".lighthouseci"); +const appBasePath = (process.env.VITE_APP_BASE_URL ?? "/").replace( + /^\/+|\/+$/g, + "", +); +const url = new URL( + `/${[appBasePath, "auth"].filter(Boolean).join("/")}`, + "https://127.0.0.1:4174", +).href; +const thresholds = { + "cumulative-layout-shift": 0.1, + "first-contentful-paint": 4_000, + interactive: 4_500, + "largest-contentful-paint": 4_000, + "total-blocking-time": 300, +}; + +const server = await preview({ + preview: { + host: "127.0.0.1", + port: 4174, + strictPort: true, + }, +}); +const chrome = await chromeLauncher.launch({ + chromeFlags: ["--headless=new", "--ignore-certificate-errors"], +}); + +try { + const result = await lighthouse(url, { + logLevel: "error", + onlyCategories: ["performance"], + output: ["html", "json"], + port: chrome.port, + }); + + if (!result) throw new Error("Lighthouse did not produce a result"); + + const reports = Array.isArray(result.report) + ? result.report + : [result.report]; + + await mkdir(reportDirectory, { recursive: true }); + await Promise.all([ + writeFile(path.join(reportDirectory, "report.html"), reports[0]), + writeFile(path.join(reportDirectory, "report.json"), reports[1]), + ]); + + const failures = []; + const performanceScore = result.lhr.categories.performance.score ?? 0; + + if (performanceScore < 0.75) { + failures.push( + `performance score ${Math.round(performanceScore * 100)} (minimum 75)`, + ); + } + + for (const [auditId, maximum] of Object.entries(thresholds)) { + const value = result.lhr.audits[auditId].numericValue; + + if (value === undefined || value > maximum) { + failures.push( + `${auditId} ${value?.toFixed(2) ?? "unavailable"} (maximum ${maximum})`, + ); + } + } + + if (failures.length > 0) { + throw new Error(`Lighthouse budget exceeded:\n${failures.join("\n")}`); + } + + console.log( + `Lighthouse budgets passed with a performance score of ${Math.round(performanceScore * 100)}.`, + ); +} finally { + try { + await chrome.kill(); + } catch (error) { + console.warn(`Chrome cleanup warning: ${error.message}`); + } + await new Promise((resolve, reject) => { + server.httpServer.close((error) => { + if (error) reject(error); + else resolve(); + }); + }); +} diff --git a/scripts/validate-production-env.mjs b/scripts/validate-production-env.mjs new file mode 100644 index 0000000..956ec55 --- /dev/null +++ b/scripts/validate-production-env.mjs @@ -0,0 +1,43 @@ +import process from "node:process"; +import { URL } from "node:url"; + +import { loadEnv } from "vite"; + +const env = loadEnv("production", process.cwd(), ""); +const apiUrl = env.VITE_API_BASE_URL; +const appBaseUrl = env.VITE_APP_BASE_URL; +const observabilityUrl = env.VITE_OBSERVABILITY_URL; + +if (!apiUrl) { + throw new Error("VITE_API_BASE_URL is required for production builds"); +} + +let parsedApiUrl; + +try { + parsedApiUrl = new URL(apiUrl); +} catch { + throw new Error("VITE_API_BASE_URL must be a valid absolute URL"); +} + +if (parsedApiUrl.protocol !== "https:") { + throw new Error("VITE_API_BASE_URL must use HTTPS in production"); +} + +if (!appBaseUrl || !appBaseUrl.startsWith("/") || !appBaseUrl.endsWith("/")) { + throw new Error("VITE_APP_BASE_URL must start and end with / in production"); +} + +if (observabilityUrl) { + let parsedObservabilityUrl; + + try { + parsedObservabilityUrl = new URL(observabilityUrl); + } catch { + throw new Error("VITE_OBSERVABILITY_URL must be a valid absolute URL"); + } + + if (parsedObservabilityUrl.protocol !== "https:") { + throw new Error("VITE_OBSERVABILITY_URL must use HTTPS in production"); + } +} diff --git a/smoke.config.ts b/smoke.config.ts new file mode 100644 index 0000000..1629568 --- /dev/null +++ b/smoke.config.ts @@ -0,0 +1,40 @@ +import { defineConfig, devices } from "@playwright/test"; + +const deploymentUrl = new URL( + process.env.SMOKE_BASE_URL ?? "https://smoke-target.invalid/", +); + +if (!deploymentUrl.pathname.endsWith("/")) { + deploymentUrl.pathname += "/"; +} + +export default defineConfig({ + expect: { + timeout: 10_000, + }, + forbidOnly: true, + globalSetup: "./smoke/setup.ts", + outputDir: "test-results/smoke", + projects: [ + { + name: "deployed-chromium", + use: { + ...devices["Desktop Chrome"], + ...(process.env.CI ? {} : { channel: "chrome" as const }), + }, + }, + ], + reporter: [ + ["line"], + ["html", { open: "never", outputFolder: "playwright-report/smoke" }], + ], + retries: process.env.CI ? 2 : 0, + testDir: "./smoke", + use: { + baseURL: deploymentUrl.href, + ignoreHTTPSErrors: process.env.SMOKE_IGNORE_HTTPS_ERRORS === "true", + screenshot: "only-on-failure", + trace: "on-first-retry", + }, + workers: 1, +}); diff --git a/smoke/deployment.spec.ts b/smoke/deployment.spec.ts new file mode 100644 index 0000000..9414e8e --- /dev/null +++ b/smoke/deployment.spec.ts @@ -0,0 +1,46 @@ +import { expect, test } from "@playwright/test"; + +const apiHealthUrl = process.env.SMOKE_API_HEALTH_URL; + +if (!apiHealthUrl) throw new Error("SMOKE_API_HEALTH_URL is required"); + +test("loads the application and its static assets", async ({ page }) => { + const applicationOrigin = new URL(test.info().project.use.baseURL as string) + .origin; + const assetResponses: { ok: boolean; url: string }[] = []; + + page.on("response", (response) => { + const resourceType = response.request().resourceType(); + + if ( + ["font", "script", "stylesheet"].includes(resourceType) && + new URL(response.url()).origin === applicationOrigin + ) { + assetResponses.push({ ok: response.ok(), url: response.url() }); + } + }); + + const documentResponse = await page.goto("auth"); + + expect(documentResponse?.ok()).toBe(true); + await expect(page).toHaveTitle("kernel"); + await expect(page.locator("#root > *").first()).toBeVisible(); + expect(assetResponses.length).toBeGreaterThan(0); + expect(assetResponses.filter(({ ok }) => !ok)).toEqual([]); +}); + +test("supports direct navigation to a client-side route", async ({ page }) => { + const response = await page.goto("auth/register"); + + expect(response?.ok()).toBe(true); + await expect(page.locator("form")).toBeVisible(); + await expect(page).toHaveURL(/\/auth\/register\/?$/); +}); + +test("reaches the deployed API health endpoint", async ({ request }) => { + const response = await request.get(apiHealthUrl, { + failOnStatusCode: false, + }); + + expect(response.ok()).toBe(true); +}); diff --git a/smoke/setup.ts b/smoke/setup.ts new file mode 100644 index 0000000..5abe994 --- /dev/null +++ b/smoke/setup.ts @@ -0,0 +1,16 @@ +const validateHttpsUrl = (name: string) => { + const value = process.env[name]; + + if (!value) throw new Error(`${name} is required`); + + const url = new URL(value); + + if (url.protocol !== "https:") { + throw new Error(`${name} must use HTTPS`); + } +}; + +export default () => { + validateHttpsUrl("SMOKE_API_HEALTH_URL"); + validateHttpsUrl("SMOKE_BASE_URL"); +}; diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..bbee1db --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,9 @@ +sonar.organization=sepaseh +sonar.sources=src +sonar.tests=src,e2e +sonar.test.inclusions=src/**/*.test.ts,src/**/*.test.tsx,e2e/**/*.spec.ts +sonar.exclusions=src/**/*.d.ts,src/**/*.test.ts,src/**/*.test.tsx,src/assets/fonts/**,src/test/** +sonar.coverage.exclusions=src/**/*.d.ts,src/**/*.test.ts,src/**/*.test.tsx,src/**/index.ts,src/main.tsx,src/test/**,src/types/** +sonar.javascript.lcov.reportPaths=coverage/lcov.info +sonar.qualitygate.ignoreSmallChanges=false +sonar.qualitygate.wait=true diff --git a/src/App.tsx b/src/App.tsx index f001e6f..f618d18 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,14 @@ +import { ErrorBoundary } from "@/components/error-boundary"; import { AntdProvider, CoreProvider } from "@/providers"; import { Routes } from "./Routes"; export const App = () => ( - - - - - + + + + + + + ); diff --git a/src/Routes.test.tsx b/src/Routes.test.tsx new file mode 100644 index 0000000..5e45806 --- /dev/null +++ b/src/Routes.test.tsx @@ -0,0 +1,79 @@ +import { render, screen } from "@testing-library/react"; +import { Outlet } from "react-router"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const routeState = vi.hoisted(() => ({ + allowedRoutes: new Set([ + "account", + "auth", + "forgotPassword", + "register", + "roles", + "root", + "users", + ]), + setCurrentRoute: vi.fn(), +})); + +vi.mock("@/hooks", () => ({ + useAllowedRoutes: () => routeState.allowedRoutes, + useCore: () => ({ setCurrentRoute: routeState.setCurrentRoute }), +})); +vi.mock("@/layouts/auth", () => ({ AuthLayout: () => })); +vi.mock("@/layouts/default", () => ({ DefaultLayout: () => })); +vi.mock("@/pages/account", () => ({ AccountPage: () => "Account page" })); +vi.mock("@/pages/dashboard", () => ({ + DashboardPage: () => "Dashboard page", +})); +vi.mock("@/pages/forgot-pass", () => ({ + ForgotPassPage: () => "Forgot password page", +})); +vi.mock("@/pages/login", () => ({ LoginPage: () => "Login page" })); +vi.mock("@/pages/register", () => ({ RegisterPage: () => "Register page" })); +vi.mock("@/pages/roles", () => ({ RolesPage: () => "Roles page" })); +vi.mock("@/pages/users", () => ({ UsersPage: () => "Users page" })); + +beforeEach(() => { + routeState.setCurrentRoute.mockClear(); + vi.resetModules(); +}); + +describe("application routes", () => { + it("renders the not-found page for an unknown route", async () => { + window.history.replaceState({}, "", "/unknown-route"); + const { Routes } = await import("./Routes"); + + render(); + + expect(await screen.findByText("404")).toBeInTheDocument(); + }); + + it.each([ + ["/", "Dashboard page", "root"], + ["/account", "Account page", "account"], + ["/users", "Users page", "users"], + ["/roles", "Roles page", "roles"], + ["/auth", "Login page", "auth"], + ["/auth/forgot-password", "Forgot password page", "forgotPassword"], + ["/auth/register", "Register page", "register"], + ])("renders the allowed route %s", async (path, content, route) => { + window.history.replaceState({}, "", path); + const { Routes } = await import("./Routes"); + + render(); + + expect(await screen.findByText(content)).toBeInTheDocument(); + expect(routeState.setCurrentRoute).toHaveBeenCalledWith(route); + }); + + it("redirects a disallowed route to the dashboard", async () => { + routeState.allowedRoutes.delete("users"); + window.history.replaceState({}, "", "/users"); + const { Routes } = await import("./Routes"); + + render(); + + expect(await screen.findByText("Dashboard page")).toBeInTheDocument(); + routeState.allowedRoutes.add("users"); + }); +}); diff --git a/src/Routes.tsx b/src/Routes.tsx index 0f6b648..81602ce 100644 --- a/src/Routes.tsx +++ b/src/Routes.tsx @@ -1,27 +1,60 @@ -import { FC, ReactNode, useEffect } from "react"; import { - createBrowserRouter, - Navigate, - RouterProvider, -} from "react-router-dom"; + FC, + Fragment, + lazy, + ReactElement, + ReactNode, + Suspense, + useEffect, +} from "react"; +import { createBrowserRouter, Navigate } from "react-router"; +import { RouterProvider } from "react-router/dom"; import { baseUrl, RouteKey, routeTree } from "@/config"; import { useAllowedRoutes, useCore } from "@/hooks"; -import { AuthLayout } from "@/layouts/Auth"; -import { DefaultLayout } from "@/layouts/Default"; -import { AccountPage } from "@/pages/Account"; -import { DashboardPage } from "@/pages/Dashboard"; -import { ForgotPassPage } from "@/pages/ForgotPass"; -import { LoginPage } from "@/pages/Login"; -import { NotFoundPage } from "@/pages/NotFound"; -import { RegisterPage } from "@/pages/Register"; -import { RolesPage } from "@/pages/Roles"; -import { UsersPage } from "@/pages/Users"; +import { NotFoundPage } from "@/pages/not-found"; + +const AccountPage = lazy(async () => { + const { AccountPage } = await import("@/pages/account"); + return { default: AccountPage }; +}); +const AuthLayout = lazy(async () => { + const { AuthLayout } = await import("@/layouts/auth"); + return { default: AuthLayout }; +}); +const DashboardPage = lazy(async () => { + const { DashboardPage } = await import("@/pages/dashboard"); + return { default: DashboardPage }; +}); +const DefaultLayout = lazy(async () => { + const { DefaultLayout } = await import("@/layouts/default"); + return { default: DefaultLayout }; +}); +const ForgotPassPage = lazy(async () => { + const { ForgotPassPage } = await import("@/pages/forgot-pass"); + return { default: ForgotPassPage }; +}); +const LoginPage = lazy(async () => { + const { LoginPage } = await import("@/pages/login"); + return { default: LoginPage }; +}); +const RegisterPage = lazy(async () => { + const { RegisterPage } = await import("@/pages/register"); + return { default: RegisterPage }; +}); +const RolesPage = lazy(async () => { + const { RolesPage } = await import("@/pages/roles"); + return { default: RolesPage }; +}); +const UsersPage = lazy(async () => { + const { UsersPage } = await import("@/pages/users"); + return { default: UsersPage }; +}); const RouteWrapper: FC<{ route: RouteKey; children: ReactNode }> = ({ route, children, -}) => { +}): ReactElement => { const { setCurrentRoute } = useCore(); const allowedRoutes = useAllowedRoutes(); @@ -29,20 +62,26 @@ const RouteWrapper: FC<{ route: RouteKey; children: ReactNode }> = ({ setCurrentRoute(route); }, [route, setCurrentRoute]); - if (allowedRoutes.has(route)) return children; + if (allowedRoutes.has(route)) return {children}; return ; }; const wrapRoute = (route: RouteKey, element: ReactNode) => ( - {element} + + {element} + ); const router = createBrowserRouter( [ { path: routeTree.auth.path, - element: , + element: ( + + + + ), children: [ { index: true, @@ -60,7 +99,11 @@ const router = createBrowserRouter( }, { path: routeTree.root.path, - element: , + element: ( + + + + ), children: [ { index: true, diff --git a/src/api/account.test.ts b/src/api/account.test.ts new file mode 100644 index 0000000..bb13073 --- /dev/null +++ b/src/api/account.test.ts @@ -0,0 +1,60 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + getAccount, + requestEmailVerification, + updateProfile, + updateUsername, + verifyEmail, +} from "./account"; +import { apiClient } from "./instance"; + +vi.mock("./instance", () => ({ + apiClient: { + get: vi.fn(), + patch: vi.fn(), + post: vi.fn(), + }, +})); + +beforeEach(() => { + vi.mocked(apiClient.get).mockResolvedValue({}); + vi.mocked(apiClient.patch).mockResolvedValue({}); + vi.mocked(apiClient.post).mockResolvedValue({}); +}); + +describe("account API", () => { + it("uses the account endpoint", async () => { + await getAccount(); + + expect(apiClient.get).toHaveBeenCalledWith("/account/me"); + }); + + it("requests and verifies email changes", async () => { + await requestEmailVerification({ email: "ada@example.com" }); + await verifyEmail({ email: "ada@example.com", otp: "123456" }); + + expect(apiClient.post).toHaveBeenNthCalledWith( + 1, + "/account/request-email-verification", + { email: "ada@example.com" }, + ); + expect(apiClient.post).toHaveBeenNthCalledWith(2, "/account/verify-email", { + email: "ada@example.com", + otp: "123456", + }); + }); + + it("updates profile and username fields", async () => { + await updateProfile({ firstName: "Ada", lastName: "Lovelace" }); + await updateUsername({ username: "ada" }); + + expect(apiClient.patch).toHaveBeenCalledWith("/account/update-profile", { + firstName: "Ada", + lastName: "Lovelace", + }); + expect(apiClient.post).toHaveBeenCalledWith("/account/update-username", { + username: "ada", + }); + }); +}); diff --git a/src/api/auth.test.ts b/src/api/auth.test.ts new file mode 100644 index 0000000..a23556c --- /dev/null +++ b/src/api/auth.test.ts @@ -0,0 +1,93 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + changePassword, + forgotPassword, + login, + logout, + register, + requestOtp, +} from "./auth"; +import { apiClient } from "./instance"; +import { clearAccessToken, setAccessToken } from "./token"; + +vi.mock("./instance", () => ({ + apiClient: { + post: vi.fn(), + }, +})); + +vi.mock("./token", () => ({ + clearAccessToken: vi.fn(), + setAccessToken: vi.fn(), +})); + +const currentCredential = "current-test-credential"; +const replacementCredential = "replacement-test-credential"; +const syntheticCredential = "synthetic-test-credential"; + +beforeEach(() => { + vi.mocked(apiClient.post).mockResolvedValue({}); +}); + +describe("authentication API", () => { + it("stores access tokens returned by login and registration", async () => { + vi.mocked(apiClient.post) + .mockResolvedValueOnce({ accessToken: "login-token" }) + .mockResolvedValueOnce({ accessToken: "register-token" }); + + await expect( + login({ identifier: "ada", password: syntheticCredential }), + ).resolves.toEqual({ accessToken: "login-token" }); + await expect( + register({ + firstName: "Ada", + lastName: "Lovelace", + mobile: "09120000000", + otp: "123456", + password: syntheticCredential, + }), + ).resolves.toEqual({ accessToken: "register-token" }); + + expect(setAccessToken).toHaveBeenNthCalledWith(1, "login-token"); + expect(setAccessToken).toHaveBeenNthCalledWith(2, "register-token"); + }); + + it("clears the access token when logout succeeds or fails", async () => { + await logout(); + + vi.mocked(apiClient.post).mockRejectedValueOnce(new Error("offline")); + await expect(logout()).rejects.toThrow("offline"); + + expect(apiClient.post).toHaveBeenNthCalledWith(1, "/auth/logout"); + expect(apiClient.post).toHaveBeenNthCalledWith(2, "/auth/logout"); + expect(clearAccessToken).toHaveBeenCalledTimes(2); + }); + + it("submits password and OTP operations to their endpoints", async () => { + await changePassword({ + currentPassword: currentCredential, + newPassword: replacementCredential, + }); + await forgotPassword({ + mobile: "09120000000", + otp: "123456", + password: replacementCredential, + }); + await requestOtp({ mobile: "09120000000", purpose: "login" }); + + expect(apiClient.post).toHaveBeenNthCalledWith(1, "/auth/change-password", { + currentPassword: currentCredential, + newPassword: replacementCredential, + }); + expect(apiClient.post).toHaveBeenNthCalledWith(2, "/auth/forgot-password", { + mobile: "09120000000", + otp: "123456", + password: replacementCredential, + }); + expect(apiClient.post).toHaveBeenNthCalledWith(3, "/auth/otp-request", { + mobile: "09120000000", + purpose: "login", + }); + }); +}); diff --git a/src/api/instance.test.ts b/src/api/instance.test.ts new file mode 100644 index 0000000..c5cb244 --- /dev/null +++ b/src/api/instance.test.ts @@ -0,0 +1,288 @@ +import axios, { AxiosError } from "axios"; +import { delay, http, HttpResponse } from "msw"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { i18nInstance } from "@/i18n"; +import { server } from "@/test/mocks/server"; + +import { apiClient, setUnauthorizedHandler } from "./instance"; +import { clearAccessToken, getAccessToken, setAccessToken } from "./token"; + +const syntheticCredential = "synthetic-test-credential"; + +afterEach(() => { + clearAccessToken(); + setUnauthorizedHandler(null); +}); + +describe("API client authentication", () => { + it("adds the access token to protected requests", async () => { + setAccessToken("access-token"); + server.use( + http.get("http://localhost/protected", ({ request }) => { + expect(request.headers.get("authorization")).toBe( + "Bearer access-token", + ); + + return HttpResponse.json({ user_id: "user-1" }); + }), + ); + + await expect( + apiClient.get<{ userId: string }>("/protected"), + ).resolves.toEqual({ userId: "user-1" }); + }); + + it("refreshes an expired token and retries the request once", async () => { + let protectedRequestCount = 0; + let refreshRequestCount = 0; + + setAccessToken("expired-token"); + server.use( + http.get("http://localhost/protected", ({ request }) => { + protectedRequestCount += 1; + + if (request.headers.get("authorization") === "Bearer fresh-token") { + return HttpResponse.json({ status: "ready" }); + } + + return HttpResponse.json({ message: "Unauthorized" }, { status: 401 }); + }), + http.post("http://localhost/auth/refresh-token", () => { + refreshRequestCount += 1; + return HttpResponse.json({ access_token: "fresh-token" }); + }), + ); + + await expect( + apiClient.get<{ status: string }>("/protected"), + ).resolves.toEqual({ status: "ready" }); + expect(getAccessToken()).toBe("fresh-token"); + expect(protectedRequestCount).toBe(2); + expect(refreshRequestCount).toBe(1); + }); + + it("shares one token refresh across concurrent unauthorized requests", async () => { + let refreshRequestCount = 0; + + setAccessToken("expired-token"); + server.use( + http.get("http://localhost/protected/:id", ({ request, params }) => { + if (request.headers.get("authorization") === "Bearer fresh-token") { + return HttpResponse.json({ id: params.id }); + } + + return HttpResponse.json({ message: "Unauthorized" }, { status: 401 }); + }), + http.post("http://localhost/auth/refresh-token", async () => { + refreshRequestCount += 1; + await delay(10); + return HttpResponse.json({ access_token: "fresh-token" }); + }), + ); + + await expect( + Promise.all([ + apiClient.get<{ id: string }>("/protected/one"), + apiClient.get<{ id: string }>("/protected/two"), + ]), + ).resolves.toEqual([{ id: "one" }, { id: "two" }]); + expect(refreshRequestCount).toBe(1); + }); + + it("clears authentication once when a shared token refresh fails", async () => { + const onUnauthorized = vi.fn(); + let refreshRequestCount = 0; + + setAccessToken("expired-token"); + setUnauthorizedHandler(onUnauthorized); + server.use( + http.get("http://localhost/protected/:id", () => + HttpResponse.json({ message: "Unauthorized" }, { status: 401 }), + ), + http.post("http://localhost/auth/refresh-token", async () => { + refreshRequestCount += 1; + await delay(10); + return HttpResponse.json( + { message: "Refresh rejected" }, + { status: 401 }, + ); + }), + ); + + const results = await Promise.allSettled([ + apiClient.get("/protected/one"), + apiClient.get("/protected/two"), + ]); + + expect(results.every(({ status }) => status === "rejected")).toBe(true); + expect(getAccessToken()).toBeNull(); + expect(onUnauthorized).toHaveBeenCalledOnce(); + expect(refreshRequestCount).toBe(1); + }); + + it("clears authentication when a single token refresh fails", async () => { + const onUnauthorized = vi.fn(); + let protectedRequestCount = 0; + let refreshRequestCount = 0; + + setAccessToken("expired-token"); + setUnauthorizedHandler(onUnauthorized); + server.use( + http.get("http://localhost/protected", () => { + protectedRequestCount += 1; + return HttpResponse.json({ message: "Unauthorized" }, { status: 401 }); + }), + http.post("http://localhost/auth/refresh-token", () => { + refreshRequestCount += 1; + return HttpResponse.json( + { message: "Refresh rejected" }, + { status: 401 }, + ); + }), + ); + + await expect(apiClient.get("/protected")).rejects.toThrow("Unauthorized"); + expect(protectedRequestCount).toBe(1); + expect(refreshRequestCount).toBe(1); + expect(getAccessToken()).toBeNull(); + expect(onUnauthorized).toHaveBeenCalledOnce(); + }); + + it("does not start another refresh when the retried request is unauthorized", async () => { + const onUnauthorized = vi.fn(); + let protectedRequestCount = 0; + let refreshRequestCount = 0; + + setAccessToken("expired-token"); + setUnauthorizedHandler(onUnauthorized); + server.use( + http.get("http://localhost/protected", () => { + protectedRequestCount += 1; + return HttpResponse.json({ message: "Unauthorized" }, { status: 401 }); + }), + http.post("http://localhost/auth/refresh-token", () => { + refreshRequestCount += 1; + return HttpResponse.json({ access_token: "fresh-token" }); + }), + ); + + await expect(apiClient.get("/protected")).rejects.toThrow("Unauthorized"); + expect(protectedRequestCount).toBe(2); + expect(refreshRequestCount).toBe(1); + expect(getAccessToken()).toBeNull(); + expect(onUnauthorized).toHaveBeenCalledOnce(); + }); + + it("does not refresh or clear authentication for public auth requests", async () => { + const onUnauthorized = vi.fn(); + const refresh = vi.fn(); + + setUnauthorizedHandler(onUnauthorized); + server.use( + http.post("http://localhost/auth/login", () => + HttpResponse.json({ message: "Invalid credentials" }, { status: 401 }), + ), + http.post("http://localhost/auth/refresh-token", refresh), + ); + + await expect( + apiClient.post("/auth/login", { + identifier: "ada", + password: syntheticCredential, + }), + ).rejects.toThrow("Invalid credentials"); + expect(refresh).not.toHaveBeenCalled(); + expect(onUnauthorized).not.toHaveBeenCalled(); + }); +}); + +describe("API client errors", () => { + it("transforms structured API errors and preserves their cause", async () => { + server.use( + http.get("http://localhost/invalid-request", () => + HttpResponse.json( + { + cause: { email: "Email is invalid" }, + message: "Validation failed", + }, + { status: 422 }, + ), + ), + ); + + await expect(apiClient.get("/invalid-request")).rejects.toMatchObject({ + cause: { email: "Email is invalid" }, + message: "Validation failed", + }); + }); + + it.each([ + ["null", null], + ["primitive", "failure"], + ["array", []], + ["missing message", {}], + ["empty message", { message: "" }], + ["non-string message", { message: 500 }], + ])( + "uses the unexpected-error message for a %s response body", + async (label, body) => { + const path = `/malformed-error/${label.replaceAll(" ", "-")}`; + server.use( + http.get(`http://localhost${path}`, () => + HttpResponse.json(body, { status: 500 }), + ), + ); + + await expect(apiClient.get(path)).rejects.toThrow( + i18nInstance.t("unexpectedError"), + ); + }, + ); + + it("preserves request cancellation", async () => { + const controller = new AbortController(); + + server.use( + http.get("http://localhost/cancelled", async () => { + await delay(100); + return HttpResponse.json({ status: "ready" }); + }), + ); + + const request = apiClient.get("/cancelled", { + signal: controller.signal, + }); + controller.abort(); + + await expect(request).rejects.toSatisfy((error: unknown) => + axios.isCancel(error), + ); + }); + + it("uses the network-error message when a request times out", async () => { + await expect( + apiClient.get("/slow", { + adapter: (config) => + Promise.reject( + new AxiosError( + "timeout exceeded", + AxiosError.ETIMEDOUT, + config, + {}, + ), + ), + }), + ).rejects.toThrow(i18nInstance.t("networkError")); + }); + + it("uses the network-error message for network failures", async () => { + server.use( + http.get("http://localhost/network-failure", () => HttpResponse.error()), + ); + + await expect(apiClient.get("/network-failure")).rejects.toThrow( + i18nInstance.t("networkError"), + ); + }); +}); diff --git a/src/api/instance.ts b/src/api/instance.ts index 08419d4..c74d0a3 100644 --- a/src/api/instance.ts +++ b/src/api/instance.ts @@ -1,6 +1,12 @@ -import axios, { AxiosRequestConfig, InternalAxiosRequestConfig } from "axios"; +import axios, { + AxiosError, + AxiosRequestConfig, + AxiosResponse, + InternalAxiosRequestConfig, +} from "axios"; import { apiUrl } from "@/config"; +import { i18nInstance } from "@/i18n"; import { AccessTokenProps } from "@/types"; import { toCamelCase, toSnakeCase } from "@/utils"; @@ -23,6 +29,23 @@ type RetryableRequestConfig = InternalAxiosRequestConfig & { _retried?: boolean; }; +type ApiErrorBody = { + cause?: unknown; + message: string; +}; + +const isApiErrorBody = (value: unknown): value is ApiErrorBody => { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + return false; + } + + return ( + "message" in value && + typeof value.message === "string" && + value.message.length > 0 + ); +}; + const handleUnauthorized = () => { if (isHandlingUnauthorized) return; @@ -53,6 +76,37 @@ const refreshAccessToken = (): Promise => { return refreshPromise; }; +const retryUnauthorizedRequest = async ( + error: AxiosError, +): Promise | undefined> => { + if (error.response?.status !== 401) return; + + const requestConfig = error.config as RetryableRequestConfig | undefined; + const isPublicAuthRequest = publicAuthUrls.has(requestConfig?.url ?? ""); + + if (!requestConfig || requestConfig._retried || isPublicAuthRequest) { + if (!isPublicAuthRequest) handleUnauthorized(); + return; + } + + requestConfig._retried = true; + + try { + await refreshAccessToken(); + return api.request(requestConfig); + } catch { + handleUnauthorized(); + } +}; + +const createResponseError = (data: unknown): Error => { + if (!isApiErrorBody(data)) { + return new Error(i18nInstance.t("unexpectedError")); + } + + return new Error(data.message, { cause: data.cause }); +}; + api.interceptors.request.use( (config) => { const authToken = getAccessToken(); @@ -72,50 +126,25 @@ api.interceptors.request.use( api.interceptors.response.use( (response) => response, async (error) => { - if ( - axios.isAxiosError<{ errors: Record; message: string }>( - error, - ) - ) { - if (error.response?.status === 401) { - const requestConfig = error.config as - RetryableRequestConfig | undefined; - const isPublicAuthRequest = publicAuthUrls.has( - requestConfig?.url ?? "", - ); - const canRefresh = - requestConfig && !requestConfig._retried && !isPublicAuthRequest; - - if (canRefresh) { - requestConfig._retried = true; - - try { - await refreshAccessToken(); - return api.request(requestConfig); - } catch { - handleUnauthorized(); - } - } else if (!isPublicAuthRequest) { - handleUnauthorized(); - } - } - - if (error.response) { - return Promise.reject( - new Error(error.response.data.message, { - cause: error.response.data.errors, - }), - ); - } - - if (error.request) { - return Promise.reject( - new Error("Network error - please check your connection"), - ); - } + if (axios.isCancel(error)) throw error; + + if (!axios.isAxiosError(error)) { + throw new Error(i18nInstance.t("unexpectedError")); + } + + const retryResponse = await retryUnauthorizedRequest(error); + + if (retryResponse) return retryResponse; + + if (error.response) { + throw createResponseError(error.response.data); + } + + if (error.request) { + throw new Error(i18nInstance.t("networkError")); } - return Promise.reject(error); + throw new Error(i18nInstance.t("unexpectedError")); }, ); diff --git a/src/api/role.test.ts b/src/api/role.test.ts new file mode 100644 index 0000000..8573b3e --- /dev/null +++ b/src/api/role.test.ts @@ -0,0 +1,51 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { apiClient } from "./instance"; +import { + createRole, + deleteRole, + fetchPermissions, + fetchRole, + fetchRoles, + updateRole, +} from "./role"; + +vi.mock("./instance", () => ({ + apiClient: { + del: vi.fn(), + get: vi.fn(), + patch: vi.fn(), + post: vi.fn(), + }, +})); + +beforeEach(() => { + vi.mocked(apiClient.del).mockResolvedValue(undefined); + vi.mocked(apiClient.get).mockResolvedValue([]); + vi.mocked(apiClient.patch).mockResolvedValue({}); + vi.mocked(apiClient.post).mockResolvedValue({}); +}); + +describe("roles API", () => { + it("creates, updates, and deletes roles", async () => { + const params = { name: "Operators", permissions: ["users.read"] }; + + await createRole(params); + await updateRole("role-1", params); + await deleteRole("role-1"); + + expect(apiClient.post).toHaveBeenCalledWith("/roles", params); + expect(apiClient.patch).toHaveBeenCalledWith("/roles/role-1", params); + expect(apiClient.del).toHaveBeenCalledWith("/roles/role-1"); + }); + + it("loads role collections, details, and permissions", async () => { + await fetchRoles(); + await fetchRole("role-1"); + await fetchPermissions(); + + expect(apiClient.get).toHaveBeenNthCalledWith(1, "/roles"); + expect(apiClient.get).toHaveBeenNthCalledWith(2, "/roles/role-1"); + expect(apiClient.get).toHaveBeenNthCalledWith(3, "/permissions"); + }); +}); diff --git a/src/api/token.test.ts b/src/api/token.test.ts new file mode 100644 index 0000000..e0fd9a9 --- /dev/null +++ b/src/api/token.test.ts @@ -0,0 +1,27 @@ +import { beforeEach, describe, expect, it } from "vitest"; + +import { clearAccessToken, getAccessToken, setAccessToken } from "./token"; + +beforeEach(() => { + clearAccessToken(); +}); + +describe("access token", () => { + it("is absent before a token is set", () => { + expect(getAccessToken()).toBeNull(); + }); + + it("stores and retrieves a token", () => { + setAccessToken("access-token"); + + expect(getAccessToken()).toBe("access-token"); + }); + + it("clears a stored token", () => { + setAccessToken("access-token"); + + clearAccessToken(); + + expect(getAccessToken()).toBeNull(); + }); +}); diff --git a/src/api/user.test.ts b/src/api/user.test.ts index 3fd8a13..7b96bfd 100644 --- a/src/api/user.test.ts +++ b/src/api/user.test.ts @@ -1,10 +1,15 @@ import { http, HttpResponse } from "msw"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; +import { i18nInstance } from "@/i18n"; import { server } from "@/test/mocks/server"; import { fetchUsers } from "./user"; +vi.mock("@/i18n", () => ({ + i18nInstance: { t: (key: string) => key }, +})); + describe("users API", () => { it("serializes filters and converts the response to camel case", async () => { server.use( @@ -50,4 +55,24 @@ describe("users API", () => { total: 1, }); }); + + it("localizes network errors", async () => { + server.use(http.get("http://localhost/users", () => HttpResponse.error())); + + await expect(fetchUsers({})).rejects.toThrow( + i18nInstance.t("networkError"), + ); + }); + + it("localizes unexpected API responses", async () => { + server.use( + http.get("http://localhost/users", () => + HttpResponse.json(null, { status: 500 }), + ), + ); + + await expect(fetchUsers({})).rejects.toThrow( + i18nInstance.t("unexpectedError"), + ); + }); }); diff --git a/src/assets/fonts/vazirmatn/OFL.txt b/src/assets/fonts/vazirmatn/OFL.txt new file mode 100644 index 0000000..be66b38 --- /dev/null +++ b/src/assets/fonts/vazirmatn/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2015 The Vazirmatn Project Authors (https://github.com/rastikerdar/vazirmatn) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/assets/fonts/vazirmatn/Vazirmatn-Variable.woff2 b/src/assets/fonts/vazirmatn/Vazirmatn-Variable.woff2 new file mode 100644 index 0000000..a501289 Binary files /dev/null and b/src/assets/fonts/vazirmatn/Vazirmatn-Variable.woff2 differ diff --git a/src/components/DigitsInput.test.tsx b/src/components/digits-input/DigitsInput.test.tsx similarity index 93% rename from src/components/DigitsInput.test.tsx rename to src/components/digits-input/DigitsInput.test.tsx index f3f61ca..165d8d5 100644 --- a/src/components/DigitsInput.test.tsx +++ b/src/components/digits-input/DigitsInput.test.tsx @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { render, screen } from "@/test/render"; -import { DigitsInput } from "./DigitsInput"; +import { DigitsInput } from "./"; const DigitsInputHarness = () => { const [value, setValue] = useState(""); diff --git a/src/components/DigitsInput.tsx b/src/components/digits-input/DigitsInput.tsx similarity index 73% rename from src/components/DigitsInput.tsx rename to src/components/digits-input/DigitsInput.tsx index 897b810..91eb97e 100644 --- a/src/components/DigitsInput.tsx +++ b/src/components/digits-input/DigitsInput.tsx @@ -1,7 +1,9 @@ import { Input, InputProps } from "antd"; import { FC } from "react"; -export const DigitsInput: FC = ({ onChange, ...rest }) => ( +type DigitsInputProps = InputProps; + +export const DigitsInput: FC = ({ onChange, ...rest }) => ( { if (onChange) { diff --git a/src/components/digits-input/index.ts b/src/components/digits-input/index.ts new file mode 100644 index 0000000..8a1450e --- /dev/null +++ b/src/components/digits-input/index.ts @@ -0,0 +1 @@ +export { DigitsInput } from "./DigitsInput"; diff --git a/src/components/error-boundary/ErrorBoundary.test.tsx b/src/components/error-boundary/ErrorBoundary.test.tsx new file mode 100644 index 0000000..6068ddf --- /dev/null +++ b/src/components/error-boundary/ErrorBoundary.test.tsx @@ -0,0 +1,57 @@ +import { render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { i18nInstance } from "@/i18n"; +import { setObservabilityTransport } from "@/utils"; + +import { ErrorBoundary } from "./"; + +const BrokenView = () => { + throw new Error("render failed"); +}; + +afterEach(() => { + setObservabilityTransport(); + vi.restoreAllMocks(); +}); + +describe("ErrorBoundary", () => { + it("renders healthy children", () => { + render( + +

Ready

+
, + ); + + expect(screen.getByText("Ready")).toBeInTheDocument(); + }); + + it("reports render failures and offers recovery", async () => { + await i18nInstance.changeLanguage("en"); + vi.spyOn(console, "error").mockImplementation(() => undefined); + const send = vi.fn(); + setObservabilityTransport(send); + + render( + + + , + ); + + expect( + screen.getByRole("heading", { + name: "An unexpected error occurred.", + }), + ).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Reload" })).toBeInTheDocument(); + expect(send).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + source: "react.error-boundary", + }), + message: "render failed", + name: "error", + }), + ); + }); +}); diff --git a/src/components/error-boundary/ErrorBoundary.tsx b/src/components/error-boundary/ErrorBoundary.tsx new file mode 100644 index 0000000..03f1a57 --- /dev/null +++ b/src/components/error-boundary/ErrorBoundary.tsx @@ -0,0 +1,56 @@ +import { + Component, + type ErrorInfo, + Fragment, + type ReactElement, + type ReactNode, +} from "react"; + +import { i18nInstance } from "@/i18n"; +import { reportError } from "@/utils"; + +type ErrorBoundaryProps = { + children: ReactNode; +}; + +type State = { + hasError: boolean; +}; + +export class ErrorBoundary extends Component { + public state: State = { hasError: false }; + + public static getDerivedStateFromError(): State { + return { hasError: true }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + reportError(error, { + componentStack: errorInfo.componentStack, + source: "react.error-boundary", + }); + } + + public render(): ReactElement { + if (!this.state.hasError) { + return {this.props.children}; + } + + return ( +
+

{i18nInstance.t("unexpectedError")}

+ +
+ ); + } +} diff --git a/src/components/error-boundary/index.ts b/src/components/error-boundary/index.ts new file mode 100644 index 0000000..8dd2689 --- /dev/null +++ b/src/components/error-boundary/index.ts @@ -0,0 +1 @@ +export { ErrorBoundary } from "./ErrorBoundary"; diff --git a/src/components/form-drawer/FormDrawer.tsx b/src/components/form-drawer/FormDrawer.tsx new file mode 100644 index 0000000..10949a8 --- /dev/null +++ b/src/components/form-drawer/FormDrawer.tsx @@ -0,0 +1,49 @@ +import { Button, Drawer, Space } from "antd"; +import { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; + +type FormDrawerProps = { + afterOpenChange?: (open: boolean) => void; + children: ReactNode; + onClose: () => void; + onSubmit: () => void; + open: boolean; + submitting: boolean; + title: ReactNode; +}; + +export const FormDrawer = ({ + afterOpenChange, + children, + onClose, + onSubmit, + open, + submitting, + title, +}: FormDrawerProps) => { + const { t } = useTranslation(); + + return ( + + + + + } + mask={{ closable: false }} + onClose={onClose} + open={open} + styles={{ footer: { textAlign: "end" } }} + title={title} + > + {children} + + ); +}; diff --git a/src/components/form-drawer/index.ts b/src/components/form-drawer/index.ts new file mode 100644 index 0000000..61b16ca --- /dev/null +++ b/src/components/form-drawer/index.ts @@ -0,0 +1 @@ +export * from "./FormDrawer"; diff --git a/src/components/Icon.tsx b/src/components/icon/Icon.tsx similarity index 76% rename from src/components/Icon.tsx rename to src/components/icon/Icon.tsx index 047b924..061e69a 100644 --- a/src/components/Icon.tsx +++ b/src/components/icon/Icon.tsx @@ -17,10 +17,12 @@ const iconMap = { type IconName = keyof typeof iconMap; -export const Icon: FC<{ name: IconName; size?: number }> = ({ - name, - size = 16, -}) => ( +type IconProps = { + name: IconName; + size?: number; +}; + +export const Icon: FC = ({ name, size = 16 }) => ( {iconMap[name]} diff --git a/src/components/icon/index.ts b/src/components/icon/index.ts new file mode 100644 index 0000000..65d727b --- /dev/null +++ b/src/components/icon/index.ts @@ -0,0 +1 @@ +export { Icon } from "./Icon"; diff --git a/src/components/otp-input/OtpInput.test.tsx b/src/components/otp-input/OtpInput.test.tsx new file mode 100644 index 0000000..a6510e5 --- /dev/null +++ b/src/components/otp-input/OtpInput.test.tsx @@ -0,0 +1,15 @@ +import { render } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { OtpInput } from "./"; + +describe("OtpInput", () => { + it("does not forward aria-required to the OTP group", () => { + const { container } = render( + , + ); + + expect(container.querySelectorAll("input")).toHaveLength(4); + expect(container.querySelector("[aria-required]")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/otp-input/OtpInput.tsx b/src/components/otp-input/OtpInput.tsx new file mode 100644 index 0000000..601c6a0 --- /dev/null +++ b/src/components/otp-input/OtpInput.tsx @@ -0,0 +1,10 @@ +import { Input } from "antd"; +import { ComponentProps, FC } from "react"; + +const Otp = Input.OTP; + +type OtpInputProps = ComponentProps; + +export const OtpInput: FC = (props) => { + return ; +}; diff --git a/src/components/otp-input/index.ts b/src/components/otp-input/index.ts new file mode 100644 index 0000000..053decd --- /dev/null +++ b/src/components/otp-input/index.ts @@ -0,0 +1 @@ +export { OtpInput } from "./OtpInput"; diff --git a/src/components/password-fields/PasswordFields.tsx b/src/components/password-fields/PasswordFields.tsx new file mode 100644 index 0000000..eaaca82 --- /dev/null +++ b/src/components/password-fields/PasswordFields.tsx @@ -0,0 +1,51 @@ +import { Form, Input } from "antd"; +import { useTranslation } from "react-i18next"; + +type PasswordFieldsProps = { + passwordLabel?: "newPass" | "password"; + size?: "large"; +}; + +export const PasswordFields = ({ + passwordLabel = "password", + size, +}: PasswordFieldsProps) => { + const { t } = useTranslation(); + + return ( + <> + + + + ({ + validator(_, value) { + return !value || getFieldValue("password") === value + ? Promise.resolve() + : Promise.reject(new Error(t("passsMismatch"))); + }, + }), + ]} + > + + + + ); +}; diff --git a/src/components/password-fields/index.ts b/src/components/password-fields/index.ts new file mode 100644 index 0000000..4bf905b --- /dev/null +++ b/src/components/password-fields/index.ts @@ -0,0 +1 @@ +export * from "./PasswordFields"; diff --git a/src/forms/UserPassword.tsx b/src/forms/UserPassword.tsx deleted file mode 100644 index 98c68a6..0000000 --- a/src/forms/UserPassword.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* eslint-disable react-hooks/set-state-in-effect */ -import { Button, Drawer, Form, FormProps, Input, Space } from "antd"; -import { FC, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useLocation } from "react-router-dom"; - -import { updateUserPassword } from "@/api"; -import { modalKeys } from "@/config"; -import { useAntd, useGoBack } from "@/hooks"; -import { UserPasswordParams, UserProps } from "@/types"; - -type UserPasswordFormParams = UserPasswordParams & { - confirmPassword: string; -}; - -export const UserPasswordForm: FC<{ data?: UserProps }> = ({ data }) => { - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - const [submitting, setSubmitting] = useState(false); - const { messageAPI } = useAntd(); - const { hash } = useLocation(); - const [form] = Form.useForm(); - const goBack = useGoBack(); - - const handleSubmit: FormProps["onFinish"] = async ({ - password, - }) => { - if (submitting || !data) return; - - try { - setSubmitting(true); - await updateUserPassword(data.id, { password }); - messageAPI.success(t("passwordUpdated")); - goBack(); - } catch (error) { - if (error instanceof Error) messageAPI.error(error.message); - else console.error(error); - } finally { - setSubmitting(false); - } - }; - - useEffect(() => { - if (hash === modalKeys.password) { - if (data) setOpen(true); - else goBack(); - } else { - if (open) form.resetFields(); - setOpen(false); - setSubmitting(false); - } - }, [data, form, goBack, hash, open]); - - return ( - - - - - } - mask={{ closable: false }} - onClose={() => goBack()} - open={open} - styles={{ footer: { textAlign: "end" } }} - title={t("changePassword")} - > - - form={form} - layout="vertical" - onFinish={handleSubmit} - > - - - - ({ - validator(_, value) { - if (!value || getFieldValue("password") === value) { - return Promise.resolve(); - } - - return Promise.reject(new Error(t("passsMismatch"))); - }, - }), - ]} - > - - - - - ); -}; diff --git a/src/forms/forms.test.tsx b/src/forms/forms.test.tsx new file mode 100644 index 0000000..2e8a06f --- /dev/null +++ b/src/forms/forms.test.tsx @@ -0,0 +1,254 @@ +import { waitFor } from "@testing-library/react"; +import { MemoryRouter } from "react-router"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import * as api from "@/api"; +import { render, screen } from "@/test/render"; +import { UserProps } from "@/types"; + +import { RoleForm } from "./role"; +import { UserForm } from "./user"; +import { UserPasswordForm } from "./user-password"; +import { UserFormRole } from "./user-role"; +import { UserWorkspaceForm } from "./user-workspace"; + +const mocks = vi.hoisted(() => ({ + goBack: vi.fn(), + messageError: vi.fn(), + messageSuccess: vi.fn(), +})); + +vi.mock("@/api", () => ({ + createRole: vi.fn(), + createUser: vi.fn(), + updateRole: vi.fn(), + updateUser: vi.fn(), + updateUserPassword: vi.fn(), + updateUserRoles: vi.fn(), + updateUserWorkspaces: vi.fn(), +})); + +vi.mock("@/hooks", () => ({ + useAntd: () => ({ + messageAPI: { + error: mocks.messageError, + success: mocks.messageSuccess, + }, + }), + useGoBack: () => mocks.goBack, +})); + +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), + useTranslation: () => ({ t: (key: string) => key }), +})); + +const userData: UserProps = { + email: "ada@example.com", + firstName: "Ada", + id: "user-1", + isSystemAdmin: false, + lastName: "Lovelace", + mobile: "09120000000", + personnelCode: "1001", + roleIds: ["role-1"], + username: "ada", + workspaceIds: ["workspace-1"], +}; +const mismatchedCredential = "mismatched-test-credential"; +const replacementCredential = "replacement-test-credential"; +const syntheticCredential = "synthetic-test-credential"; + +const renderAtHash = (ui: React.ReactNode, hash: string) => + render({ui}); + +beforeEach(() => { + vi.mocked(api.createRole).mockResolvedValue({ + id: "role-1", + name: "Operators", + permissions: ["users.read"], + }); + vi.mocked(api.createUser).mockResolvedValue(userData); + vi.mocked(api.updateRole).mockResolvedValue({ + id: "role-1", + name: "Operators", + permissions: ["users.read"], + }); + vi.mocked(api.updateUser).mockResolvedValue(userData); + vi.mocked(api.updateUserPassword).mockResolvedValue(undefined); + vi.mocked(api.updateUserRoles).mockResolvedValue(undefined); + vi.mocked(api.updateUserWorkspaces).mockResolvedValue(undefined); +}); + +describe("management forms", () => { + it("validates and creates a user", async () => { + const onFinish = vi.fn(); + const { user } = renderAtHash(, "#create"); + + await user.type(await screen.findByLabelText("firstName"), "Ada"); + await user.type(screen.getByLabelText("lastName"), "Lovelace"); + await user.type(screen.getByLabelText("mobile"), "09120000000"); + await user.type(screen.getByLabelText("personnelCode"), "1001"); + await user.type(screen.getByLabelText("password"), syntheticCredential); + await user.type(screen.getByLabelText("confirmPass"), syntheticCredential); + await user.click(screen.getByRole("button", { name: "submit" })); + + await waitFor(() => + expect(api.createUser).toHaveBeenCalledWith({ + firstName: "Ada", + lastName: "Lovelace", + mobile: "09120000000", + password: syntheticCredential, + personnelCode: "1001", + }), + ); + expect(mocks.messageSuccess).toHaveBeenCalledWith("userCreated"); + expect(mocks.goBack).toHaveBeenCalledOnce(); + expect(onFinish).toHaveBeenCalledOnce(); + }); + + it("rejects mismatched user passwords", async () => { + const { user } = renderAtHash(, "#create"); + + await user.type(await screen.findByLabelText("firstName"), "Ada"); + await user.type(screen.getByLabelText("lastName"), "Lovelace"); + await user.type(screen.getByLabelText("mobile"), "09120000000"); + await user.type(screen.getByLabelText("personnelCode"), "1001"); + await user.type(screen.getByLabelText("password"), syntheticCredential); + await user.type(screen.getByLabelText("confirmPass"), mismatchedCredential); + await user.click(screen.getByRole("button", { name: "submit" })); + + expect(await screen.findByText("passsMismatch")).toBeInTheDocument(); + expect(api.createUser).not.toHaveBeenCalled(); + }); + + it("updates a user without submitting password fields", async () => { + const onFinish = vi.fn(); + const { user } = renderAtHash( + , + "#update", + ); + + const firstName = await screen.findByLabelText("firstName"); + await user.clear(firstName); + await user.type(firstName, "Augusta"); + await user.click(screen.getByRole("button", { name: "submit" })); + + await waitFor(() => + expect(api.updateUser).toHaveBeenCalledWith("user-1", { + firstName: "Augusta", + lastName: "Lovelace", + mobile: "09120000000", + personnelCode: "1001", + }), + ); + expect(screen.queryByLabelText("password")).not.toBeInTheDocument(); + expect(mocks.messageSuccess).toHaveBeenCalledWith("userUpdated"); + expect(onFinish).toHaveBeenCalledOnce(); + }); + + it("creates a role with selected permissions and reports failures", async () => { + vi.mocked(api.createRole).mockRejectedValueOnce( + new Error("Role already exists"), + ); + const { user } = renderAtHash( + , + "#create", + ); + + await user.type(await screen.findByLabelText("name"), "Operators"); + await user.click(screen.getByLabelText("Read users")); + await user.click(screen.getByRole("button", { name: "submit" })); + + await waitFor(() => + expect(api.createRole).toHaveBeenCalledWith({ + name: "Operators", + permissions: ["users.read"], + }), + ); + expect(mocks.messageError).toHaveBeenCalledWith("Role already exists"); + }); + + it("updates a user's password", async () => { + const { user } = renderAtHash( + , + "#password", + ); + + await user.type( + await screen.findByLabelText("newPass"), + replacementCredential, + ); + await user.type( + screen.getByLabelText("confirmPass"), + replacementCredential, + ); + await user.click(screen.getByRole("button", { name: "submit" })); + + await waitFor(() => + expect(api.updateUserPassword).toHaveBeenCalledWith("user-1", { + password: replacementCredential, + }), + ); + expect(mocks.messageSuccess).toHaveBeenCalledWith("passwordUpdated"); + }); + + it("submits the user's current roles", async () => { + const onFinish = vi.fn(); + const { user } = renderAtHash( + , + "#roles", + ); + + await screen.findByText("Operators"); + await user.click(screen.getByRole("button", { name: "submit" })); + + await waitFor(() => + expect(api.updateUserRoles).toHaveBeenCalledWith("user-1", { + roleIds: ["role-1"], + }), + ); + expect(mocks.messageSuccess).toHaveBeenCalledWith("rolesUpdated"); + expect(onFinish).toHaveBeenCalledOnce(); + }); + + it("submits the user's current workspaces", async () => { + const onFinish = vi.fn(); + const { user } = renderAtHash( + , + "#workspaces", + ); + + await screen.findByText("Analytical Engine"); + await user.click(screen.getByRole("button", { name: "submit" })); + + await waitFor(() => + expect(api.updateUserWorkspaces).toHaveBeenCalledWith("user-1", { + workspaceIds: ["workspace-1"], + }), + ); + expect(mocks.messageSuccess).toHaveBeenCalledWith("workspacesUpdated"); + expect(onFinish).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/forms/Role.tsx b/src/forms/role/Role.tsx similarity index 74% rename from src/forms/Role.tsx rename to src/forms/role/Role.tsx index e4fe2e5..10b3c67 100644 --- a/src/forms/Role.tsx +++ b/src/forms/role/Role.tsx @@ -1,29 +1,27 @@ /* eslint-disable react-hooks/set-state-in-effect */ -import { - Button, - Checkbox, - Divider, - Drawer, - Flex, - Form, - FormProps, - Input, - Space, -} from "antd"; +import { Checkbox, Divider, Flex, Form, Input } from "antd"; import { FC, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useLocation } from "react-router-dom"; +import { useLocation } from "react-router"; import { createRole, updateRole } from "@/api"; +import { FormDrawer } from "@/components/form-drawer"; import { modalKeys } from "@/config"; import { useAntd, useGoBack } from "@/hooks"; import { PermissionGroupProps, RoleMutationParams, RoleProps } from "@/types"; +import { getErrorMessage } from "@/utils"; -export const RoleForm: FC<{ +type RoleFormProps = { data?: RoleProps; onFinish: () => void; options: { permissions: PermissionGroupProps[] }; -}> = ({ data, onFinish, options: { permissions } }) => { +}; + +export const RoleForm: FC = ({ + data, + onFinish, + options: { permissions }, +}) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [submitting, setSubmitting] = useState(false); @@ -33,9 +31,7 @@ export const RoleForm: FC<{ const goBack = useGoBack(); const isUpdate = hash === modalKeys.update && !!data; - const handleSubmit: FormProps["onFinish"] = async ( - values, - ) => { + const handleSubmit = async (values: RoleMutationParams) => { if (submitting) return; try { @@ -46,8 +42,7 @@ export const RoleForm: FC<{ goBack(); onFinish(); } catch (error) { - if (error instanceof Error) messageAPI.error(error.message); - else console.error(error); + messageAPI.error(getErrorMessage(error)); } finally { setSubmitting(false); } @@ -77,33 +72,21 @@ export const RoleForm: FC<{ }, [data, form, goBack, hash, open]); return ( - - - - - } - mask={{ closable: false }} - onClose={() => goBack()} + { + if (isOpen) form.focusField("name"); + }} + onClose={goBack} + onSubmit={() => form.submit()} open={open} - styles={{ footer: { textAlign: "end" } }} + submitting={submitting} title={t(isUpdate ? "update" : "create")} > form={form} initialValues={{ permissions: [] }} layout="vertical" - onFinish={handleSubmit} + onFinish={(values) => void handleSubmit(values)} > @@ -134,6 +117,6 @@ export const RoleForm: FC<{ - + ); }; diff --git a/src/forms/role/index.ts b/src/forms/role/index.ts new file mode 100644 index 0000000..0a9c9bc --- /dev/null +++ b/src/forms/role/index.ts @@ -0,0 +1 @@ +export { RoleForm } from "./Role"; diff --git a/src/forms/user-password/UserPassword.tsx b/src/forms/user-password/UserPassword.tsx new file mode 100644 index 0000000..bcd5c7a --- /dev/null +++ b/src/forms/user-password/UserPassword.tsx @@ -0,0 +1,75 @@ +/* eslint-disable react-hooks/set-state-in-effect */ +import { Form } from "antd"; +import { FC, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useLocation } from "react-router"; + +import { updateUserPassword } from "@/api"; +import { FormDrawer } from "@/components/form-drawer"; +import { PasswordFields } from "@/components/password-fields"; +import { modalKeys } from "@/config"; +import { useAntd, useGoBack } from "@/hooks"; +import { UserPasswordParams, UserProps } from "@/types"; +import { getErrorMessage } from "@/utils"; + +type UserPasswordFormParams = UserPasswordParams & { + confirmPassword: string; +}; + +type UserPasswordFormProps = { + data?: UserProps; +}; + +export const UserPasswordForm: FC = ({ data }) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); + const { messageAPI } = useAntd(); + const { hash } = useLocation(); + const [form] = Form.useForm(); + const goBack = useGoBack(); + + const handleSubmit = async ({ password }: UserPasswordFormParams) => { + if (submitting || !data) return; + + try { + setSubmitting(true); + await updateUserPassword(data.id, { password }); + messageAPI.success(t("passwordUpdated")); + goBack(); + } catch (error) { + messageAPI.error(getErrorMessage(error)); + } finally { + setSubmitting(false); + } + }; + + useEffect(() => { + if (hash === modalKeys.password) { + if (data) setOpen(true); + else goBack(); + } else { + if (open) form.resetFields(); + setOpen(false); + setSubmitting(false); + } + }, [data, form, goBack, hash, open]); + + return ( + form.submit()} + open={open} + submitting={submitting} + title={t("changePassword")} + > + + form={form} + layout="vertical" + onFinish={(values) => void handleSubmit(values)} + > + + + + ); +}; diff --git a/src/forms/user-password/index.ts b/src/forms/user-password/index.ts new file mode 100644 index 0000000..8838a47 --- /dev/null +++ b/src/forms/user-password/index.ts @@ -0,0 +1 @@ +export { UserPasswordForm } from "./UserPassword"; diff --git a/src/forms/UserRole.tsx b/src/forms/user-role/UserRole.tsx similarity index 68% rename from src/forms/UserRole.tsx rename to src/forms/user-role/UserRole.tsx index 646643f..e49fba6 100644 --- a/src/forms/UserRole.tsx +++ b/src/forms/user-role/UserRole.tsx @@ -1,13 +1,15 @@ /* eslint-disable react-hooks/set-state-in-effect */ -import { Button, Drawer, Form, FormProps, Select, Space } from "antd"; +import { Form, Select } from "antd"; import { FC, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useLocation } from "react-router-dom"; +import { useLocation } from "react-router"; import { updateUserRoles } from "@/api"; +import { FormDrawer } from "@/components/form-drawer"; import { modalKeys } from "@/config"; import { useAntd, useGoBack } from "@/hooks"; import { UserOptionProps, UserProps, UserRoleParams } from "@/types"; +import { getErrorMessage } from "@/utils"; type UserFormRoleProps = { data?: UserProps; @@ -28,9 +30,7 @@ export const UserFormRole: FC = ({ const [form] = Form.useForm(); const goBack = useGoBack(); - const handleSubmit: FormProps["onFinish"] = async ( - values, - ) => { + const handleSubmit = async (values: UserRoleParams) => { if (submitting || !data) return; try { @@ -40,8 +40,7 @@ export const UserFormRole: FC = ({ goBack(); onFinish(); } catch (error) { - if (error instanceof Error) messageAPI.error(error.message); - else console.error(error); + messageAPI.error(getErrorMessage(error)); } finally { setSubmitting(false); } @@ -63,32 +62,17 @@ export const UserFormRole: FC = ({ }, [data, form, goBack, hash, open]); return ( - - - - - } - mask={{ closable: false }} - onClose={() => goBack()} + form.submit()} open={open} + submitting={submitting} title={t("roles")} - styles={{ footer: { textAlign: "end" } }} > form={form} layout="vertical" - onFinish={handleSubmit} + onFinish={(values) => void handleSubmit(values)} > = ({ /> - + ); }; diff --git a/src/forms/user-workspace/index.ts b/src/forms/user-workspace/index.ts new file mode 100644 index 0000000..a7e40a9 --- /dev/null +++ b/src/forms/user-workspace/index.ts @@ -0,0 +1 @@ +export { UserWorkspaceForm } from "./UserWorkspace"; diff --git a/src/forms/User.tsx b/src/forms/user/User.tsx similarity index 57% rename from src/forms/User.tsx rename to src/forms/user/User.tsx index 58d5c79..d9f91d0 100644 --- a/src/forms/User.tsx +++ b/src/forms/user/User.tsx @@ -1,23 +1,28 @@ /* eslint-disable react-hooks/set-state-in-effect */ -import { Button, Drawer, Form, FormProps, Input, Space } from "antd"; +import { Form, Input } from "antd"; import { FC, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useLocation } from "react-router-dom"; +import { useLocation } from "react-router"; import { createUser, updateUser } from "@/api"; -import { DigitsInput } from "@/components/DigitsInput"; +import { DigitsInput } from "@/components/digits-input"; +import { FormDrawer } from "@/components/form-drawer"; +import { PasswordFields } from "@/components/password-fields"; import { modalKeys } from "@/config"; import { useAntd, useGoBack } from "@/hooks"; import { CreateUserParams, UpdateUserParams, UserProps } from "@/types"; +import { getErrorMessage } from "@/utils"; type UserFormParams = CreateUserParams & { confirmPassword: string; }; -export const UserForm: FC<{ data?: UserProps; onFinish: () => void }> = ({ - data, - onFinish, -}) => { +type UserFormProps = { + data?: UserProps; + onFinish: () => void; +}; + +export const UserForm: FC = ({ data, onFinish }) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [submitting, setSubmitting] = useState(false); @@ -27,13 +32,13 @@ export const UserForm: FC<{ data?: UserProps; onFinish: () => void }> = ({ const goBack = useGoBack(); const isUpdate = hash === modalKeys.update && !!data; - const handleSubmit: FormProps["onFinish"] = async ({ + const handleSubmit = async ({ firstName, lastName, mobile, password, personnelCode, - }) => { + }: UserFormParams) => { if (submitting) return; try { @@ -51,8 +56,7 @@ export const UserForm: FC<{ data?: UserProps; onFinish: () => void }> = ({ goBack(); onFinish(); } catch (error) { - if (error instanceof Error) messageAPI.error(error.message); - else console.error(error); + messageAPI.error(getErrorMessage(error)); } finally { setSubmitting(false); } @@ -82,32 +86,17 @@ export const UserForm: FC<{ data?: UserProps; onFinish: () => void }> = ({ }, [data, form, goBack, hash, open]); return ( - - - - - } - mask={{ closable: false }} - onClose={() => goBack()} + form.submit()} open={open} + submitting={submitting} title={t(isUpdate ? "update" : "create")} - styles={{ footer: { textAlign: "end" } }} > form={form} layout="vertical" - onFinish={handleSubmit} + onFinish={(values) => void handleSubmit(values)} > void }> = ({ > - {!isUpdate && ( - <> - - - - ({ - validator(_, value) { - if (!value || getFieldValue("password") === value) { - return Promise.resolve(); - } - - return Promise.reject(new Error(t("passsMismatch"))); - }, - }), - ]} - > - - - - )} + {!isUpdate && } - + ); }; diff --git a/src/forms/user/index.ts b/src/forms/user/index.ts new file mode 100644 index 0000000..9066b86 --- /dev/null +++ b/src/forms/user/index.ts @@ -0,0 +1 @@ +export { UserForm } from "./User"; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index fd9a0d0..327e02c 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,6 +1,6 @@ -export * from "./useActionPermissions"; -export * from "./useAllowedRoutes"; -export * from "./useAntd"; -export * from "./useCore"; -export * from "./useFilterParams"; -export * from "./useGoBack"; +export * from "./use-action-permissions"; +export * from "./use-allowed-routes"; +export * from "./use-antd"; +export * from "./use-core"; +export * from "./use-filter-params"; +export * from "./use-go-back"; diff --git a/src/hooks/use-action-permissions/index.ts b/src/hooks/use-action-permissions/index.ts new file mode 100644 index 0000000..4fe5e63 --- /dev/null +++ b/src/hooks/use-action-permissions/index.ts @@ -0,0 +1 @@ +export { useActionPermissions } from "./useActionPermissions"; diff --git a/src/hooks/use-action-permissions/useActionPermissions.test.tsx b/src/hooks/use-action-permissions/useActionPermissions.test.tsx new file mode 100644 index 0000000..ae7fa79 --- /dev/null +++ b/src/hooks/use-action-permissions/useActionPermissions.test.tsx @@ -0,0 +1,83 @@ +import { renderHook } from "@testing-library/react"; +import { ReactNode } from "react"; +import { describe, expect, it } from "vitest"; + +import { CoreContext, CoreContextProps } from "@/contexts"; +import { AccountProps } from "@/types"; + +import { useActionPermissions } from "./"; + +const createUser = (overrides: Partial = {}): AccountProps => ({ + email: null, + firstName: "Test", + id: "user-1", + isSystemAdmin: false, + lastName: "User", + mobile: "09120000000", + permissions: [], + personnelCode: null, + status: "active", + username: "test-user", + ...overrides, +}); + +const renderActionPermissions = (user?: AccountProps) => { + const value: CoreContextProps = { + currentRoute: "root", + language: "en", + setCurrentRoute: () => undefined, + setLanguage: () => undefined, + setTheme: () => undefined, + setUser: () => undefined, + theme: "light", + user, + }; + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ); + + return renderHook(useActionPermissions, { wrapper }).result.current; +}; + +describe("useActionPermissions", () => { + it("denies every action for an unauthenticated user", () => { + expect(renderActionPermissions()).toEqual({ + canCreateRoles: false, + canCreateUsers: false, + canDeleteRoles: false, + canDeleteUsers: false, + canUpdateRoles: false, + canUpdateUsers: false, + }); + }); + + it("allows only explicitly granted actions", () => { + expect( + renderActionPermissions( + createUser({ + permissions: ["roles.create", "users.delete", "users.update"], + }), + ), + ).toEqual({ + canCreateRoles: true, + canCreateUsers: false, + canDeleteRoles: false, + canDeleteUsers: true, + canUpdateRoles: false, + canUpdateUsers: true, + }); + }); + + it("allows every action for a system administrator", () => { + expect( + renderActionPermissions(createUser({ isSystemAdmin: true })), + ).toEqual({ + canCreateRoles: true, + canCreateUsers: true, + canDeleteRoles: true, + canDeleteUsers: true, + canUpdateRoles: true, + canUpdateUsers: true, + }); + }); +}); diff --git a/src/hooks/useActionPermissions.ts b/src/hooks/use-action-permissions/useActionPermissions.ts similarity index 86% rename from src/hooks/useActionPermissions.ts rename to src/hooks/use-action-permissions/useActionPermissions.ts index 49b7af1..1ad82bc 100644 --- a/src/hooks/useActionPermissions.ts +++ b/src/hooks/use-action-permissions/useActionPermissions.ts @@ -1,8 +1,6 @@ import { useMemo } from "react"; -import { PermissionKey } from "@/types"; - -import { useCore } from "./useCore"; +import { useCore } from "@/hooks/use-core"; type ActionPermissions = { canCreateRoles: boolean; @@ -28,7 +26,7 @@ export const useActionPermissions = (): ActionPermissions => { }; } - const permissions = new Set(user?.permissions); + const permissions = new Set(user?.permissions); return { canCreateRoles: permissions.has("roles.create"), diff --git a/src/hooks/use-allowed-routes/index.ts b/src/hooks/use-allowed-routes/index.ts new file mode 100644 index 0000000..13d0584 --- /dev/null +++ b/src/hooks/use-allowed-routes/index.ts @@ -0,0 +1 @@ +export { useAllowedRoutes } from "./useAllowedRoutes"; diff --git a/src/hooks/use-allowed-routes/useAllowedRoutes.test.tsx b/src/hooks/use-allowed-routes/useAllowedRoutes.test.tsx new file mode 100644 index 0000000..60db01c --- /dev/null +++ b/src/hooks/use-allowed-routes/useAllowedRoutes.test.tsx @@ -0,0 +1,92 @@ +import { renderHook } from "@testing-library/react"; +import { ReactNode } from "react"; +import { describe, expect, it } from "vitest"; + +import { CoreContext, CoreContextProps } from "@/contexts"; +import { AccountProps } from "@/types"; + +import { useAllowedRoutes } from "./"; + +const createUser = (overrides: Partial = {}): AccountProps => ({ + email: null, + firstName: "Test", + id: "user-1", + isSystemAdmin: false, + lastName: "User", + mobile: "09120000000", + permissions: [], + personnelCode: null, + status: "active", + username: "test-user", + ...overrides, +}); + +const renderAllowedRoutes = (user?: AccountProps) => { + const value: CoreContextProps = { + currentRoute: "root", + language: "en", + setCurrentRoute: () => undefined, + setLanguage: () => undefined, + setTheme: () => undefined, + setUser: () => undefined, + theme: "light", + user, + }; + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ); + + return renderHook(useAllowedRoutes, { wrapper }).result.current; +}; + +describe("useAllowedRoutes", () => { + it("allows only public routes for an unauthenticated user", () => { + const routes = renderAllowedRoutes(); + + expect(routes).toEqual( + new Set(["auth", "forgotPassword", "notFound", "register"]), + ); + }); + + it("allows authenticated routes and routes with granted permissions", () => { + const routes = renderAllowedRoutes( + createUser({ permissions: ["users.read"] }), + ); + + expect(routes).toEqual( + new Set([ + "account", + "auth", + "forgotPassword", + "notFound", + "register", + "root", + "users", + ]), + ); + }); + + it("denies routes when the required permission is absent", () => { + const routes = renderAllowedRoutes(createUser()); + + expect(routes.has("users")).toBe(false); + expect(routes.has("roles")).toBe(false); + }); + + it("allows every route for a system administrator", () => { + const routes = renderAllowedRoutes(createUser({ isSystemAdmin: true })); + + expect(routes).toEqual( + new Set([ + "account", + "auth", + "forgotPassword", + "notFound", + "register", + "roles", + "root", + "users", + ]), + ); + }); +}); diff --git a/src/hooks/useAllowedRoutes.ts b/src/hooks/use-allowed-routes/useAllowedRoutes.ts similarity index 81% rename from src/hooks/useAllowedRoutes.ts rename to src/hooks/use-allowed-routes/useAllowedRoutes.ts index b86ebb4..f82bbe5 100644 --- a/src/hooks/useAllowedRoutes.ts +++ b/src/hooks/use-allowed-routes/useAllowedRoutes.ts @@ -1,13 +1,9 @@ import { useMemo } from "react"; import { RouteKey } from "@/config"; -import { PermissionKey } from "@/types"; +import { useCore } from "@/hooks/use-core"; -import { useCore } from "./useCore"; - -type RouteAccess = "auth" | "public" | PermissionKey; - -const ROUTE_RULES: Record = { +const ROUTE_RULES: Record = { account: "auth", auth: "public", forgotPassword: "public", diff --git a/src/hooks/use-antd/index.ts b/src/hooks/use-antd/index.ts new file mode 100644 index 0000000..babfa0a --- /dev/null +++ b/src/hooks/use-antd/index.ts @@ -0,0 +1 @@ +export { useAntd } from "./useAntd"; diff --git a/src/hooks/useAntd.ts b/src/hooks/use-antd/useAntd.ts similarity index 100% rename from src/hooks/useAntd.ts rename to src/hooks/use-antd/useAntd.ts diff --git a/src/hooks/use-core/index.ts b/src/hooks/use-core/index.ts new file mode 100644 index 0000000..5fd5b17 --- /dev/null +++ b/src/hooks/use-core/index.ts @@ -0,0 +1 @@ +export { useCore } from "./useCore"; diff --git a/src/hooks/useCore.ts b/src/hooks/use-core/useCore.ts similarity index 100% rename from src/hooks/useCore.ts rename to src/hooks/use-core/useCore.ts diff --git a/src/hooks/use-filter-params/index.ts b/src/hooks/use-filter-params/index.ts new file mode 100644 index 0000000..ff974ee --- /dev/null +++ b/src/hooks/use-filter-params/index.ts @@ -0,0 +1 @@ +export { useFilterParams } from "./useFilterParams"; diff --git a/src/hooks/useFilterParams.ts b/src/hooks/use-filter-params/useFilterParams.ts similarity index 57% rename from src/hooks/useFilterParams.ts rename to src/hooks/use-filter-params/useFilterParams.ts index 493b3c4..a46d60b 100644 --- a/src/hooks/useFilterParams.ts +++ b/src/hooks/use-filter-params/useFilterParams.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from "react"; -import { useSearchParams } from "react-router-dom"; +import { useSearchParams } from "react-router"; export const useFilterParams = >() => { const [searchParams, setSearchParams] = useSearchParams(); @@ -17,12 +17,19 @@ export const useFilterParams = >() => { const setFilters = useCallback( (newFilters: Partial) => { - const sanitized = Object.fromEntries( - Object.entries(newFilters).filter( - ([, v]) => v !== undefined && v !== null && v !== "", - ), - ); - setSearchParams(sanitized); + setSearchParams((current) => { + const next = new URLSearchParams(current); + + for (const [key, value] of Object.entries(newFilters)) { + if (value === undefined || value === null || value === "") { + next.delete(key); + } else { + next.set(key, value); + } + } + + return next; + }); }, [setSearchParams], ); diff --git a/src/hooks/use-go-back/index.ts b/src/hooks/use-go-back/index.ts new file mode 100644 index 0000000..069cfd6 --- /dev/null +++ b/src/hooks/use-go-back/index.ts @@ -0,0 +1 @@ +export { useGoBack } from "./useGoBack"; diff --git a/src/hooks/useGoBack.ts b/src/hooks/use-go-back/useGoBack.ts similarity index 84% rename from src/hooks/useGoBack.ts rename to src/hooks/use-go-back/useGoBack.ts index a43eb52..04eae3b 100644 --- a/src/hooks/useGoBack.ts +++ b/src/hooks/use-go-back/useGoBack.ts @@ -1,10 +1,5 @@ import { useCallback } from "react"; -import { - NavigateOptions, - To, - useLocation, - useNavigate, -} from "react-router-dom"; +import { NavigateOptions, To, useLocation, useNavigate } from "react-router"; export const useGoBack = (): ((to?: To, options?: NavigateOptions) => void) => { const { pathname, state } = useLocation(); diff --git a/src/layouts/Default.tsx b/src/layouts/Default.tsx deleted file mode 100644 index d055b41..0000000 --- a/src/layouts/Default.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { - Avatar, - Button, - Divider, - Drawer, - Dropdown, - Flex, - Grid, - Menu, - Spin, - Typography, -} from "antd"; -import { useAntdToken } from "antd-style"; -import { useCallback, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Link, Outlet, useNavigate } from "react-router-dom"; - -import { getAccount, logout, setUnauthorizedHandler } from "@/api"; -import { clearAccessToken } from "@/api/token"; -import { Icon } from "@/components/Icon"; -import { RouteKey, routeTree } from "@/config"; -import { useAllowedRoutes, useCore } from "@/hooks"; - -const { useBreakpoint } = Grid; - -export const DefaultLayout = () => { - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - const { lg } = useBreakpoint(); - const { currentRoute, setTheme, setUser, theme: coreTheme, user } = useCore(); - const token = useAntdToken(); - const navigate = useNavigate(); - const allowedRoutes = useAllowedRoutes(); - const darkMode = coreTheme === "dark"; - - const menuItemLabels: Partial> = { - root: t("dashboard"), - users: t("users"), - roles: t("roles"), - }; - - const menuItems = (Object.entries(menuItemLabels) as [RouteKey, string][]) - .filter(([key]) => allowedRoutes.has(key)) - .map(([key, label]) => ({ - key, - label: {label}, - })); - - const clearSession = useCallback(() => { - clearAccessToken(); - setUser(); - - navigate(routeTree.auth.path, { replace: true }); - }, [navigate, setUser]); - - const handleLogout = async () => { - await logout().catch(() => undefined); - - clearSession(); - }; - - useEffect(() => { - void (async () => { - try { - const user = await getAccount(); - - setUser(user); - } catch { - clearSession(); - } - })(); - - setUnauthorizedHandler(clearSession); - - return () => setUnauthorizedHandler(null); - }, [clearSession, setUser]); - - if (!user) return ; - - return ( - <> - - - - kernel - - - {lg ? ( - - ) : ( - - - - label={t("newPass")} - name="password" - rules={[{ required: true }]} - > - - - - dependencies={["password"]} - label={t("confirmPass")} - name="confirmPassword" - rules={[ - { required: true }, - ({ getFieldValue }) => ({ - validator(_, value) { - if (!value || getFieldValue("password") === value) { - return Promise.resolve(); - } - - return Promise.reject(new Error(t("passsMismatch"))); - }, - }), - ]} - > - - + } + extra={ + + + + } /> ); }; diff --git a/src/pages/not-found/index.ts b/src/pages/not-found/index.ts new file mode 100644 index 0000000..bbf8269 --- /dev/null +++ b/src/pages/not-found/index.ts @@ -0,0 +1 @@ +export { NotFoundPage } from "./NotFound"; diff --git a/src/pages/pages.test.tsx b/src/pages/pages.test.tsx new file mode 100644 index 0000000..da255a3 --- /dev/null +++ b/src/pages/pages.test.tsx @@ -0,0 +1,464 @@ +import { within } from "@testing-library/react"; +import { MemoryRouter, useLocation } from "react-router"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import * as api from "@/api"; +import { render, screen } from "@/test/render"; + +import { AccountPage } from "./account"; +import { ForgotPassPage } from "./forgot-pass"; +import { LoginPage } from "./login"; +import { RegisterPage } from "./register"; +import { RolesPage } from "./roles"; +import { UsersPage } from "./users"; + +const mocks = vi.hoisted(() => { + const account = { + email: "ada@example.com", + firstName: "Ada", + id: "user-1", + isSystemAdmin: false, + lastName: "Lovelace", + mobile: "09120000000", + permissions: [], + personnelCode: "1001", + status: "active", + username: "ada", + }; + const messageError = vi.fn(); + const messageSuccess = vi.fn(); + const modalConfirm = vi.fn(); + const setFilters = vi.fn(); + const setUser = vi.fn(); + + return { + actionPermissions: { + canCreateRoles: false, + canCreateUsers: false, + canDeleteRoles: false, + canDeleteUsers: false, + canUpdateRoles: false, + canUpdateUsers: false, + }, + account, + antd: { + messageAPI: { + error: messageError, + success: messageSuccess, + }, + modalAPI: { confirm: modalConfirm }, + }, + core: { + setUser, + user: account, + }, + filterState: { + filters: {}, + setFilters, + }, + messageError, + messageSuccess, + modalConfirm, + setFilters, + setUser, + }; +}); + +vi.mock("@/api", () => ({ + changePassword: vi.fn(), + deleteRole: vi.fn(), + deleteUser: vi.fn(), + fetchPermissions: vi.fn(), + fetchRole: vi.fn(), + fetchRoles: vi.fn(), + fetchUser: vi.fn(), + fetchUserRoleOptions: vi.fn(), + fetchUsers: vi.fn(), + fetchUserWorkspaceOptions: vi.fn(), + forgotPassword: vi.fn(), + getAccount: vi.fn(), + login: vi.fn(), + register: vi.fn(), + requestEmailVerification: vi.fn(), + requestOtp: vi.fn(), + updateProfile: vi.fn(), + updateUsername: vi.fn(), + updateUserStatus: vi.fn(), + updateUserSystemAdmin: vi.fn(), + verifyEmail: vi.fn(), +})); + +vi.mock("@/forms/role", () => ({ RoleForm: () => null })); +vi.mock("@/forms/user", () => ({ UserForm: () => null })); +vi.mock("@/forms/user-password", () => ({ UserPasswordForm: () => null })); +vi.mock("@/forms/user-role", () => ({ UserFormRole: () => null })); +vi.mock("@/forms/user-workspace", () => ({ UserWorkspaceForm: () => null })); + +vi.mock("@/hooks", () => ({ + useActionPermissions: () => mocks.actionPermissions, + useAntd: () => mocks.antd, + useCore: () => mocks.core, + useFilterParams: () => mocks.filterState, +})); + +vi.mock("antd-style", () => ({ + useAntdToken: () => ({ + colorSuccess: "#00aa00", + marginMD: 16, + marginXS: 8, + paddingMD: 16, + paddingSM: 8, + screenMD: 768, + screenXL: 1200, + }), +})); + +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal()), + useTranslation: () => ({ t: (key: string) => key }), +})); + +const Location = () => { + const location = useLocation(); + return {location.pathname}; +}; + +const renderPage = (page: React.ReactNode) => + render( + + {page} + + , + ); + +beforeEach(() => { + vi.clearAllMocks(); + Object.assign(mocks.actionPermissions, { + canCreateRoles: false, + canCreateUsers: false, + canDeleteRoles: false, + canDeleteUsers: false, + canUpdateRoles: false, + canUpdateUsers: false, + }); + mocks.core.user = mocks.account; + vi.mocked(api.deleteUser).mockResolvedValue(undefined); + vi.mocked(api.fetchUserRoleOptions).mockResolvedValue([]); + vi.mocked(api.fetchUserWorkspaceOptions).mockResolvedValue([]); + vi.mocked(api.changePassword).mockResolvedValue(undefined); + vi.mocked(api.forgotPassword).mockResolvedValue(undefined); + vi.mocked(api.getAccount).mockResolvedValue(mocks.account); + vi.mocked(api.login).mockResolvedValue({ + accessToken: "access-token", + }); + vi.mocked(api.requestOtp).mockResolvedValue({ + expiresIn: 300, + remainingSeconds: 60, + }); + vi.mocked(api.register).mockResolvedValue({ accessToken: "access-token" }); + vi.mocked(api.requestEmailVerification).mockResolvedValue({ + expiresIn: 300, + remainingSeconds: 60, + }); + vi.mocked(api.updateProfile).mockResolvedValue(mocks.account); + vi.mocked(api.updateUsername).mockResolvedValue(mocks.account); + vi.mocked(api.updateUserStatus).mockResolvedValue(undefined); + vi.mocked(api.updateUserSystemAdmin).mockResolvedValue(undefined); + vi.mocked(api.verifyEmail).mockResolvedValue(undefined); + vi.mocked(api.fetchRoles).mockResolvedValue([]); + vi.mocked(api.fetchUsers).mockResolvedValue({ items: [], total: 0 }); +}); + +describe("priority pages", () => { + it("submits login credentials and navigates home", async () => { + const { user } = renderPage(); + + await user.type(screen.getByLabelText("identifier"), "ada"); + await user.type(screen.getByLabelText("password"), "secret"); + await user.click(screen.getByRole("button", { name: "enter" })); + + expect(api.login).toHaveBeenCalledWith({ + identifier: "ada", + password: "secret", + }); + expect(screen.getByLabelText("location")).toHaveTextContent("/"); + }); + + it("requests a password-recovery OTP for the entered mobile", async () => { + const { user } = renderPage(); + + await user.type(screen.getByLabelText("mobile"), "09120000000"); + await user.click(screen.getByRole("button", { name: "requestOtp" })); + + expect(api.requestOtp).toHaveBeenCalledWith({ + mobile: "09120000000", + purpose: "forgot_password", + }); + expect(mocks.messageSuccess).toHaveBeenCalledWith("otpSent"); + }); + + it("requests a registration OTP for the entered mobile", async () => { + const { user } = renderPage(); + + await user.type(screen.getByLabelText("mobile"), "09120000000"); + await user.click(screen.getByRole("button", { name: "requestOtp" })); + + expect(api.requestOtp).toHaveBeenCalledWith({ + mobile: "09120000000", + purpose: "register", + }); + expect(mocks.messageSuccess).toHaveBeenCalledWith("otpSent"); + }); + + it("completes password recovery", async () => { + const { user } = renderPage(); + + await user.type(screen.getByLabelText("mobile"), "09120000000"); + await user.type(screen.getByLabelText("OTP Input 1"), "123456"); + await user.type(screen.getByLabelText("newPass"), "replacement-pass"); + await user.type(screen.getByLabelText("confirmPass"), "replacement-pass"); + await user.click(screen.getByRole("button", { name: "submit" })); + + expect(api.forgotPassword).toHaveBeenCalledWith({ + mobile: "09120000000", + otp: "123456", + password: "replacement-pass", + }); + expect(mocks.messageSuccess).toHaveBeenCalledWith("passwordReset"); + expect(screen.getByLabelText("location")).toHaveTextContent("/"); + }); + + it("registers a verified user", async () => { + const { user } = renderPage(); + + await user.type(screen.getByLabelText("firstName"), "Ada"); + await user.type(screen.getByLabelText("lastName"), "Lovelace"); + await user.type(screen.getByLabelText("mobile"), "09120000000"); + await user.type(screen.getByLabelText("OTP Input 1"), "123456"); + await user.type(screen.getByLabelText("password"), "registration-pass"); + await user.type(screen.getByLabelText("confirmPass"), "registration-pass"); + await user.click(screen.getByRole("button", { name: "register" })); + + expect(api.register).toHaveBeenCalledWith({ + firstName: "Ada", + lastName: "Lovelace", + mobile: "09120000000", + otp: "123456", + password: "registration-pass", + }); + expect(screen.getByLabelText("location")).toHaveTextContent("/"); + }); + + it("updates the account profile", async () => { + const updatedAccount = { ...mocks.account, firstName: "Augusta" }; + vi.mocked(api.updateProfile).mockResolvedValue(updatedAccount); + const { user } = renderPage(); + const firstName = screen.getByLabelText("firstName"); + const profileForm = firstName.closest("form"); + + expect(profileForm).not.toBeNull(); + await user.clear(firstName); + await user.type(firstName, "Augusta"); + await user.click( + within(profileForm!).getByRole("button", { name: "update" }), + ); + + expect(api.updateProfile).toHaveBeenCalledWith({ + firstName: "Augusta", + lastName: "Lovelace", + personnelCode: "1001", + }); + expect(mocks.setUser).toHaveBeenCalledWith(updatedAccount); + expect(mocks.messageSuccess).toHaveBeenCalledWith("profileUpdated"); + }); + + it("updates the account username", async () => { + const updatedAccount = { ...mocks.account, username: "augusta" }; + vi.mocked(api.updateUsername).mockResolvedValue(updatedAccount); + const { user } = renderPage(); + + const username = screen.getByLabelText("username"); + await user.clear(username); + await user.type(username, "augusta"); + await user.click( + within(username.closest("form")!).getByRole("button", { name: "update" }), + ); + + expect(api.updateUsername).toHaveBeenCalledWith({ username: "augusta" }); + expect(mocks.setUser).toHaveBeenCalledWith(updatedAccount); + }); + + it("verifies a replacement account email", async () => { + const updatedAccount = { + ...mocks.account, + email: "augusta@example.com", + }; + vi.mocked(api.getAccount).mockResolvedValue(updatedAccount); + const { user } = renderPage(); + const email = screen.getByLabelText("email"); + + await user.clear(email); + await user.type(email, "augusta@example.com"); + await user.click(screen.getByRole("button", { name: "requestOtp" })); + await user.type(screen.getByLabelText("OTP Input 1"), "123456"); + await user.click(screen.getByRole("button", { name: "verifyEmail" })); + + expect(api.requestEmailVerification).toHaveBeenCalledWith({ + email: "augusta@example.com", + }); + expect(api.verifyEmail).toHaveBeenCalledWith({ + email: "augusta@example.com", + otp: "123456", + }); + expect(mocks.setUser).toHaveBeenCalledWith(updatedAccount); + }); + + it("changes the account password", async () => { + const { user } = renderPage(); + + await user.type(screen.getByLabelText("currentPass"), "current-pass"); + await user.type(screen.getByLabelText("newPass"), "new-password"); + await user.type(screen.getByLabelText("confirmPass"), "new-password"); + await user.click(screen.getByRole("button", { name: "changePassword" })); + + expect(api.changePassword).toHaveBeenCalledWith({ + currentPassword: "current-pass", + newPassword: "new-password", + }); + }); + + it("loads and displays roles", async () => { + vi.mocked(api.fetchRoles).mockResolvedValue([ + { + id: "role-1", + name: "Operators", + permissions: ["users.read", "users.update"], + }, + ]); + + renderPage(); + + expect(await screen.findByText("Operators")).toBeInTheDocument(); + expect(api.fetchRoles).toHaveBeenCalledOnce(); + }); + + it("updates and deletes roles when permitted", async () => { + Object.assign(mocks.actionPermissions, { + canCreateRoles: true, + canDeleteRoles: true, + canUpdateRoles: true, + }); + const role = { + id: "role-1", + name: "Operators", + permissions: ["users.read"], + }; + vi.mocked(api.fetchRoles).mockResolvedValue([role]); + vi.mocked(api.fetchRole).mockResolvedValue(role); + vi.mocked(api.fetchPermissions).mockResolvedValue([]); + vi.mocked(api.deleteRole).mockResolvedValue(undefined); + const { user } = renderPage(); + + await screen.findByText("Operators"); + await user.click(screen.getByRole("button", { name: "update" })); + + expect(api.fetchRole).toHaveBeenCalledWith("role-1"); + expect(screen.getByLabelText("location")).toHaveTextContent("/"); + + await user.click(screen.getByRole("button", { name: "delete" })); + const confirmation = mocks.modalConfirm.mock.calls.at(-1)?.[0] as { + onOk: () => Promise; + }; + await confirmation.onOk(); + + expect(api.deleteRole).toHaveBeenCalledWith("role-1"); + expect(mocks.messageSuccess).toHaveBeenCalledWith("roleDeleted"); + expect(api.fetchRoles).toHaveBeenCalledTimes(2); + }); + + it("loads and displays users", async () => { + vi.mocked(api.fetchUsers).mockResolvedValue({ + items: [ + { + email: "ada@example.com", + firstName: "Ada", + id: "user-1", + isSystemAdmin: false, + lastName: "Lovelace", + mobile: "09120000000", + personnelCode: "1001", + status: "active", + username: "ada", + }, + ], + total: 1, + }); + + renderPage(); + + expect(await screen.findByText("Ada Lovelace")).toBeInTheDocument(); + expect(api.fetchUsers).toHaveBeenCalledWith({ + offset: "0", + size: "12", + }); + }); + + it("manages users when permitted", { timeout: 15_000 }, async () => { + Object.assign(mocks.actionPermissions, { + canCreateUsers: true, + canDeleteUsers: true, + canUpdateUsers: true, + }); + mocks.core.user = { ...mocks.account, isSystemAdmin: true }; + const userRecord = { + email: "ada@example.com", + firstName: "Ada", + id: "user-1", + isSystemAdmin: false, + lastName: "Lovelace", + mobile: "09120000000", + personnelCode: "1001", + status: "active" as const, + username: "ada", + }; + vi.mocked(api.fetchUsers).mockResolvedValue({ + items: [userRecord], + total: 1, + }); + vi.mocked(api.fetchUser).mockResolvedValue({ + ...userRecord, + roleIds: [], + workspaceIds: [], + }); + const { user } = renderPage(); + + await screen.findByText("Ada Lovelace"); + await user.click(screen.getByRole("button", { name: "update" })); + expect(api.fetchUser).toHaveBeenCalledWith("user-1"); + + await user.click(screen.getByRole("button", { name: "active" })); + let confirmation = mocks.modalConfirm.mock.calls.at(-1)?.[0] as { + onOk: () => Promise; + }; + await confirmation.onOk(); + expect(api.updateUserStatus).toHaveBeenCalledWith("user-1", { + status: "inactive", + }); + + await user.click(screen.getByRole("button", { name: "systemAdmin" })); + confirmation = mocks.modalConfirm.mock.calls.at(-1)?.[0] as { + onOk: () => Promise; + }; + await confirmation.onOk(); + expect(api.updateUserSystemAdmin).toHaveBeenCalledWith("user-1", { + isSystemAdmin: true, + }); + + await user.click(screen.getByRole("button", { name: "delete" })); + confirmation = mocks.modalConfirm.mock.calls.at(-1)?.[0] as { + onOk: () => Promise; + }; + await confirmation.onOk(); + expect(api.deleteUser).toHaveBeenCalledWith("user-1"); + }); +}); diff --git a/src/pages/Register.tsx b/src/pages/register/Register.tsx similarity index 69% rename from src/pages/Register.tsx rename to src/pages/register/Register.tsx index 48c04d6..1e34d7d 100644 --- a/src/pages/Register.tsx +++ b/src/pages/register/Register.tsx @@ -1,14 +1,17 @@ import { Button, Flex, Form, FormProps, Input, Typography } from "antd"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Link, useNavigate } from "react-router-dom"; +import { Link, useNavigate } from "react-router"; import { register, requestOtp } from "@/api"; -import { DigitsInput } from "@/components/DigitsInput"; -import { Icon } from "@/components/Icon"; +import { DigitsInput } from "@/components/digits-input"; +import { Icon } from "@/components/icon"; +import { OtpInput } from "@/components/otp-input"; +import { PasswordFields } from "@/components/password-fields"; import { routeTree } from "@/config"; import { useAntd } from "@/hooks"; import { RegisterParams } from "@/types"; +import { getErrorMessage } from "@/utils"; type RegisterFormParams = RegisterParams & { confirmPassword: string; @@ -47,20 +50,19 @@ export const RegisterPage = () => { setRemainingSeconds(response.remainingSeconds); messageAPI.success(t("otpSent")); } catch (error) { - if (error instanceof Error) messageAPI.error(error.message); - else console.error(error); + messageAPI.error(getErrorMessage(error)); } finally { setRequestingOtp(false); } }; - const handleSubmit: FormProps["onFinish"] = async ({ + const handleSubmit = async ({ firstName, lastName, mobile, otp, password, - }) => { + }: RegisterFormParams) => { if (submitting) return; try { @@ -70,23 +72,30 @@ export const RegisterPage = () => { navigate(routeTree.root.path, { replace: true }); } catch (error) { - if (error instanceof Error) messageAPI.error(error.message); - else console.error(error); + messageAPI.error(getErrorMessage(error)); } finally { setSubmitting(false); } }; + const handleFormSubmit: NonNullable< + FormProps["onFinish"] + > = (values) => { + void handleSubmit(values); + }; + return ( - {t("register")} + + {t("register")} + @@ -126,7 +135,7 @@ export const RegisterPage = () => { noStyle rules={[{ required: true }]} > - + - - label={t("password")} - name="password" - rules={[{ required: true }]} - > - - - - dependencies={["password"]} - label={t("confirmPass")} - name="confirmPassword" - rules={[ - { required: true }, - ({ getFieldValue }) => ({ - validator(_, value) { - if (!value || getFieldValue("password") === value) { - return Promise.resolve(); - } - - return Promise.reject(new Error(t("passsMismatch"))); - }, - }), - ]} - > - - +