From dd5e196420bd985d9bf744bb526d7dc3b77bdbe4 Mon Sep 17 00:00:00 2001 From: Dennis Paler Date: Sat, 18 Jul 2026 19:29:12 +0800 Subject: [PATCH 1/4] docs release ci updates --- .github/workflows/release-cli.yml | 46 -- .github/workflows/release-docker.yml | 77 --- .github/workflows/release-edge.yml | 40 -- .github/workflows/release-sdk-flutter.yml | 61 -- .github/workflows/release-sdk-nodejs.yml | 185 ----- .../workflows/release-sdk-react-native.yml | 51 -- .github/workflows/release-sdk-web.yml | 71 -- .github/workflows/release-ssr.yml | 40 -- .github/workflows/release.yml | 650 ++++++++++++++++++ 9 files changed, 650 insertions(+), 571 deletions(-) delete mode 100644 .github/workflows/release-cli.yml delete mode 100644 .github/workflows/release-docker.yml delete mode 100644 .github/workflows/release-edge.yml delete mode 100644 .github/workflows/release-sdk-flutter.yml delete mode 100644 .github/workflows/release-sdk-nodejs.yml delete mode 100644 .github/workflows/release-sdk-react-native.yml delete mode 100644 .github/workflows/release-sdk-web.yml delete mode 100644 .github/workflows/release-ssr.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml deleted file mode 100644 index f783b79..0000000 --- a/.github/workflows/release-cli.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Release CLI - -# Triggered by version tags: v1.2.3 (shared convention with the SDK releases). -on: - push: - tags: ["v*.*.*"] - -jobs: - publish: - name: Publish @checkgate/cli to npm - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - id-token: write # OIDC token for npm provenance - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - registry-url: https://registry.npmjs.org - - # The CLI has zero runtime dependencies — no install step needed. - - name: Run tests - working-directory: cli - run: node --test - - # End-to-end sanity: generate all three languages from the fixture so a - # broken generator can never ship. - - name: Smoke-test codegen - working-directory: cli - run: node bin/checkgate.js typegen --lang ts,dart,rust --input test/fixtures/flags.json --out-dir /tmp/checkgate-gen - - - name: Set version from tag - working-directory: cli - run: | - TAG="${{ github.ref_name }}" - VERSION="${TAG#v}" - npm version "$VERSION" --no-git-tag-version --allow-same-version - - - name: Publish @checkgate/cli - working-directory: cli - run: npm publish --access public --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml deleted file mode 100644 index 6ace0ee..0000000 --- a/.github/workflows/release-docker.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Release Docker image - -# Triggered by version tags: v1.2.3 -on: - push: - tags: ["v*.*.*"] - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ghcr.io/${{ github.repository }} - -jobs: - docker: - name: Build and push checkgate - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - id-token: write # Required for Cosign keyless signing - steps: - - uses: actions/checkout@v4 - - - name: Set up QEMU (multi-arch) - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Install Cosign - uses: sigstore/cosign-installer@v3.8.0 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate image tags - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.IMAGE_NAME }} - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest - - - name: Build and push - id: build-push - uses: docker/build-push-action@v6 - with: - context: . - file: Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master - with: - image-ref: ${{ env.IMAGE_NAME }}@${{ steps.build-push.outputs.digest }} - format: 'table' - exit-code: '1' - ignore-unfixed: true - vuln-type: 'os,library' - severity: 'CRITICAL' - env: - TRIVY_USERNAME: ${{ github.actor }} - TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - - - name: Sign the image with Cosign - run: | - cosign sign --yes ${{ env.IMAGE_NAME }}@${{ steps.build-push.outputs.digest }} diff --git a/.github/workflows/release-edge.yml b/.github/workflows/release-edge.yml deleted file mode 100644 index 71e0f02..0000000 --- a/.github/workflows/release-edge.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Release Edge - -# Triggered by version tags: v1.2.3 (shared convention with the SDK releases). -on: - push: - tags: ["v*.*.*"] - -jobs: - publish: - name: Publish @checkgate/edge to npm - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - id-token: write # OIDC token for npm provenance - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - registry-url: https://registry.npmjs.org - - # @checkgate/edge has zero runtime dependencies — no install step needed. - - name: Run tests - working-directory: edge - run: node --test - - - name: Set version from tag - working-directory: edge - run: | - TAG="${{ github.ref_name }}" - VERSION="${TAG#v}" - npm version "$VERSION" --no-git-tag-version --allow-same-version - - - name: Publish @checkgate/edge - working-directory: edge - run: npm publish --access public --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release-sdk-flutter.yml b/.github/workflows/release-sdk-flutter.yml deleted file mode 100644 index 3bb5fe4..0000000 --- a/.github/workflows/release-sdk-flutter.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Release Flutter SDK - -# Triggered by version tags: v1.2.3 -on: - push: - tags: ["v*.*.*"] - -# The Flutter SDK ships Dart code + Rust FFI source to pub.dev. -# The Rust native library (.so / .dylib / .a) is compiled by the consuming -# app's build system via cargokit / flutter_rust_bridge conventions, so no -# pre-built binary is uploaded here. - -jobs: - publish: - name: Publish to pub.dev - runs-on: ubuntu-latest - permissions: - id-token: write - steps: - - uses: actions/checkout@v4 - - - name: Install Flutter (stable) - uses: subosito/flutter-action@v2 - with: - channel: stable - cache: true - - - name: Install Dart dependencies - working-directory: sdks/flutter/dart - run: dart pub get - - - name: Validate package integrity - working-directory: sdks/flutter/dart - run: | - # Manually trigger bundling - node scripts/bundle-rust.js - - # Check for critical files - if [ ! -d "rust-core/src" ]; then echo "Error: rust-core missing!"; exit 1; fi - if [ ! -d "rust/src" ]; then echo "Error: Bridge source missing!"; exit 1; fi - - # Satisfy dart pub's "clean git state" requirement - git config user.name "github-actions" - git config user.email "github-actions@github.com" - git add . - git commit -m "chore: temporary commit for release packaging" - - echo "Flutter package integrity and git state verified." - - - name: Set up pub.dev credentials - run: | - mkdir -p ~/.config/dart - echo '${{ secrets.PUB_CREDENTIALS }}' > ~/.config/dart/pub-credentials.json - - - name: Validate pub.dev score (dry run) - working-directory: sdks/flutter/dart - run: dart pub publish --dry-run - - - name: Publish to pub.dev - working-directory: sdks/flutter/dart - run: dart pub publish --force diff --git a/.github/workflows/release-sdk-nodejs.yml b/.github/workflows/release-sdk-nodejs.yml deleted file mode 100644 index 88e1477..0000000 --- a/.github/workflows/release-sdk-nodejs.yml +++ /dev/null @@ -1,185 +0,0 @@ -name: Release Node.js SDK - -# Triggered by version tags: v1.2.3 -on: - push: - tags: ["v*.*.*"] - -jobs: - # --------------------------------------------------------------------------- - # Build the native .node binary for each platform in parallel - # --------------------------------------------------------------------------- - build-native: - name: Build — ${{ matrix.target }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - # Linux x64 (glibc) - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - artifact: checkgate.linux-x64-gnu.node - - # Linux arm64 (glibc) — cross-compile via napi-rs cross toolchain - - os: ubuntu-latest - target: aarch64-unknown-linux-gnu - artifact: checkgate.linux-arm64-gnu.node - cross: true - - # Linux x64 (musl — Alpine) — cross-compile via napi-rs cross toolchain - - os: ubuntu-latest - target: x86_64-unknown-linux-musl - artifact: checkgate.linux-x64-musl.node - cross: true - - # macOS x64 — cross-compile natively from arm64 runner - - os: macos-latest - target: x86_64-apple-darwin - artifact: checkgate.darwin-x64.node - - # macOS arm64 (Apple Silicon) - - os: macos-latest - target: aarch64-apple-darwin - artifact: checkgate.darwin-arm64.node - - # Windows x64 - - os: windows-latest - target: x86_64-pc-windows-msvc - artifact: checkgate.win32-x64-msvc.node - - steps: - - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Cache Rust build artifacts - uses: Swatinem/rust-cache@v2 - with: - key: ${{ matrix.target }} - - - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Install dependencies - working-directory: sdks/nodejs - run: npm install - - - name: Build - working-directory: sdks/nodejs - shell: bash - run: | - JS_FLAGS="--js native-binding.js --dts native-binding.d.ts" - if [ "${{ matrix.target }}" = "aarch64-unknown-linux-musl" ]; then - # Satisfy host-side NAPI-RS linker check (will use real linker inside Docker) - sudo ln -s /usr/bin/true /usr/bin/aarch64-linux-musl-gcc - npx napi build --platform --release --target ${{ matrix.target }} --use-napi-cross $JS_FLAGS - elif [ "${{ matrix.cross }}" = "true" ]; then - npx napi build --platform --release --target ${{ matrix.target }} --use-napi-cross $JS_FLAGS - else - npx napi build --platform --release --target ${{ matrix.target }} $JS_FLAGS - fi - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: node-${{ matrix.target }} - path: sdks/nodejs/${{ matrix.artifact }} - if-no-files-found: error - - # The generated JS dispatch loader (native-binding.js/.d.ts) is - # platform-agnostic — it bundles runtime branches for every supported - # OS/arch/libc, not just the one that produced it (verified by - # inspecting its output). Upload it once, from whichever build happens - # to run first, for the publish job to assemble alongside all six - # `.node` binaries. Named `node-*` so it's picked up by the same - # download pattern as the per-target artifacts below. - - name: Upload JS dispatch loader - if: matrix.target == 'x86_64-unknown-linux-gnu' - uses: actions/upload-artifact@v4 - with: - name: node-js-binding - path: | - sdks/nodejs/native-binding.js - sdks/nodejs/native-binding.d.ts - if-no-files-found: error - - # --------------------------------------------------------------------------- - # Publish Job — waits for all native builds above - # --------------------------------------------------------------------------- - publish: - name: Publish to npm - needs: build-native - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - id-token: write # OIDC token for npm provenance - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - registry-url: https://registry.npmjs.org - - - name: Download all native artifacts - uses: actions/download-artifact@v4 - with: - pattern: node-* - merge-multiple: true - path: sdks/nodejs/ - - - name: Validate native binaries count - working-directory: sdks/nodejs - run: | - COUNT=$(ls checkgate.*.node | wc -l) - echo "Found $COUNT labeled native binaries." - if [ "$COUNT" -ne 6 ]; then - echo "Error: Expected 6 labeled native binaries, found $COUNT." - ls -l *.node - exit 1 - fi - if [ ! -f native-binding.js ] || [ ! -f native-binding.d.ts ]; then - echo "Error: native-binding.js/.d.ts missing — index.js requires these at runtime." - exit 1 - fi - - - name: Install dependencies - working-directory: sdks/nodejs - run: npm install - - # Actually require() the assembled package end to end — this is the - # exact failure mode that shipped silently before: index.js requiring a - # file that doesn't exist in the real multi-platform output. A missing - # native-binding.js, a stale `require('./index.node')`, or a renamed - # export would all be caught here instead of surfacing only when a - # consumer runs `npm install @checkgate/node`. - - name: Smoke test the assembled package - working-directory: sdks/nodejs - run: | - node -e " - const { CheckgateClient } = require('./index.js'); - if (typeof CheckgateClient !== 'function') throw new Error('CheckgateClient export missing'); - const client = new CheckgateClient({ serverUrl: 'http://localhost:1' }); - if (typeof client.isEnabled !== 'function') throw new Error('isEnabled method missing'); - if (typeof client.connect !== 'function') throw new Error('connect method missing'); - console.log('Smoke test passed — native binding resolved and CheckgateClient constructed.'); - " - - - name: Set version from tag - working-directory: sdks/nodejs - run: | - TAG="${{ github.ref_name }}" - VERSION="${TAG#v}" - npm version "$VERSION" --no-git-tag-version --allow-same-version - - - name: Publish @checkgate/node - working-directory: sdks/nodejs - run: npm publish --access public --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release-sdk-react-native.yml b/.github/workflows/release-sdk-react-native.yml deleted file mode 100644 index be4290b..0000000 --- a/.github/workflows/release-sdk-react-native.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Release React Native SDK - -# Triggered by version tags: v1.2.3 -on: - push: - tags: ["v*.*.*"] - -# The React Native SDK ships JS + C++/Rust source files to npm. -# Native compilation (JSI .so / .a) happens inside the consuming app's -# Xcode / Gradle build via the included CMakeLists.txt / Podspec, -# so no cross-compilation matrix is required here. - -jobs: - publish: - name: Publish to npm - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write # OIDC token for npm provenance - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - registry-url: https://registry.npmjs.org - - - name: Set version from tag - working-directory: sdks/react-native - run: | - TAG="${{ github.ref_name }}" - VERSION="${TAG#v}" - npm version "$VERSION" --no-git-tag-version --allow-same-version - - - name: Validate package integrity - working-directory: sdks/react-native - run: | - # Manually trigger bundling to ensure it works in CI environment - node scripts/bundle-rust.js - - # Check for critical files that must be in the npm package - if [ ! -d "rust-core/src" ]; then echo "Error: rust-core source missing!"; exit 1; fi - if [ ! -f "Checkgate.podspec" ]; then echo "Error: Podspec missing!"; exit 1; fi - if [ ! -d "android/src/main" ]; then echo "Error: Android configuration missing!"; exit 1; fi - echo "Package integrity verified." - - - name: Publish @checkgate/react-native - working-directory: sdks/react-native - run: npm publish --access public --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release-sdk-web.yml b/.github/workflows/release-sdk-web.yml deleted file mode 100644 index 4616701..0000000 --- a/.github/workflows/release-sdk-web.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Release Web SDK - -# Triggered by version tags: v1.2.3 -on: - push: - tags: ["v*.*.*"] - -jobs: - publish: - name: Build WASM and publish to npm - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write # OIDC token for npm provenance - steps: - - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown - - - name: Cache Rust build artifacts - uses: Swatinem/rust-cache@v2 - - - name: Install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - - - uses: actions/setup-node@v4 - with: - node-version: 22 - registry-url: https://registry.npmjs.org - - - name: Build WASM (web target) - working-directory: sdks/web - run: wasm-pack build --target web --out-name checkgate --out-dir dist --release - - - name: Set version from tag - working-directory: sdks/web - run: | - TAG="${{ github.ref_name }}" - VERSION="${TAG#v}" - npm version "$VERSION" --no-git-tag-version --allow-same-version - - - name: Bundle Rust Core - working-directory: sdks/web - run: node scripts/bundle-rust.cjs - - - name: Remove wasm-pack ignore file - working-directory: sdks/web - run: rm -f dist/.gitignore - - - name: Validate WASM package integrity - working-directory: sdks/web - run: | - if [ ! -f "dist/checkgate_bg.wasm" ]; then echo "Error: WASM binary missing!"; exit 1; fi - if [ ! -f "dist/checkgate.js" ]; then echo "Error: JS wrapper missing!"; exit 1; fi - if [ ! -f "core.js" ]; then echo "Error: server-side core export missing!"; exit 1; fi - echo "WASM package integrity verified." - - # Actually run the engine — @checkgate/edge and @checkgate/ssr depend on - # this export, so a broken init path must never reach the registry. - - name: Smoke-test the server-side core export - working-directory: sdks/web - run: node scripts/smoke-core.mjs - - - name: Publish @checkgate/web - working-directory: sdks/web - run: npm publish --access public --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release-ssr.yml b/.github/workflows/release-ssr.yml deleted file mode 100644 index 8f06b40..0000000 --- a/.github/workflows/release-ssr.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Release SSR - -# Triggered by version tags: v1.2.3 (shared convention with the SDK releases). -on: - push: - tags: ["v*.*.*"] - -jobs: - publish: - name: Publish @checkgate/ssr to npm - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - id-token: write # OIDC token for npm provenance - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - registry-url: https://registry.npmjs.org - - # @checkgate/ssr has zero runtime dependencies — no install step needed. - - name: Run tests - working-directory: ssr - run: node --test - - - name: Set version from tag - working-directory: ssr - run: | - TAG="${{ github.ref_name }}" - VERSION="${TAG#v}" - npm version "$VERSION" --no-git-tag-version --allow-same-version - - - name: Publish @checkgate/ssr - working-directory: ssr - run: npm publish --access public --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a4ca942 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,650 @@ +name: Release + +# One workflow for the entire release, triggered by a version tag: v1.2.3 +# +# It runs in two strict phases: +# +# Phase 1 — VERIFY. Every package is tested, every artifact is built, and +# every packaging step is dry-run. Nothing is published. +# Phase 2 — PUBLISH. Each publish job `needs` *all* of phase 1, so a single +# failing test or broken build anywhere stops the whole release +# before a single package reaches a registry. +# +# This replaces the previous eight independent tag-triggered workflows, which +# each published on their own and could leave a version half-released. +# +# Note: registries have no cross-package transaction, so a failure *during* +# phase 2 (a registry outage mid-publish) can still leave a partial release. +# Phase 1 exists to make that the only remaining window. + +on: + push: + tags: ["v*.*.*"] + +# Never let two releases run against each other. +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ghcr.io/${{ github.repository }} + +jobs: + # =========================================================================== + # PHASE 1 — VERIFY. No publishing happens in any job below. + # =========================================================================== + + # Zero-dependency JS packages: cli, edge, ssr (+ react-native packaging). + verify-js: + name: Verify — JS packages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: CLI — tests + working-directory: cli + run: node --test + + # End-to-end sanity: generate all three languages from the fixture so a + # broken generator can never ship. + - name: CLI — smoke-test codegen + working-directory: cli + run: node bin/checkgate.js typegen --lang ts,dart,rust --input test/fixtures/flags.json --out-dir /tmp/checkgate-gen + + - name: Edge — tests + working-directory: edge + run: node --test + + - name: SSR — tests + working-directory: ssr + run: node --test + + - name: React Native — packaging integrity + working-directory: sdks/react-native + run: | + node scripts/bundle-rust.js + if [ ! -d "rust-core/src" ]; then echo "Error: rust-core source missing!"; exit 1; fi + if [ ! -f "Checkgate.podspec" ]; then echo "Error: Podspec missing!"; exit 1; fi + if [ ! -d "android/src/main" ]; then echo "Error: Android configuration missing!"; exit 1; fi + echo "Package integrity verified." + + # Build the native .node binary for each platform in parallel. + build-native: + name: Verify — native ${{ matrix.target }} + runs-on: ${{ matrix.os }} + strategy: + # Run every target to completion so one failure surfaces all of them. + # The job still fails, which is what gates phase 2. + fail-fast: false + matrix: + include: + # Linux x64 (glibc) + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + artifact: checkgate.linux-x64-gnu.node + + # Linux arm64 (glibc) — cross-compile via napi-rs cross toolchain + - os: ubuntu-latest + target: aarch64-unknown-linux-gnu + artifact: checkgate.linux-arm64-gnu.node + cross: true + + # Linux x64 (musl — Alpine) — cross-compile via napi-rs cross toolchain + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + artifact: checkgate.linux-x64-musl.node + cross: true + + # macOS x64 — cross-compile natively from arm64 runner + - os: macos-latest + target: x86_64-apple-darwin + artifact: checkgate.darwin-x64.node + + # macOS arm64 (Apple Silicon) + - os: macos-latest + target: aarch64-apple-darwin + artifact: checkgate.darwin-arm64.node + + # Windows x64 + - os: windows-latest + target: x86_64-pc-windows-msvc + artifact: checkgate.win32-x64-msvc.node + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Cache Rust build artifacts + uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.target }} + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install dependencies + working-directory: sdks/nodejs + run: npm install + + - name: Build + working-directory: sdks/nodejs + shell: bash + run: | + JS_FLAGS="--js native-binding.js --dts native-binding.d.ts" + if [ "${{ matrix.target }}" = "aarch64-unknown-linux-musl" ]; then + # Satisfy host-side NAPI-RS linker check (will use real linker inside Docker) + sudo ln -s /usr/bin/true /usr/bin/aarch64-linux-musl-gcc + npx napi build --platform --release --target ${{ matrix.target }} --use-napi-cross $JS_FLAGS + elif [ "${{ matrix.cross }}" = "true" ]; then + npx napi build --platform --release --target ${{ matrix.target }} --use-napi-cross $JS_FLAGS + else + npx napi build --platform --release --target ${{ matrix.target }} $JS_FLAGS + fi + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: node-${{ matrix.target }} + path: sdks/nodejs/${{ matrix.artifact }} + if-no-files-found: error + + # The generated JS dispatch loader (native-binding.js/.d.ts) is + # platform-agnostic — it bundles runtime branches for every supported + # OS/arch/libc, not just the one that produced it. Upload it once for the + # publish job to assemble alongside all six `.node` binaries. + - name: Upload JS dispatch loader + if: matrix.target == 'x86_64-unknown-linux-gnu' + uses: actions/upload-artifact@v4 + with: + name: node-js-binding + path: | + sdks/nodejs/native-binding.js + sdks/nodejs/native-binding.d.ts + if-no-files-found: error + + # Build the WASM engine and prove it actually runs before anything publishes. + build-web: + name: Verify — web WASM + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + - name: Cache Rust build artifacts + uses: Swatinem/rust-cache@v2 + + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Build WASM (web target) + working-directory: sdks/web + run: wasm-pack build --target web --out-name checkgate --out-dir dist --release + + # wasm-pack drops a `*` .gitignore in dist/, which would exclude the whole + # directory from the published tarball. + - name: Remove wasm-pack ignore file + working-directory: sdks/web + run: rm -f dist/.gitignore + + - name: Validate WASM package integrity + working-directory: sdks/web + run: | + if [ ! -f "dist/checkgate_bg.wasm" ]; then echo "Error: WASM binary missing!"; exit 1; fi + if [ ! -f "dist/checkgate.js" ]; then echo "Error: JS wrapper missing!"; exit 1; fi + if [ ! -f "core.js" ]; then echo "Error: server-side core export missing!"; exit 1; fi + echo "WASM package integrity verified." + + # Actually run the engine — @checkgate/edge and @checkgate/ssr depend on + # this export, so a broken init path must never reach the registry. + - name: Smoke-test the server-side core export + working-directory: sdks/web + run: node scripts/smoke-core.mjs + + - name: Upload built WASM + uses: actions/upload-artifact@v4 + with: + name: web-dist + path: sdks/web/dist + if-no-files-found: error + + # pub.dev packaging is validated with a dry run — no credentials needed. + verify-flutter: + name: Verify — Flutter package + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Flutter (stable) + uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + + - name: Install Dart dependencies + working-directory: sdks/flutter/dart + run: dart pub get + + - name: Run Dart tests + working-directory: sdks/flutter/dart + run: flutter test + + - name: Validate package integrity + working-directory: sdks/flutter/dart + run: | + node scripts/bundle-rust.js + if [ ! -d "rust-core/src" ]; then echo "Error: rust-core missing!"; exit 1; fi + if [ ! -d "rust/src" ]; then echo "Error: Bridge source missing!"; exit 1; fi + # Satisfy dart pub's "clean git state" requirement + git config user.name "github-actions" + git config user.email "github-actions@github.com" + git add . + git commit -m "chore: temporary commit for release packaging" + echo "Flutter package integrity and git state verified." + + - name: Validate pub.dev score (dry run) + working-directory: sdks/flutter/dart + run: dart pub publish --dry-run + + # Build the image without pushing, so a broken or vulnerable image blocks the + # release instead of being discovered after the npm packages are already live. + build-docker: + name: Verify — Docker image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU (multi-arch) + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build (no push) and load for scanning + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + # Single-arch for the scan — the multi-arch build/push happens in + # phase 2 and reuses this same GHA cache. + platforms: linux/amd64 + push: false + load: true + tags: ${{ env.IMAGE_NAME }}:scan + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ env.IMAGE_NAME }}:scan + format: 'table' + exit-code: '1' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL' + + # =========================================================================== + # PHASE 2 — PUBLISH. Every job below waits for *all* of phase 1. + # =========================================================================== + + publish-node: + name: Publish — @checkgate/node + needs: [verify-js, build-native, build-web, verify-flutter, build-docker] + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # OIDC token for npm provenance + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + + - name: Download all native artifacts + uses: actions/download-artifact@v4 + with: + pattern: node-* + merge-multiple: true + path: sdks/nodejs/ + + - name: Validate native binaries count + working-directory: sdks/nodejs + run: | + COUNT=$(ls checkgate.*.node | wc -l) + echo "Found $COUNT labeled native binaries." + if [ "$COUNT" -ne 6 ]; then + echo "Error: Expected 6 labeled native binaries, found $COUNT." + ls -l *.node + exit 1 + fi + if [ ! -f native-binding.js ] || [ ! -f native-binding.d.ts ]; then + echo "Error: native-binding.js/.d.ts missing — index.js requires these at runtime." + exit 1 + fi + + - name: Install dependencies + working-directory: sdks/nodejs + run: npm install + + # Actually require() the assembled package end to end — a missing + # native-binding.js or a renamed export is caught here instead of + # surfacing when a consumer runs `npm install @checkgate/node`. + - name: Smoke test the assembled package + working-directory: sdks/nodejs + run: | + node -e " + const { CheckgateClient } = require('./index.js'); + if (typeof CheckgateClient !== 'function') throw new Error('CheckgateClient export missing'); + const client = new CheckgateClient({ serverUrl: 'http://localhost:1' }); + if (typeof client.isEnabled !== 'function') throw new Error('isEnabled method missing'); + if (typeof client.connect !== 'function') throw new Error('connect method missing'); + console.log('Smoke test passed — native binding resolved and CheckgateClient constructed.'); + " + + - name: Set version from tag + working-directory: sdks/nodejs + run: | + TAG="${{ github.ref_name }}" + VERSION="${TAG#v}" + npm version "$VERSION" --no-git-tag-version --allow-same-version + + - name: Publish @checkgate/node + working-directory: sdks/nodejs + run: npm publish --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + publish-web: + name: Publish — @checkgate/web + needs: [verify-js, build-native, build-web, verify-flutter, build-docker] + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # OIDC token for npm provenance + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + + - name: Download built WASM + uses: actions/download-artifact@v4 + with: + name: web-dist + path: sdks/web/dist + + - name: Set version from tag + working-directory: sdks/web + run: | + TAG="${{ github.ref_name }}" + VERSION="${TAG#v}" + npm version "$VERSION" --no-git-tag-version --allow-same-version + + - name: Bundle Rust Core + working-directory: sdks/web + run: node scripts/bundle-rust.cjs + + - name: Re-verify packaged artifacts + working-directory: sdks/web + run: | + rm -f dist/.gitignore + if [ ! -f "dist/checkgate_bg.wasm" ]; then echo "Error: WASM binary missing!"; exit 1; fi + if [ ! -f "core.js" ]; then echo "Error: server-side core export missing!"; exit 1; fi + node scripts/smoke-core.mjs + + - name: Publish @checkgate/web + working-directory: sdks/web + run: npm publish --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + publish-cli: + name: Publish — @checkgate/cli + needs: [verify-js, build-native, build-web, verify-flutter, build-docker] + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # OIDC token for npm provenance + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + + - name: Set version from tag + working-directory: cli + run: | + TAG="${{ github.ref_name }}" + VERSION="${TAG#v}" + npm version "$VERSION" --no-git-tag-version --allow-same-version + + - name: Publish @checkgate/cli + working-directory: cli + run: npm publish --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + publish-edge: + name: Publish — @checkgate/edge + needs: [verify-js, build-native, build-web, verify-flutter, build-docker] + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # OIDC token for npm provenance + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + + - name: Set version from tag + working-directory: edge + run: | + TAG="${{ github.ref_name }}" + VERSION="${TAG#v}" + npm version "$VERSION" --no-git-tag-version --allow-same-version + + - name: Publish @checkgate/edge + working-directory: edge + run: npm publish --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + publish-ssr: + name: Publish — @checkgate/ssr + needs: [verify-js, build-native, build-web, verify-flutter, build-docker] + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # OIDC token for npm provenance + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + + - name: Set version from tag + working-directory: ssr + run: | + TAG="${{ github.ref_name }}" + VERSION="${TAG#v}" + npm version "$VERSION" --no-git-tag-version --allow-same-version + + - name: Publish @checkgate/ssr + working-directory: ssr + run: npm publish --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + publish-react-native: + name: Publish — @checkgate/react-native + needs: [verify-js, build-native, build-web, verify-flutter, build-docker] + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # OIDC token for npm provenance + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + + - name: Set version from tag + working-directory: sdks/react-native + run: | + TAG="${{ github.ref_name }}" + VERSION="${TAG#v}" + npm version "$VERSION" --no-git-tag-version --allow-same-version + + - name: Bundle Rust core into the package + working-directory: sdks/react-native + run: node scripts/bundle-rust.js + + - name: Publish @checkgate/react-native + working-directory: sdks/react-native + run: npm publish --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + publish-flutter: + name: Publish — checkgate_flutter (pub.dev) + needs: [verify-js, build-native, build-web, verify-flutter, build-docker] + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Install Flutter (stable) + uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + + - name: Install Dart dependencies + working-directory: sdks/flutter/dart + run: dart pub get + + - name: Prepare package + working-directory: sdks/flutter/dart + run: | + node scripts/bundle-rust.js + if [ ! -d "rust-core/src" ]; then echo "Error: rust-core missing!"; exit 1; fi + if [ ! -d "rust/src" ]; then echo "Error: Bridge source missing!"; exit 1; fi + # Satisfy dart pub's "clean git state" requirement + git config user.name "github-actions" + git config user.email "github-actions@github.com" + git add . + git commit -m "chore: temporary commit for release packaging" + + - name: Set up pub.dev credentials + run: | + mkdir -p ~/.config/dart + echo '${{ secrets.PUB_CREDENTIALS }}' > ~/.config/dart/pub-credentials.json + + - name: Publish to pub.dev + working-directory: sdks/flutter/dart + run: dart pub publish --force + + publish-docker: + name: Publish — Docker image + needs: [verify-js, build-native, build-web, verify-flutter, build-docker] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write # Required for Cosign keyless signing + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU (multi-arch) + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Install Cosign + uses: sigstore/cosign-installer@v3.8.0 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate image tags + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - name: Build and push + id: build-push + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Sign the image with Cosign + run: | + cosign sign --yes ${{ env.IMAGE_NAME }}@${{ steps.build-push.outputs.digest }} + + # =========================================================================== + # Final status — one green check that means "the whole release succeeded". + # =========================================================================== + release-complete: + name: Release complete + needs: + - publish-node + - publish-web + - publish-cli + - publish-edge + - publish-ssr + - publish-react-native + - publish-flutter + - publish-docker + runs-on: ubuntu-latest + steps: + - name: Summary + run: | + echo "All packages published for ${{ github.ref_name }}:" >> $GITHUB_STEP_SUMMARY + echo "- npm: @checkgate/{node,web,cli,edge,ssr,react-native}" >> $GITHUB_STEP_SUMMARY + echo "- pub.dev: checkgate_flutter" >> $GITHUB_STEP_SUMMARY + echo "- ghcr.io: ${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY From 4c5a5c81885e49f9098925703c155ead0a4efb80 Mon Sep 17 00:00:00 2001 From: Dennis Paler Date: Sat, 18 Jul 2026 21:26:27 +0800 Subject: [PATCH 2/4] chore: dashboard migrate to bun --- .github/workflows/ci.yml | 17 +- .gitignore | 3 +- CONTRIBUTING.md | 20 +- Cargo.toml | 2 +- Dockerfile | 10 +- cli/package.json | 2 +- dashboard/.npmrc | 1 - dashboard/bun.lock | 582 ++++ dashboard/package.json | 4 +- dashboard/pnpm-lock.yaml | 2451 ----------------- dashboard/src/App.tsx | 19 +- dashboard/src/components/Drawer.tsx | 118 + .../FlagForm.tsx} | 653 ++--- dashboard/src/components/Sidebar.test.tsx | 82 +- dashboard/src/components/Sidebar.tsx | 209 +- dashboard/src/components/TopBar.tsx | 2 - dashboard/src/pages/Dashboard.tsx | 4 +- dashboard/src/pages/EnvironmentDiff.tsx | 2 +- dashboard/src/pages/FlagList.test.tsx | 124 + dashboard/src/pages/FlagList.tsx | 83 +- dashboard/tsconfig.app.tsbuildinfo | 2 +- edge/package.json | 2 +- sdks/flutter/dart/CHANGELOG.md | 4 + sdks/flutter/dart/pubspec.yaml | 2 +- sdks/nodejs/package.json | 2 +- sdks/react-native/package.json | 2 +- sdks/web/package.json | 2 +- ssr/package.json | 2 +- 28 files changed, 1527 insertions(+), 2879 deletions(-) delete mode 100644 dashboard/.npmrc create mode 100644 dashboard/bun.lock delete mode 100644 dashboard/pnpm-lock.yaml create mode 100644 dashboard/src/components/Drawer.tsx rename dashboard/src/{pages/FlagEditor.tsx => components/FlagForm.tsx} (54%) create mode 100644 dashboard/src/pages/FlagList.test.tsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d92849..c9d3bca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,28 +167,21 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + - uses: oven-sh/setup-bun@v2 with: - version: 10 - run_install: false - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: pnpm - cache-dependency-path: dashboard/pnpm-lock.yaml + bun-version: 1.3.14 - name: Install dependencies working-directory: dashboard - run: pnpm install --frozen-lockfile + run: bun install --frozen-lockfile - name: Type-check and build working-directory: dashboard - run: pnpm build + run: bun run build - name: Run unit and integration tests working-directory: dashboard - run: pnpm test -- --run + run: bun run test --run # --------------------------------------------------------------------------- # CLI — @checkgate/cli codegen tests (zero deps, Node's built-in test runner) diff --git a/.gitignore b/.gitignore index a46daa0..0b20a22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,10 @@ # Rust target/ -# Node / pnpm +# Node / Bun dashboard/node_modules/ docs/node_modules/ docs/.vitepress/dist/ -dashboard/.pnpm-store/ server/public/ # macOS diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0707c60..2f2970a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,8 @@ Thanks for your interest in contributing. This document covers how to get set up ## Prerequisites - Rust 1.88+ -- Node.js 18+ +- Node.js 18+ (SDKs, CLI, docs) +- Bun 1.3+ (dashboard) - Docker and Docker Compose (for integration tests) - `wasm-pack` (browser SDK) - `@napi-rs/cli` (Node.js SDK) @@ -30,10 +31,25 @@ sdks/ browser/ wasm-bindgen / wasm-pack react-native/ JSI C++ bridge flutter/ dart:ffi binding -dashboard/ Next.js control-plane UI +dashboard/ Vite + React control-plane UI (Bun) docs/ VitePress documentation ``` +### Dashboard + +The dashboard uses Bun as both package manager and runtime: + +```bash +cd dashboard +bun install +bun run dev # dev server on :5173, proxies /api to :3000 +bun run build # type-check + build into ../server/public +bun run test --run # vitest suite +``` + +Use `bun run test`, not `bun test` — the latter invokes Bun's own test runner, +which does not understand the vitest setup in `src/setupTests.ts`. + ## Making changes 1. Fork the repo and create a branch from `main`. diff --git a/Cargo.toml b/Cargo.toml index 80c9ff3..f96cbc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,4 @@ members = [ resolver = "2" [workspace.package] -version = "0.1.21" +version = "0.1.22" diff --git a/Dockerfile b/Dockerfile index bee2792..8fe7b71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,16 +8,14 @@ # --------------------------------------------------------------------------- # Stage 1 — build dashboard # --------------------------------------------------------------------------- -FROM node:22-slim AS dashboard-builder - -RUN npm install -g pnpm@10 +FROM oven/bun:1-slim AS dashboard-builder WORKDIR /app/dashboard -COPY dashboard/package.json dashboard/pnpm-lock.yaml* ./ -RUN pnpm install --frozen-lockfile +COPY dashboard/package.json dashboard/bun.lock* ./ +RUN bun install --frozen-lockfile COPY dashboard/ . -RUN pnpm build +RUN bun run build # → emits to ../server/public which maps to /app/server/public # --------------------------------------------------------------------------- diff --git a/cli/package.json b/cli/package.json index ce9411f..4b1aef8 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@checkgate/cli", - "version": "0.1.21", + "version": "0.1.22", "description": "Checkgate command-line tools — generate type-safe flag accessors for TypeScript, Dart, and Rust from your flag definitions.", "type": "module", "bin": { diff --git a/dashboard/.npmrc b/dashboard/.npmrc deleted file mode 100644 index 3b4c550..0000000 --- a/dashboard/.npmrc +++ /dev/null @@ -1 +0,0 @@ -allow-build[]=esbuild diff --git a/dashboard/bun.lock b/dashboard/bun.lock new file mode 100644 index 0000000..40dbece --- /dev/null +++ b/dashboard/bun.lock @@ -0,0 +1,582 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "checkgate-dashboard", + "dependencies": { + "lucide-react": "^0.477.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^6.30.4", + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.7.0", + "autoprefixer": "^10.5.4", + "jsdom": "^29.1.1", + "postcss": "^8.5.19", + "tailwindcss": "^3.4.19", + "typescript": "^5.9.3", + "vite": "^6.4.3", + "vitest": "^4.1.10", + }, + }, + }, + "trustedDependencies": [ + "esbuild", + ], + "packages": { + "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], + + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="], + + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "", { "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" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="], + + "@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="], + + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="], + + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@6.1.0", "", {}, "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg=="], + + "@csstools/css-calc": ["@csstools/css-calc@3.2.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.9", "", { "dependencies": { "@csstools/color-helpers": "^6.1.0", "@csstools/css-calc": "^3.2.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], + + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.6", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@remix-run/router": ["@remix-run/router@1.23.3", "", {}, "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "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" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], + + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "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" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], + + "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], + + "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + + "@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + + "@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="], + + "@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], + + "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "autoprefixer": ["autoprefixer@10.5.4", "", { "dependencies": { "browserslist": "^4.28.6", "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.43", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ=="], + + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.6", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", "electron-to-chromium": "^1.5.389", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.393", "", {}, "sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg=="], + + "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsdom": ["jsdom@29.1.1", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.3", "@exodus/bytes": "^1.15.0", "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", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], + + "lucide-react": ["lucide-react@0.477.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yCf7aYxerFZAbd8jHJxjwe1j7jEMPptjnaOqdYeirFnEy85cNR3/L+o0I875CYFYya+eEVzZSbNuRk8BZPDpVw=="], + + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + + "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], + + "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="], + + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + + "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + + "react-router": ["react-router@6.30.4", "", { "dependencies": { "@remix-run/router": "1.23.3" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA=="], + + "react-router-dom": ["react-router-dom@6.30.4", "", { "dependencies": { "@remix-run/router": "1.23.3", "react-router": "6.30.4" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q=="], + + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], + + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "tldts": ["tldts@7.4.9", "", { "dependencies": { "tldts-core": "^7.4.9" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA=="], + + "tldts-core": ["tldts-core@7.4.9", "", {}, "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tough-cookie": ["tough-cookie@6.0.2", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA=="], + + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], + + "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + + "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + } +} diff --git a/dashboard/package.json b/dashboard/package.json index 14fb3e6..4badf07 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -1,8 +1,10 @@ { "name": "checkgate-dashboard", - "version": "0.1.21", + "version": "0.1.22", "private": true, "type": "module", + "packageManager": "bun@1.3.14", + "trustedDependencies": ["esbuild"], "scripts": { "dev": "vite", "build": "tsc -b && vite build", diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml deleted file mode 100644 index 18d6e2f..0000000 --- a/dashboard/pnpm-lock.yaml +++ /dev/null @@ -1,2451 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - lucide-react: - specifier: ^0.477.0 - version: 0.477.0(react@19.2.7) - react: - specifier: ^19.2.7 - version: 19.2.7 - react-dom: - specifier: ^19.2.7 - version: 19.2.7(react@19.2.7) - react-router-dom: - specifier: ^6.30.4 - version: 6.30.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - devDependencies: - '@testing-library/jest-dom': - specifier: ^6.9.1 - version: 6.9.1 - '@testing-library/react': - specifier: ^16.3.2 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@testing-library/user-event': - specifier: ^14.6.1 - version: 14.6.1(@testing-library/dom@10.4.1) - '@types/react': - specifier: ^19.2.17 - version: 19.2.17 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.17) - '@vitejs/plugin-react': - specifier: ^4.7.0 - version: 4.7.0(vite@6.4.3(jiti@1.21.7)) - autoprefixer: - specifier: ^10.5.4 - version: 10.5.4(postcss@8.5.19) - jsdom: - specifier: ^29.1.1 - version: 29.1.1 - postcss: - specifier: ^8.5.19 - version: 8.5.19 - tailwindcss: - specifier: ^3.4.19 - version: 3.4.19 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - vite: - specifier: ^6.4.3 - version: 6.4.3(jiti@1.21.7) - vitest: - specifier: ^4.1.10 - version: 4.1.10(jsdom@29.1.1)(vite@6.4.3(jiti@1.21.7)) - -packages: - - '@adobe/css-tools@4.5.0': - resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} - - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} - - '@asamuzakjp/css-color@5.1.11': - resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - '@asamuzakjp/dom-selector@7.1.1': - resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - '@asamuzakjp/generational-cache@1.0.1': - resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - '@asamuzakjp/nwsapi@2.3.9': - resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} - - '@babel/code-frame@7.29.7': - resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.29.7': - resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.7': - resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.7': - resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.29.7': - resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.29.7': - resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.29.7': - resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.29.7': - resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-plugin-utils@7.29.7': - resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.29.7': - resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.29.7': - resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.29.7': - resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.29.7': - resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.29.7': - resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-transform-react-jsx-self@7.29.7': - resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-source@7.29.7': - resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/runtime@7.29.7': - resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.29.7': - resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.7': - resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} - engines: {node: '>=6.9.0'} - - '@bramus/specificity@2.4.2': - resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} - hasBin: true - - '@csstools/color-helpers@6.1.0': - resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} - engines: {node: '>=20.19.0'} - - '@csstools/css-calc@3.2.1': - resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} - engines: {node: '>=20.19.0'} - peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 - - '@csstools/css-color-parser@4.1.9': - resolution: {integrity: sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==} - engines: {node: '>=20.19.0'} - peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 - - '@csstools/css-parser-algorithms@4.0.0': - resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} - engines: {node: '>=20.19.0'} - peerDependencies: - '@csstools/css-tokenizer': ^4.0.0 - - '@csstools/css-syntax-patches-for-csstree@1.1.6': - resolution: {integrity: sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==} - peerDependencies: - css-tree: ^3.2.1 - peerDependenciesMeta: - css-tree: - optional: true - - '@csstools/css-tokenizer@4.0.0': - resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} - engines: {node: '>=20.19.0'} - - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@exodus/bytes@1.15.1': - resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - peerDependencies: - '@noble/hashes': ^1.8.0 || ^2.0.0 - peerDependenciesMeta: - '@noble/hashes': - optional: true - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@remix-run/router@1.23.3': - resolution: {integrity: sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==} - engines: {node: '>=14.0.0'} - - '@rolldown/pluginutils@1.0.0-beta.27': - resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} - - '@rollup/rollup-android-arm-eabi@4.62.2': - resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.62.2': - resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.62.2': - resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.62.2': - resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.62.2': - resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.62.2': - resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.62.2': - resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.62.2': - resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.62.2': - resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.62.2': - resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loong64-gnu@4.62.2': - resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-loong64-musl@4.62.2': - resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-ppc64-gnu@4.62.2': - resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-ppc64-musl@4.62.2': - resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.62.2': - resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.62.2': - resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.62.2': - resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.62.2': - resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.62.2': - resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-openbsd-x64@4.62.2': - resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.62.2': - resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.62.2': - resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.62.2': - resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.62.2': - resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.62.2': - resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} - cpu: [x64] - os: [win32] - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@testing-library/dom@10.4.1': - resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} - engines: {node: '>=18'} - - '@testing-library/jest-dom@6.9.1': - resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - - '@testing-library/react@16.3.2': - resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} - 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 - - '@testing-library/user-event@14.6.1': - resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' - - '@types/aria-query@5.0.4': - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - - '@vitejs/plugin-react@4.7.0': - resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - - '@vitest/expect@4.1.10': - resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - - '@vitest/mocker@4.1.10': - resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@4.1.10': - resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - - '@vitest/runner@4.1.10': - resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - - '@vitest/snapshot@4.1.10': - resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - - '@vitest/spy@4.1.10': - resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - - '@vitest/utils@4.1.10': - resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - autoprefixer@10.5.4: - resolution: {integrity: sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - - baseline-browser-mapping@2.10.43: - resolution: {integrity: sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - bidi-js@1.0.3: - resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.28.6: - resolution: {integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} - - caniuse-lite@1.0.30001806: - resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} - - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} - engines: {node: '>=18'} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - css-tree@3.2.1: - resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - - css.escape@1.5.1: - resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - data-urls@7.0.0: - resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decimal.js@10.6.0: - resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - - didyoumean@1.2.2: - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - - dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - - dom-accessibility-api@0.6.3: - resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - - electron-to-chromium@1.5.393: - resolution: {integrity: sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==} - - entities@8.0.0: - resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} - engines: {node: '>=20.19.0'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} - - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} - engines: {node: '>=18'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - expect-type@1.4.0: - resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} - engines: {node: '>=12.0.0'} - - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - fraction.js@5.3.4: - resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - hasown@2.0.4: - resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} - engines: {node: '>= 0.4'} - - html-encoding-sniffer@6.0.0: - resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-core-module@2.16.2: - resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} - engines: {node: '>= 0.4'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - - jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} - hasBin: true - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - jsdom@29.1.1: - resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} - peerDependencies: - canvas: ^3.0.0 - peerDependenciesMeta: - canvas: - optional: true - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - lru-cache@11.5.2: - resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} - engines: {node: 20 || >=22} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - lucide-react@0.477.0: - resolution: {integrity: sha512-yCf7aYxerFZAbd8jHJxjwe1j7jEMPptjnaOqdYeirFnEy85cNR3/L+o0I875CYFYya+eEVzZSbNuRk8BZPDpVw==} - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - mdn-data@2.27.1: - resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - node-releases@2.0.51: - resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} - engines: {node: '>=18'} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - - obug@2.1.4: - resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} - engines: {node: '>=12.20.0'} - - parse5@8.0.1: - resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.2: - resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} - engines: {node: '>=8.6'} - - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} - - pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} - - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - - postcss-import@15.1.0: - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} - peerDependencies: - postcss: ^8.0.0 - - postcss-js@4.1.0: - resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} - engines: {node: ^12 || ^14 || >= 16} - peerDependencies: - postcss: ^8.4.21 - - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true - - postcss-nested@6.2.0: - resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.2.14 - - postcss-selector-parser@6.1.4: - resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} - engines: {node: '>=4'} - - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - - postcss@8.5.19: - resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} - engines: {node: ^10 || ^12 || >=14} - - pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - react-dom@19.2.7: - resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} - peerDependencies: - react: ^19.2.7 - - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - - react-refresh@0.17.0: - resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} - engines: {node: '>=0.10.0'} - - react-router-dom@6.30.4: - resolution: {integrity: sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' - - react-router@6.30.4: - resolution: {integrity: sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: '>=16.8' - - react@19.2.7: - resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} - engines: {node: '>=0.10.0'} - - read-cache@1.0.0: - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - resolve@1.22.12: - resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} - engines: {node: '>= 0.4'} - hasBin: true - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rollup@4.62.2: - resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - saxes@6.0.0: - resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} - engines: {node: '>=v12.22.7'} - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@4.2.0: - resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} - - strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - - tailwindcss@3.4.19: - resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} - engines: {node: '>=14.0.0'} - hasBin: true - - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@1.2.4: - resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} - engines: {node: '>=18'} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - tinyrainbow@3.1.0: - resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} - engines: {node: '>=14.0.0'} - - tldts-core@7.4.9: - resolution: {integrity: sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==} - - tldts@7.4.9: - resolution: {integrity: sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==} - hasBin: true - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - tough-cookie@6.0.2: - resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} - engines: {node: '>=16'} - - tr46@6.0.0: - resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} - engines: {node: '>=20'} - - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - undici@7.28.0: - resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} - engines: {node: '>=20.18.1'} - - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - vite@6.4.3: - resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - jiti: '>=1.21.0' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@4.1.10: - resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.10 - '@vitest/browser-preview': 4.1.10 - '@vitest/browser-webdriverio': 4.1.10 - '@vitest/coverage-istanbul': 4.1.10 - '@vitest/coverage-v8': 4.1.10 - '@vitest/ui': 4.1.10 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/coverage-istanbul': - optional: true - '@vitest/coverage-v8': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - w3c-xmlserializer@5.0.0: - resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} - engines: {node: '>=18'} - - webidl-conversions@8.0.1: - resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} - engines: {node: '>=20'} - - whatwg-mimetype@5.0.0: - resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} - engines: {node: '>=20'} - - whatwg-url@16.0.1: - resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - xml-name-validator@5.0.0: - resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} - engines: {node: '>=18'} - - xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - -snapshots: - - '@adobe/css-tools@4.5.0': {} - - '@alloc/quick-lru@5.2.0': {} - - '@asamuzakjp/css-color@5.1.11': - dependencies: - '@asamuzakjp/generational-cache': 1.0.1 - '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@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) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - - '@asamuzakjp/dom-selector@7.1.1': - 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 - - '@asamuzakjp/generational-cache@1.0.1': {} - - '@asamuzakjp/nwsapi@2.3.9': {} - - '@babel/code-frame@7.29.7': - dependencies: - '@babel/helper-validator-identifier': 7.29.7 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.29.7': {} - - '@babel/core@7.29.7': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) - '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.29.7': - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.29.7': - dependencies: - '@babel/compat-data': 7.29.7 - '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.6 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.29.7': {} - - '@babel/helper-module-imports@7.29.7': - dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.29.7': {} - - '@babel/helper-string-parser@7.29.7': {} - - '@babel/helper-validator-identifier@7.29.7': {} - - '@babel/helper-validator-option@7.29.7': {} - - '@babel/helpers@7.29.7': - dependencies: - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - - '@babel/parser@7.29.7': - dependencies: - '@babel/types': 7.29.7 - - '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 - - '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 - - '@babel/runtime@7.29.7': {} - - '@babel/template@7.29.7': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - - '@babel/traverse@7.29.7': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.29.7': - dependencies: - '@babel/helper-string-parser': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - - '@bramus/specificity@2.4.2': - dependencies: - css-tree: 3.2.1 - - '@csstools/color-helpers@6.1.0': {} - - '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': - dependencies: - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - - '@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)': - dependencies: - '@csstools/color-helpers': 6.1.0 - '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - - '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': - dependencies: - '@csstools/css-tokenizer': 4.0.0 - - '@csstools/css-syntax-patches-for-csstree@1.1.6(css-tree@3.2.1)': - optionalDependencies: - css-tree: 3.2.1 - - '@csstools/css-tokenizer@4.0.0': {} - - '@esbuild/aix-ppc64@0.25.12': - optional: true - - '@esbuild/android-arm64@0.25.12': - optional: true - - '@esbuild/android-arm@0.25.12': - optional: true - - '@esbuild/android-x64@0.25.12': - optional: true - - '@esbuild/darwin-arm64@0.25.12': - optional: true - - '@esbuild/darwin-x64@0.25.12': - optional: true - - '@esbuild/freebsd-arm64@0.25.12': - optional: true - - '@esbuild/freebsd-x64@0.25.12': - optional: true - - '@esbuild/linux-arm64@0.25.12': - optional: true - - '@esbuild/linux-arm@0.25.12': - optional: true - - '@esbuild/linux-ia32@0.25.12': - optional: true - - '@esbuild/linux-loong64@0.25.12': - optional: true - - '@esbuild/linux-mips64el@0.25.12': - optional: true - - '@esbuild/linux-ppc64@0.25.12': - optional: true - - '@esbuild/linux-riscv64@0.25.12': - optional: true - - '@esbuild/linux-s390x@0.25.12': - optional: true - - '@esbuild/linux-x64@0.25.12': - optional: true - - '@esbuild/netbsd-arm64@0.25.12': - optional: true - - '@esbuild/netbsd-x64@0.25.12': - optional: true - - '@esbuild/openbsd-arm64@0.25.12': - optional: true - - '@esbuild/openbsd-x64@0.25.12': - optional: true - - '@esbuild/openharmony-arm64@0.25.12': - optional: true - - '@esbuild/sunos-x64@0.25.12': - optional: true - - '@esbuild/win32-arm64@0.25.12': - optional: true - - '@esbuild/win32-ia32@0.25.12': - optional: true - - '@esbuild/win32-x64@0.25.12': - optional: true - - '@exodus/bytes@1.15.1': {} - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 - - '@remix-run/router@1.23.3': {} - - '@rolldown/pluginutils@1.0.0-beta.27': {} - - '@rollup/rollup-android-arm-eabi@4.62.2': - optional: true - - '@rollup/rollup-android-arm64@4.62.2': - optional: true - - '@rollup/rollup-darwin-arm64@4.62.2': - optional: true - - '@rollup/rollup-darwin-x64@4.62.2': - optional: true - - '@rollup/rollup-freebsd-arm64@4.62.2': - optional: true - - '@rollup/rollup-freebsd-x64@4.62.2': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.62.2': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.62.2': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-x64-musl@4.62.2': - optional: true - - '@rollup/rollup-openbsd-x64@4.62.2': - optional: true - - '@rollup/rollup-openharmony-arm64@4.62.2': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.62.2': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.62.2': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.62.2': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.62.2': - optional: true - - '@standard-schema/spec@1.1.0': {} - - '@testing-library/dom@10.4.1': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/runtime': 7.29.7 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - picocolors: 1.1.1 - pretty-format: 27.5.1 - - '@testing-library/jest-dom@6.9.1': - dependencies: - '@adobe/css-tools': 4.5.0 - aria-query: 5.3.2 - css.escape: 1.5.1 - dom-accessibility-api: 0.6.3 - picocolors: 1.1.1 - redent: 3.0.0 - - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@babel/runtime': 7.29.7 - '@testing-library/dom': 10.4.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': - dependencies: - '@testing-library/dom': 10.4.1 - - '@types/aria-query@5.0.4': {} - - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.29.7 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - - '@types/babel__traverse@7.28.0': - dependencies: - '@babel/types': 7.29.7 - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/deep-eql@4.0.2': {} - - '@types/estree@1.0.9': {} - - '@types/react-dom@19.2.3(@types/react@19.2.17)': - dependencies: - '@types/react': 19.2.17 - - '@types/react@19.2.17': - dependencies: - csstype: 3.2.3 - - '@vitejs/plugin-react@4.7.0(vite@6.4.3(jiti@1.21.7))': - dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) - '@rolldown/pluginutils': 1.0.0-beta.27 - '@types/babel__core': 7.20.5 - react-refresh: 0.17.0 - vite: 6.4.3(jiti@1.21.7) - transitivePeerDependencies: - - supports-color - - '@vitest/expect@4.1.10': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - chai: 6.2.2 - tinyrainbow: 3.1.0 - - '@vitest/mocker@4.1.10(vite@6.4.3(jiti@1.21.7))': - dependencies: - '@vitest/spy': 4.1.10 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 6.4.3(jiti@1.21.7) - - '@vitest/pretty-format@4.1.10': - dependencies: - tinyrainbow: 3.1.0 - - '@vitest/runner@4.1.10': - dependencies: - '@vitest/utils': 4.1.10 - pathe: 2.0.3 - - '@vitest/snapshot@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - '@vitest/utils': 4.1.10 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.1.10': {} - - '@vitest/utils@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 - - ansi-regex@5.0.1: {} - - ansi-styles@5.2.0: {} - - any-promise@1.3.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.2 - - arg@5.0.2: {} - - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - - aria-query@5.3.2: {} - - assertion-error@2.0.1: {} - - autoprefixer@10.5.4(postcss@8.5.19): - dependencies: - browserslist: 4.28.6 - caniuse-lite: 1.0.30001806 - fraction.js: 5.3.4 - picocolors: 1.1.1 - postcss: 8.5.19 - postcss-value-parser: 4.2.0 - - baseline-browser-mapping@2.10.43: {} - - bidi-js@1.0.3: - dependencies: - require-from-string: 2.0.2 - - binary-extensions@2.3.0: {} - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.28.6: - dependencies: - baseline-browser-mapping: 2.10.43 - caniuse-lite: 1.0.30001806 - electron-to-chromium: 1.5.393 - node-releases: 2.0.51 - update-browserslist-db: 1.2.3(browserslist@4.28.6) - - camelcase-css@2.0.1: {} - - caniuse-lite@1.0.30001806: {} - - chai@6.2.2: {} - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - commander@4.1.1: {} - - convert-source-map@2.0.0: {} - - css-tree@3.2.1: - dependencies: - mdn-data: 2.27.1 - source-map-js: 1.2.1 - - css.escape@1.5.1: {} - - cssesc@3.0.0: {} - - csstype@3.2.3: {} - - data-urls@7.0.0: - dependencies: - whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 - transitivePeerDependencies: - - '@noble/hashes' - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - decimal.js@10.6.0: {} - - dequal@2.0.3: {} - - didyoumean@1.2.2: {} - - dlv@1.1.3: {} - - dom-accessibility-api@0.5.16: {} - - dom-accessibility-api@0.6.3: {} - - electron-to-chromium@1.5.393: {} - - entities@8.0.0: {} - - es-errors@1.3.0: {} - - es-module-lexer@2.3.1: {} - - esbuild@0.25.12: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 - - escalade@3.2.0: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.9 - - expect-type@1.4.0: {} - - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fastq@1.20.1: - dependencies: - reusify: 1.1.0 - - fdir@6.5.0(picomatch@4.0.5): - optionalDependencies: - picomatch: 4.0.5 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - fraction.js@5.3.4: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - gensync@1.0.0-beta.2: {} - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - hasown@2.0.4: - dependencies: - function-bind: 1.1.2 - - html-encoding-sniffer@6.0.0: - dependencies: - '@exodus/bytes': 1.15.1 - transitivePeerDependencies: - - '@noble/hashes' - - indent-string@4.0.0: {} - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-core-module@2.16.2: - dependencies: - hasown: 2.0.4 - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-number@7.0.0: {} - - is-potential-custom-element-name@1.0.1: {} - - jiti@1.21.7: {} - - js-tokens@4.0.0: {} - - jsdom@29.1.1: - dependencies: - '@asamuzakjp/css-color': 5.1.11 - '@asamuzakjp/dom-selector': 7.1.1 - '@bramus/specificity': 2.4.2 - '@csstools/css-syntax-patches-for-csstree': 1.1.6(css-tree@3.2.1) - '@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.5.2 - parse5: 8.0.1 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 6.0.2 - undici: 7.28.0 - w3c-xmlserializer: 5.0.0 - webidl-conversions: 8.0.1 - whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 - xml-name-validator: 5.0.0 - transitivePeerDependencies: - - '@noble/hashes' - - jsesc@3.1.0: {} - - json5@2.2.3: {} - - lilconfig@3.1.3: {} - - lines-and-columns@1.2.4: {} - - lru-cache@11.5.2: {} - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - lucide-react@0.477.0(react@19.2.7): - dependencies: - react: 19.2.7 - - lz-string@1.5.0: {} - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - mdn-data@2.27.1: {} - - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.2 - - min-indent@1.0.1: {} - - ms@2.1.3: {} - - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - - nanoid@3.3.16: {} - - node-releases@2.0.51: {} - - normalize-path@3.0.0: {} - - object-assign@4.1.1: {} - - object-hash@3.0.0: {} - - obug@2.1.4: {} - - parse5@8.0.1: - dependencies: - entities: 8.0.0 - - path-parse@1.0.7: {} - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@2.3.2: {} - - picomatch@4.0.5: {} - - pify@2.3.0: {} - - pirates@4.0.7: {} - - postcss-import@15.1.0(postcss@8.5.19): - dependencies: - postcss: 8.5.19 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.12 - - postcss-js@4.1.0(postcss@8.5.19): - dependencies: - camelcase-css: 2.0.1 - postcss: 8.5.19 - - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.19): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - jiti: 1.21.7 - postcss: 8.5.19 - - postcss-nested@6.2.0(postcss@8.5.19): - dependencies: - postcss: 8.5.19 - postcss-selector-parser: 6.1.4 - - postcss-selector-parser@6.1.4: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss-value-parser@4.2.0: {} - - postcss@8.5.19: - dependencies: - nanoid: 3.3.16 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - pretty-format@27.5.1: - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - - punycode@2.3.1: {} - - queue-microtask@1.2.3: {} - - react-dom@19.2.7(react@19.2.7): - dependencies: - react: 19.2.7 - scheduler: 0.27.0 - - react-is@17.0.2: {} - - react-refresh@0.17.0: {} - - react-router-dom@6.30.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - '@remix-run/router': 1.23.3 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-router: 6.30.4(react@19.2.7) - - react-router@6.30.4(react@19.2.7): - dependencies: - '@remix-run/router': 1.23.3 - react: 19.2.7 - - react@19.2.7: {} - - read-cache@1.0.0: - dependencies: - pify: 2.3.0 - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.2 - - redent@3.0.0: - dependencies: - indent-string: 4.0.0 - strip-indent: 3.0.0 - - require-from-string@2.0.2: {} - - resolve@1.22.12: - dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - reusify@1.1.0: {} - - rollup@4.62.2: - dependencies: - '@types/estree': 1.0.9 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.62.2 - '@rollup/rollup-android-arm64': 4.62.2 - '@rollup/rollup-darwin-arm64': 4.62.2 - '@rollup/rollup-darwin-x64': 4.62.2 - '@rollup/rollup-freebsd-arm64': 4.62.2 - '@rollup/rollup-freebsd-x64': 4.62.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 - '@rollup/rollup-linux-arm-musleabihf': 4.62.2 - '@rollup/rollup-linux-arm64-gnu': 4.62.2 - '@rollup/rollup-linux-arm64-musl': 4.62.2 - '@rollup/rollup-linux-loong64-gnu': 4.62.2 - '@rollup/rollup-linux-loong64-musl': 4.62.2 - '@rollup/rollup-linux-ppc64-gnu': 4.62.2 - '@rollup/rollup-linux-ppc64-musl': 4.62.2 - '@rollup/rollup-linux-riscv64-gnu': 4.62.2 - '@rollup/rollup-linux-riscv64-musl': 4.62.2 - '@rollup/rollup-linux-s390x-gnu': 4.62.2 - '@rollup/rollup-linux-x64-gnu': 4.62.2 - '@rollup/rollup-linux-x64-musl': 4.62.2 - '@rollup/rollup-openbsd-x64': 4.62.2 - '@rollup/rollup-openharmony-arm64': 4.62.2 - '@rollup/rollup-win32-arm64-msvc': 4.62.2 - '@rollup/rollup-win32-ia32-msvc': 4.62.2 - '@rollup/rollup-win32-x64-gnu': 4.62.2 - '@rollup/rollup-win32-x64-msvc': 4.62.2 - fsevents: 2.3.3 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - saxes@6.0.0: - dependencies: - xmlchars: 2.2.0 - - scheduler@0.27.0: {} - - semver@6.3.1: {} - - siginfo@2.0.0: {} - - source-map-js@1.2.1: {} - - stackback@0.0.2: {} - - std-env@4.2.0: {} - - strip-indent@3.0.0: - dependencies: - min-indent: 1.0.1 - - sucrase@3.35.1: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.17 - ts-interface-checker: 0.1.13 - - supports-preserve-symlinks-flag@1.0.0: {} - - symbol-tree@3.2.4: {} - - tailwindcss@3.4.19: - dependencies: - '@alloc/quick-lru': 5.2.0 - arg: 5.0.2 - chokidar: 3.6.0 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.3 - glob-parent: 6.0.2 - is-glob: 4.0.3 - jiti: 1.21.7 - lilconfig: 3.1.3 - micromatch: 4.0.8 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.1.1 - postcss: 8.5.19 - postcss-import: 15.1.0(postcss@8.5.19) - postcss-js: 4.1.0(postcss@8.5.19) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.19) - postcss-nested: 6.2.0(postcss@8.5.19) - postcss-selector-parser: 6.1.4 - resolve: 1.22.12 - sucrase: 3.35.1 - transitivePeerDependencies: - - tsx - - yaml - - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - - tinybench@2.9.0: {} - - tinyexec@1.2.4: {} - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 - - tinyrainbow@3.1.0: {} - - tldts-core@7.4.9: {} - - tldts@7.4.9: - dependencies: - tldts-core: 7.4.9 - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - tough-cookie@6.0.2: - dependencies: - tldts: 7.4.9 - - tr46@6.0.0: - dependencies: - punycode: 2.3.1 - - ts-interface-checker@0.1.13: {} - - typescript@5.9.3: {} - - undici@7.28.0: {} - - update-browserslist-db@1.2.3(browserslist@4.28.6): - dependencies: - browserslist: 4.28.6 - escalade: 3.2.0 - picocolors: 1.1.1 - - util-deprecate@1.0.2: {} - - vite@6.4.3(jiti@1.21.7): - dependencies: - esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 - postcss: 8.5.19 - rollup: 4.62.2 - tinyglobby: 0.2.17 - optionalDependencies: - fsevents: 2.3.3 - jiti: 1.21.7 - - vitest@4.1.10(jsdom@29.1.1)(vite@6.4.3(jiti@1.21.7)): - dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@6.4.3(jiti@1.21.7)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.3.1 - expect-type: 1.4.0 - magic-string: 0.30.21 - obug: 2.1.4 - pathe: 2.0.3 - picomatch: 4.0.5 - std-env: 4.2.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 6.4.3(jiti@1.21.7) - why-is-node-running: 2.3.0 - optionalDependencies: - jsdom: 29.1.1 - transitivePeerDependencies: - - msw - - w3c-xmlserializer@5.0.0: - dependencies: - xml-name-validator: 5.0.0 - - webidl-conversions@8.0.1: {} - - whatwg-mimetype@5.0.0: {} - - whatwg-url@16.0.1: - dependencies: - '@exodus/bytes': 1.15.1 - tr46: 6.0.0 - webidl-conversions: 8.0.1 - transitivePeerDependencies: - - '@noble/hashes' - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - xml-name-validator@5.0.0: {} - - xmlchars@2.2.0: {} - - yallist@3.1.1: {} diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 375819b..6986801 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -1,4 +1,4 @@ -import { Routes, Route, Navigate } from 'react-router-dom' +import { Routes, Route, Navigate, useParams } from 'react-router-dom' import { useAuth } from './context/AuthContext' import { ProjectProvider } from './context/ProjectContext' import { EnvironmentProvider } from './context/EnvironmentContext' @@ -8,7 +8,6 @@ import Setup from './pages/Setup' import AuditLog from './pages/AuditLog' import Dashboard from './pages/Dashboard' import FlagList from './pages/FlagList' -import FlagEditor from './pages/FlagEditor' import Scheduled from './pages/Scheduled' import SdkHealth from './pages/SdkHealth' import Segments from './pages/Segments' @@ -24,6 +23,18 @@ import Projects from './pages/Projects' import ProjectSettings from './pages/ProjectSettings' import Webhooks from './pages/Webhooks' +/** + * Flag create/edit moved from standalone pages into a slide-over on the list. + * These keep the old URLs working — bookmarks and links land on the list with + * the panel already open on the right flag. + */ +function RedirectToFlagPanel({ mode }: { readonly mode: 'new' | 'edit' }) { + const { key } = useParams<{ key?: string }>() + if (mode === 'new') return + if (!key) return + return +} + function RequireAuth({ children }: { readonly children: React.ReactNode }) { const { session, sessionLoading, isSetupComplete, setupLoading } = useAuth() // Must not act on isSetupComplete until the authoritative check resolves — @@ -85,8 +96,8 @@ export default function App() { } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/dashboard/src/components/Drawer.tsx b/dashboard/src/components/Drawer.tsx new file mode 100644 index 0000000..799410c --- /dev/null +++ b/dashboard/src/components/Drawer.tsx @@ -0,0 +1,118 @@ +import { useEffect, useRef, useState } from 'react' +import { X } from 'lucide-react' + +/** + * Right-hand slide-over panel. + * + * Rendered inline (not via a portal) — the app has no stacking contexts that + * would clip a `fixed` child, and staying in the tree keeps React context + * (environment, auth) available to the contents without re-providing it. + */ +export default function Drawer({ + open, + title, + subtitle, + onClose, + children, + width = 'max-w-3xl', +}: { + readonly open: boolean + readonly title: string + readonly subtitle?: React.ReactNode + readonly onClose: () => void + /** + * Fills the area below the header as a `min-h-0` flex column — the consumer + * owns scrolling, so it can pin its own action bar below a scrolling body. + */ + readonly children: React.ReactNode + readonly width?: string +}) { + const panelRef = useRef(null) + // Drives the enter transition: the panel mounts translated off-screen, then + // flips to `entered` on the next frame so the browser has two distinct + // states to animate between. + const [entered, setEntered] = useState(false) + + useEffect(() => { + if (!open) { + setEntered(false) + return + } + const raf = requestAnimationFrame(() => setEntered(true)) + return () => cancelAnimationFrame(raf) + }, [open]) + + // Esc closes. Bound to the document so it works regardless of focus position. + useEffect(() => { + if (!open) return + function onKey(e: KeyboardEvent) { + if (e.key === 'Escape') { + e.stopPropagation() + onClose() + } + } + document.addEventListener('keydown', onKey) + return () => document.removeEventListener('keydown', onKey) + }, [open, onClose]) + + // Lock background scroll while open, so the page behind doesn't move when + // the cursor leaves the panel. + useEffect(() => { + if (!open) return + const prev = document.body.style.overflow + document.body.style.overflow = 'hidden' + return () => { document.body.style.overflow = prev } + }, [open]) + + // Move focus into the panel on open so keyboard and screen-reader users land + // inside the dialog rather than back at the top of the page. + useEffect(() => { + if (!open) return + const first = panelRef.current?.querySelector( + 'input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled])', + ) + first?.focus() + }, [open]) + + if (!open) return null + + return ( +
+ +
+ +
{children}
+ + + ) +} diff --git a/dashboard/src/pages/FlagEditor.tsx b/dashboard/src/components/FlagForm.tsx similarity index 54% rename from dashboard/src/pages/FlagEditor.tsx rename to dashboard/src/components/FlagForm.tsx index 8f24cbd..92f44b9 100644 --- a/dashboard/src/pages/FlagEditor.tsx +++ b/dashboard/src/components/FlagForm.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from 'react' -import { useNavigate, useParams, Link } from 'react-router-dom' -import { Archive, ArchiveRestore, ArrowLeft, Plus, Save, Trash2, X } from 'lucide-react' +import { Link } from 'react-router-dom' +import { Archive, ArchiveRestore, Plus, Save, Trash2, X } from 'lucide-react' import { api, scheduledApi, segmentsApi } from '../api' import type { Flag, FlagType, FlagValue, Prerequisite, ScheduledChange, Segment, TargetingRule, WeightedVariant } from '../types' -import RuleEditor from '../components/RuleEditor' +import RuleEditor from './RuleEditor' import { useEnvironment } from '../context/EnvironmentContext' const EMPTY_FLAG: Flag = { @@ -24,10 +24,10 @@ const EMPTY_FLAG: Flag = { function SectionCard({ title, children }: { title: string; children: React.ReactNode }) { return (
-
-

{title}

+
+

{title}

-
{children}
+
{children}
) } @@ -373,10 +373,27 @@ function TagsInput({ tags, onChange }: { tags: string[]; onChange: (tags: string ) } -export default function FlagEditor() { - const { key } = useParams<{ key?: string }>() - const isEdit = Boolean(key) - const navigate = useNavigate() +/** + * Create/edit form for a single flag, laid out as one scrolling column with a + * pinned action bar — sized for the slide-over panel rather than a full page. + * + * Fills its parent as a flex column, so it expects a `min-h-0` flex container. + */ +export default function FlagForm({ + flagKey, + onSaved, + onFlagChanged, + onCancel, +}: { + /** Key of the flag to edit; omit to create a new one. */ + readonly flagKey?: string + /** The save landed and the panel is done — not called for queued approvals. */ + readonly onSaved: (flag: Flag, mode: 'created' | 'updated') => void + /** A side-channel write (archive/unarchive) landed; the panel stays open. */ + readonly onFlagChanged?: (flag: Flag) => void + readonly onCancel: () => void +}) { + const isEdit = Boolean(flagKey) const { activeEnv } = useEnvironment() const [flag, setFlag] = useState(EMPTY_FLAG) @@ -393,37 +410,55 @@ export default function FlagEditor() { const [scheduleSaving, setScheduleSaving] = useState(false) const [archiving, setArchiving] = useState(false) + // The panel is reused across flags without unmounting, so every piece of + // per-flag state has to be reset when the target changes — otherwise the + // previous flag's values bleed into the next one. + useEffect(() => { + setFlag(EMPTY_FLAG) + setRolloutInput('') + setError(null) + setPendingApproval(false) + setScheduledChanges([]) + setScheduleAt('') + setLoading(Boolean(flagKey)) + }, [flagKey, activeEnv]) + useEffect(() => { if (!activeEnv) return segmentsApi.list(activeEnv.id).then(setSegments).catch(() => {/* non-fatal */}) // Candidates for the Prerequisites picker — exclude the flag being edited // itself, since a self-referencing prerequisite is a trivial cycle. api.listFlags(activeEnv.id) - .then(list => setOtherFlags(list.filter(f => f.key !== key))) + .then(list => setOtherFlags(list.filter(f => f.key !== flagKey))) .catch(() => {/* non-fatal */}) - }, [activeEnv, key]) + }, [activeEnv, flagKey]) useEffect(() => { - if (!key || !activeEnv) return - api.getFlag(activeEnv.id, key) + if (!flagKey || !activeEnv) return + let cancelled = false + api.getFlag(activeEnv.id, flagKey) .then(f => { + if (cancelled) return setFlag(f) setRolloutInput(f.rollout_percentage != null ? String(f.rollout_percentage) : '') }) - .catch(e => setError(e instanceof Error ? e.message : 'Failed to load flag')) - .finally(() => setLoading(false)) - scheduledApi.listForFlag(activeEnv.id, key) - .then(setScheduledChanges) + .catch(e => { + if (!cancelled) setError(e instanceof Error ? e.message : 'Failed to load flag') + }) + .finally(() => { if (!cancelled) setLoading(false) }) + scheduledApi.listForFlag(activeEnv.id, flagKey) + .then(cs => { if (!cancelled) setScheduledChanges(cs) }) .catch(() => {/* non-fatal */}) - }, [key, activeEnv]) + return () => { cancelled = true } + }, [flagKey, activeEnv]) async function handleSchedule(e: React.FormEvent) { e.preventDefault() - if (!activeEnv || !key) return + if (!activeEnv || !flagKey) return setScheduleSaving(true) try { const patch: Record = { is_enabled: scheduleAction === 'enable' } - const created = await scheduledApi.create(activeEnv.id, key, { + const created = await scheduledApi.create(activeEnv.id, flagKey, { scheduled_at: new Date(scheduleAt).toISOString(), patch, }) @@ -447,15 +482,19 @@ export default function FlagEditor() { } async function handleArchiveToggle() { - if (!activeEnv || !key) return + if (!activeEnv || !flagKey) return const archiving_ = !flag.archived_at - if (archiving_ && !confirm(`Archive "${key}"? It will be hidden from the flag list, but keeps evaluating exactly as before.`)) return + if (archiving_ && !confirm(`Archive "${flagKey}"? It will be hidden from the flag list, but keeps evaluating exactly as before.`)) return setArchiving(true) try { const updated = archiving_ - ? await api.archiveFlag(activeEnv.id, key) - : await api.unarchiveFlag(activeEnv.id, key) + ? await api.archiveFlag(activeEnv.id, flagKey) + : await api.unarchiveFlag(activeEnv.id, flagKey) setFlag(updated) + // Surface the archive/unarchive to the list behind the panel, which + // filters on exactly this field. The panel stays open — archiving is not + // "done editing". + onFlagChanged?.(updated) } catch (err) { alert(err instanceof Error ? err.message : String(err)) } finally { @@ -500,8 +539,8 @@ export default function FlagEditor() { } try { - if (isEdit && key) { - const result = await api.patchFlag(activeEnv.id, key, { + if (isEdit && flagKey) { + const result = await api.patchFlag(activeEnv.id, flagKey, { is_enabled: payload.is_enabled, rollout_percentage: payload.rollout_percentage, description: payload.description, @@ -516,15 +555,16 @@ export default function FlagEditor() { }) if (!result.applied) { // Environment requires approval — the patch was queued, not applied. - // Stay on the page rather than navigating away as if it took effect. + // Keep the panel open rather than closing it as if it took effect. setPendingApproval(true) setSaving(false) return } + onSaved(result.flag, 'updated') } else { - await api.createFlag(activeEnv.id, payload) + const created = await api.createFlag(activeEnv.id, payload) + onSaved(created, 'created') } - navigate('/flags') } catch (e) { setError(e instanceof Error ? e.message : 'Save failed') } finally { @@ -537,327 +577,306 @@ export default function FlagEditor() { if (loading) { return ( -
+
Loading…
) } return ( -
-
- - Back to flags - - {flag.archived_at && ( - - Archived - +
void handleSubmit(e)} className="flex min-h-0 flex-1 flex-col"> +
+ {error && ( +
+ {error} +
)} -
- {error && ( -
- {error} -
- )} - - {pendingApproval && ( -
- This environment requires approval — your change was queued instead of applied.{' '} - View change requests -
- )} + {pendingApproval && ( +
+ This environment requires approval — your change was queued instead of applied.{' '} + View change requests +
+ )} - void handleSubmit(e)} className="space-y-5"> -
- {/* Main column — evaluation behavior */} -
- -
-
- - - {isEdit && ( -

- Flag type cannot be changed after creation. -

- )} -
+ +
+
+ + setField('key', e.target.value)} + placeholder="e.g. dark_mode" + className={`${inputClass} font-mono`} + /> + {!isEdit && ( +

+ Immutable after creation. Use snake_case. +

+ )} +
- {isVariant && ( - <> - setField('default_value', v)} - /> - setField('disabled_value', v)} - /> - - )} -
-
- - -
-
-
-

Enabled

-

- {isVariant - ? 'When disabled, returns the disabled value.' - : <>When disabled, always evaluates to false.} -

-
- setField('is_enabled', !flag.is_enabled)} /> -
+
+ + setField('description', e.target.value || null)} + placeholder="What does this flag control?" + className={inputClass} + /> +
-
- -
- setRolloutInput(e.target.value)} - placeholder="100" - className="w-28 bg-white border border-gray-100 rounded-xl px-4 py-2.5 text-gray-900 text-sm focus:outline-none focus:ring-4 focus:ring-emerald-500/10 focus:border-emerald-500/30 transition-all shadow-premium" - /> - % -
-

- Leave empty for 100%. Users are bucketed deterministically by their key. -

-
-
-
+
+ + + {isEdit && ( +

+ Flag type cannot be changed after creation. +

+ )} +
{isVariant && ( - -

- Split traffic across multiple values by weight (e.g. 60/30/10). Applies to users who - are enabled, inside the rollout percentage above, and don't match a targeting rule. - Weights don't need to sum to 100 — only their proportions matter. Leave empty to - always return the default value above. -

- + setField('default_value', v)} + /> + setField('variants', variants)} + onChange={v => setField('disabled_value', v)} /> -
+ )} +
+
- -

- {isVariant - ? 'Users matching a rule return that rule\'s value (or the default value if no per-rule value is set), bypassing the rollout cap.' - : 'Users matching any rule always see the flag as enabled, bypassing the rollout cap.'} + +

+
+
+

Enabled

+

+ {isVariant + ? 'When disabled, returns the disabled value.' + : <>When disabled, always evaluates to false.} +

+
+ setField('is_enabled', !flag.is_enabled)} /> +
+ +
+ +
+ setRolloutInput(e.target.value)} + placeholder="100" + className="w-28 bg-white border border-gray-100 rounded-xl px-4 py-2.5 text-gray-900 text-sm focus:outline-none focus:ring-4 focus:ring-emerald-500/10 focus:border-emerald-500/30 transition-all shadow-premium" + /> + % +
+

+ Leave empty for 100%. Users are bucketed deterministically by their key.

- setField('rules', rules)} - flagType={flagType} - segments={segments} - /> - +
+
+ + {isVariant && ( + +

+ Split traffic across multiple values by weight (e.g. 60/30/10). Applies to users who + are enabled, inside the rollout percentage above, and don't match a targeting rule. + Weights don't need to sum to 100 — only their proportions matter. Leave empty to + always return the default value above. +

+ setField('variants', variants)} + /> +
+ )} - {/* Sidebar — identity, ownership, dependencies */} -
- -
-
- - setField('key', e.target.value)} - placeholder="e.g. dark_mode" - className={`${inputClass} font-mono`} - /> - {!isEdit && ( -

- Immutable after creation. Use snake_case. -

- )} + +

+ {isVariant + ? 'Users matching a rule return that rule\'s value (or the default value if no per-rule value is set), bypassing the rollout cap.' + : 'Users matching any rule always see the flag as enabled, bypassing the rollout cap.'} +

+ setField('rules', rules)} + flagType={flagType} + segments={segments} + /> +
+ + +
+ setField('tags', tags)} /> +
+ + setField('owner_email', e.target.value || null)} + placeholder="owner@example.com" + className={inputClass} + /> +

+ Who's responsible for this flag — useful when deciding what's safe to clean up. +

+
+
+
+ + +

+ Require other flags to be enabled (or resolve to a specific value) before this flag's + own rules and rollout are even considered. Checked first — if any prerequisite fails, + this flag evaluates as disabled. +

+ setField('prerequisites', prerequisites)} + /> +
+ + {isEdit && flagKey && ( + +
+ {scheduledChanges.filter(c => !c.executed_at).length > 0 && ( +
+

+ Pending +

+ {scheduledChanges + .filter(c => !c.executed_at) + .map(sc => ( +
+ + {sc.patch.is_enabled ? 'Enable' : 'Disable'} + {' at '} + {new Date(sc.scheduled_at).toLocaleString()} + + +
+ ))}
+ )} + {/* Scheduling posts on its own — nesting a inside the flag + form is invalid HTML, so this is a plain div and the button + calls the handler directly. */} +
- - setField('description', e.target.value || null)} - placeholder="What does this flag control?" - className={inputClass} - /> + +
-
- - - -
- setField('tags', tags)} />
- + setField('owner_email', e.target.value || null)} - placeholder="owner@example.com" - className={inputClass} + type="datetime-local" + value={scheduleAt} + onChange={e => setScheduleAt(e.target.value)} + className={inputClass + ' w-auto'} /> -

- Who's responsible for this flag — useful when deciding what's safe to clean up. -

+
-
- - -

- Require other flags to be enabled (or resolve to a specific value) before this flag's - own rules and rollout are even considered. Checked first — if any prerequisite fails, - this flag evaluates as disabled. -

- setField('prerequisites', prerequisites)} - /> -
- - {isEdit && key && ( - -
- {scheduledChanges.filter(c => !c.executed_at).length > 0 && ( -
-

- Pending -

- {scheduledChanges - .filter(c => !c.executed_at) - .map(sc => ( -
- - {sc.patch.is_enabled ? 'Enable' : 'Disable'} - {' at '} - {new Date(sc.scheduled_at).toLocaleString()} - - -
- ))} -
- )} - - void handleSchedule(e)} className="flex items-end gap-3 flex-wrap"> -
- - -
-
- - setScheduleAt(e.target.value)} - className={inputClass + ' w-auto'} - /> -
- - -
-
- )} -
-
+
+
+ )} +
-
+ {/* Pinned action bar — stays reachable however long the form scrolls. */} +
+ + + {isEdit && flagKey && ( - - Cancel - - {isEdit && key && ( - - )} -
- -
+ )} +
+ ) } diff --git a/dashboard/src/components/Sidebar.test.tsx b/dashboard/src/components/Sidebar.test.tsx index e3b512e..fc1bf92 100644 --- a/dashboard/src/components/Sidebar.test.tsx +++ b/dashboard/src/components/Sidebar.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react' -import { describe, it, expect, vi } from 'vitest' +import userEvent from '@testing-library/user-event' +import { describe, it, expect, vi, beforeEach } from 'vitest' import { MemoryRouter } from 'react-router-dom' import Sidebar from './Sidebar' @@ -17,39 +18,90 @@ vi.mock('../context/AuthContext', () => ({ useAuth: () => mockAuthContext, })) +const ENV = { id: 'env-1', name: 'Development', color: '#10b981' } +const PROJECT = { id: 'proj-1', name: 'Bookspine' } + vi.mock('../context/EnvironmentContext', () => ({ useEnvironment: () => ({ - environments: [], - activeEnv: null, + environments: [ENV], + activeEnv: ENV, setActiveEnv: vi.fn(), }), })) vi.mock('../context/ProjectContext', () => ({ useProject: () => ({ - projects: [], - activeProject: null, + projects: [PROJECT], + activeProject: PROJECT, setActiveProject: vi.fn(), loading: false, reload: vi.fn(), }), })) -describe('Sidebar Integration', () => { - it('renders navigation links and user profile', () => { - render( - - - - ) +function renderSidebar(initialEntry = '/') { + return render( + + + , + ) +} + +beforeEach(() => { + localStorage.clear() +}) + +describe('Sidebar', () => { + it('renders navigation and user profile', () => { + renderSidebar() - // Check main navigation features expect(screen.getByText('Checkgate')).toBeInTheDocument() expect(screen.getByText('Dashboard')).toBeInTheDocument() expect(screen.getByText('Feature Flags')).toBeInTheDocument() + expect(screen.getByText('testuser')).toBeInTheDocument() + }) + + it('labels each scope group with the context it applies to', () => { + renderSidebar() + + // The env group header names the active environment, so it's clear which + // switcher above changes what these pages show. + expect(screen.getByRole('button', { name: /environment.*development/i })).toBeInTheDocument() + expect(screen.getByRole('button', { name: /project.*bookspine/i })).toBeInTheDocument() + expect(screen.getByRole('button', { name: /workspace/i })).toBeInTheDocument() + }) + + it('starts with admin sections closed to keep the default list short', async () => { + const user = userEvent.setup() + renderSidebar() + + // Environment items are the daily drivers — visible up front. + expect(screen.getByText('Feature Flags')).toBeInTheDocument() + // Workspace admin items are tucked away until asked for. + expect(screen.queryByText('Users')).not.toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: /workspace/i })) + expect(screen.getByText('Users')).toBeInTheDocument() + }) - // Assert the mocked user is shown - expect(screen.getByText('testuser')).toBeInTheDocument() + it('keeps a section open when it holds the current page', () => { + // /users lives in the workspace group, which is closed by default — + // it must still render, or the active page would be invisible. + renderSidebar('/users') + + expect(screen.getByText('Users')).toBeInTheDocument() + }) + + it('persists an opened section across remounts', async () => { + const user = userEvent.setup() + const { unmount } = renderSidebar() + + await user.click(screen.getByRole('button', { name: /workspace/i })) + expect(screen.getByText('Users')).toBeInTheDocument() + unmount() + + renderSidebar() + expect(screen.getByText('Users')).toBeInTheDocument() }) }) diff --git a/dashboard/src/components/Sidebar.tsx b/dashboard/src/components/Sidebar.tsx index 5636377..be69138 100644 --- a/dashboard/src/components/Sidebar.tsx +++ b/dashboard/src/components/Sidebar.tsx @@ -1,4 +1,4 @@ -import { NavLink, useNavigate } from 'react-router-dom' +import { NavLink, useNavigate, useLocation } from 'react-router-dom' import { LayoutDashboard, ToggleLeft, @@ -20,6 +20,7 @@ import { FlaskConical, PanelLeftClose, PanelLeftOpen, + ChevronRight, } from 'lucide-react' import { useState, useRef, useEffect } from 'react' import { useAuth } from '../context/AuthContext' @@ -27,22 +28,67 @@ import { useEnvironment, type Environment } from '../context/EnvironmentContext' import { useProject } from '../context/ProjectContext' import type { Project } from '../types' -const NAV_ALL = [ - { to: '/', icon: LayoutDashboard, label: 'Dashboard', end: true, adminOnly: false }, - { to: '/flags', icon: ToggleLeft, label: 'Feature Flags', end: false, adminOnly: false }, - { to: '/segments', icon: Tags, label: 'Segments', end: false, adminOnly: false }, - { to: '/change-requests', icon: GitPullRequest, label: 'Change Requests', end: false, adminOnly: false }, - { to: '/schedule', icon: CalendarClock, label: 'Scheduled', end: false, adminOnly: false }, - { to: '/impressions', icon: Activity, label: 'Impressions', end: false, adminOnly: false }, - { to: '/exposure', icon: PieChart, label: 'Exposure', end: false, adminOnly: false }, - { to: '/experiments', icon: FlaskConical, label: 'Experiments', end: false, adminOnly: false }, - { to: '/audit', icon: History, label: 'Audit Log', end: false, adminOnly: false }, - { to: '/sdk-health', icon: Wifi, label: 'SDK Health', end: false, adminOnly: false }, - { to: '/webhooks', icon: Webhook, label: 'Webhooks', end: false, adminOnly: true }, - { to: '/environments', icon: Globe, label: 'Environments', end: false, adminOnly: true }, - { to: '/projects', icon: FolderKanban, label: 'Projects', end: false, adminOnly: true }, - { to: '/users', icon: Users, label: 'Users', end: false, adminOnly: true }, - { to: '/settings', icon: Settings, label: 'Settings', end: false, adminOnly: true }, +interface NavItem { + to: string + icon: typeof LayoutDashboard + label: string + end?: boolean + adminOnly?: boolean +} + +interface NavGroup { + id: string + /** Scope label shown in the section header. */ + label: string + items: NavItem[] +} + +/** Always visible, above the grouped sections — the one page with no scope. */ +const HOME: NavItem = { to: '/', icon: LayoutDashboard, label: 'Dashboard', end: true } + +/** + * Nav grouped by the scope its data actually belongs to, so it's obvious which + * switcher above changes what you're looking at. Membership follows the API + * each page calls, not intuition: + * + * environment — passes `activeEnv.id` (flags, segments, change requests, + * scheduled, experiments, exposure, impressions, webhooks, audit) + * project — passes `activeProject.id` (environments) + * workspace — unscoped endpoints (projects, users, sdk health, settings) + */ +const NAV_GROUPS: NavGroup[] = [ + { + id: 'environment', + label: 'Environment', + items: [ + { to: '/flags', icon: ToggleLeft, label: 'Feature Flags' }, + { to: '/segments', icon: Tags, label: 'Segments' }, + { to: '/change-requests', icon: GitPullRequest, label: 'Change Requests' }, + { to: '/schedule', icon: CalendarClock, label: 'Scheduled' }, + { to: '/experiments', icon: FlaskConical, label: 'Experiments' }, + { to: '/exposure', icon: PieChart, label: 'Exposure' }, + { to: '/impressions', icon: Activity, label: 'Impressions' }, + { to: '/audit', icon: History, label: 'Audit Log' }, + { to: '/webhooks', icon: Webhook, label: 'Webhooks', adminOnly: true }, + ], + }, + { + id: 'project', + label: 'Project', + items: [ + { to: '/environments', icon: Globe, label: 'Environments', adminOnly: true }, + ], + }, + { + id: 'workspace', + label: 'Workspace', + items: [ + { to: '/projects', icon: FolderKanban, label: 'Projects', adminOnly: true }, + { to: '/users', icon: Users, label: 'Users', adminOnly: true }, + { to: '/sdk-health', icon: Wifi, label: 'SDK Health' }, + { to: '/settings', icon: Settings, label: 'Settings', adminOnly: true }, + ], + }, ] // --------------------------------------------------------------------------- @@ -175,13 +221,74 @@ function EnvSwitcher() { // --------------------------------------------------------------------------- const KEY_SIDEBAR_COLLAPSED = 'lg_sidebar_collapsed' +const KEY_CLOSED_GROUPS = 'lg_sidebar_closed_groups' + +function NavItemLink({ item, collapsed }: { item: NavItem; collapsed: boolean }) { + const { to, icon: Icon, label, end } = item + return ( + + `group flex items-center rounded-xl text-sm font-medium transition-all duration-200 ${ + collapsed ? 'justify-center px-0 py-2.5' : 'gap-3 px-3 py-2.5' + } ${ + isActive + ? 'bg-emerald-50 text-emerald-700 shadow-sm shadow-emerald-100/50' + : 'text-gray-500 hover:text-gray-900 hover:bg-gray-50' + }` + } + > + {({ isActive }) => ( + <> +
+ +
+ {!collapsed && {label}} + + )} +
+ ) +} export default function Sidebar() { const { session, logout } = useAuth() + const { activeProject } = useProject() + const { activeEnv } = useEnvironment() const navigate = useNavigate() + const { pathname } = useLocation() const isAdmin = session?.user.role === 'admin' - const NAV = NAV_ALL.filter(item => !item.adminOnly || isAdmin) const [collapsed, setCollapsed] = useState(() => localStorage.getItem(KEY_SIDEBAR_COLLAPSED) === 'true') + // Admin/config sections start closed so the first impression is the handful + // of pages people use daily, not all fifteen at once. Opening one sticks. + const [closedGroups, setClosedGroups] = useState(() => { + try { + const raw = localStorage.getItem(KEY_CLOSED_GROUPS) + return raw ? JSON.parse(raw) as string[] : ['project', 'workspace'] + } catch { + return ['project', 'workspace'] + } + }) + + const groups = NAV_GROUPS + .map(g => ({ ...g, items: g.items.filter(i => !i.adminOnly || isAdmin) })) + .filter(g => g.items.length > 0) + + /** Context name shown beside the scope label, so the header names what it applies to. */ + function groupContext(id: string): string | undefined { + if (id === 'environment') return activeEnv?.name + if (id === 'project') return activeProject?.name + return undefined + } + + function toggleGroup(id: string) { + setClosedGroups(prev => { + const next = prev.includes(id) ? prev.filter(g => g !== id) : [...prev, id] + localStorage.setItem(KEY_CLOSED_GROUPS, JSON.stringify(next)) + return next + }) + } function toggleCollapsed() { setCollapsed(c => { @@ -223,33 +330,47 @@ export default function Sidebar() { )} {/* Nav */} -
+ +
+ + setUrl(e.target.value)} + placeholder="https://hooks.slack.com/services/…" + className={`${inputClass} font-mono text-xs`} + /> +

{KIND_HINT[kind]}

+

+ Stored write-only — anyone with this URL can post to the channel, so it is never shown again. +

+
+ +
+ +
+ {INTEGRATION_EVENTS.map(event => { + const on = events.includes(event) + return ( + + ) + })} +
+

+ {events.length === 0 + ? 'Nothing selected — every event will be sent.' + : `${events.length} selected.`} +

+
+ + + + ) +} + +function IntegrationRow({ + integration, + envId, + onChanged, + onDeleted, +}: { + integration: Integration + envId: string + onChanged: (i: Integration) => void + onDeleted: (id: string) => void +}) { + const [busy, setBusy] = useState(false) + const [tested, setTested] = useState(false) + + async function toggleEnabled() { + setBusy(true) + try { + onChanged(await integrationsApi.patch(envId, integration.id, { enabled: !integration.enabled })) + } catch (e) { + alert(e instanceof Error ? e.message : 'Update failed') + } finally { + setBusy(false) + } + } + + async function sendTest() { + setBusy(true) + try { + await integrationsApi.test(envId, integration.id) + setTested(true) + setTimeout(() => setTested(false), 2500) + } catch (e) { + alert(e instanceof Error ? e.message : 'Test failed') + } finally { + setBusy(false) + } + } + + async function remove() { + if (!confirm(`Disconnect "${integration.name}"? Events will stop being sent to this channel.`)) return + setBusy(true) + try { + await integrationsApi.delete(envId, integration.id) + onDeleted(integration.id) + } catch (e) { + alert(e instanceof Error ? e.message : 'Delete failed') + setBusy(false) + } + } + + return ( + + + {integration.name} +
+ {KIND_LABEL[integration.kind]} · {integration.webhook_url_preview} +
+ + + {integration.events.length === 0 ? ( + All events + ) : ( +
+ {integration.events.map(e => ( + + {eventLabel(e)} + + ))} +
+ )} + + + + + +
+ + +
+ + + ) +} + +export default function Integrations() { + const { activeEnv } = useEnvironment() + const [integrations, setIntegrations] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const load = useCallback(async () => { + if (!activeEnv) return + try { + setError(null) + setIntegrations(await integrationsApi.list(activeEnv.id)) + } catch (e) { + setError(e instanceof Error ? e.message : 'Failed to load integrations') + } finally { + setLoading(false) + } + }, [activeEnv]) + + useEffect(() => { + setLoading(true) + void load() + }, [load]) + + if (!activeEnv) { + return
Select an environment first.
+ } + + return ( +
+

+ Send flag and change-request activity to Slack or Microsoft Teams. Configured per environment — + these apply to {activeEnv.name}. +

+ + setIntegrations(prev => [...prev, i])} /> + +
+ {loading ? ( +
Loading…
+ ) : error ? ( +
+

{error}

+ +
+ ) : integrations.length === 0 ? ( +
+ +

No channels connected yet.

+
+ ) : ( +
+ + + + + + + + + + {integrations.map(i => ( + + setIntegrations(prev => prev.map(x => (x.id === updated.id ? updated : x))) + } + onDeleted={id => setIntegrations(prev => prev.filter(x => x.id !== id))} + /> + ))} + +
ChannelEventsEnabled +
+
+ )} +
+
+ ) +} + +export { eventLabel } diff --git a/dashboard/src/types.ts b/dashboard/src/types.ts index 572cee7..1fbee52 100644 --- a/dashboard/src/types.ts +++ b/dashboard/src/types.ts @@ -253,3 +253,43 @@ export interface SdkKeyInfo { environment_name: string created_at: string } + +// --------------------------------------------------------------------------- +// Chat integrations (Slack / Microsoft Teams) +// --------------------------------------------------------------------------- + +export type IntegrationKind = 'slack' | 'teams' + +/** Event names the server can notify on. Mirrors `SUPPORTED_EVENTS` in Rust. */ +export const INTEGRATION_EVENTS = [ + 'flag.created', + 'flag.updated', + 'flag.deleted', + 'flag.promoted', + 'change_request.opened', + 'change_request.approved', + 'change_request.rejected', +] as const + +export interface Integration { + id: string + environment_id: string + kind: IntegrationKind + name: string + /** Elided tail of the webhook URL — the full URL is never sent back. */ + webhook_url_preview: string + /** Empty means "every event". */ + events: string[] + enabled: boolean + created_at: string +} + +export interface IntegrationDelivery { + id: number + integration_id: string + event: string + status_code: number | null + response_body: string | null + error: string | null + delivered_at: string +} diff --git a/dashboard/tsconfig.app.tsbuildinfo b/dashboard/tsconfig.app.tsbuildinfo index 0ef87ac..163bb98 100644 --- a/dashboard/tsconfig.app.tsbuildinfo +++ b/dashboard/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.test.tsx","./src/app.tsx","./src/api.test.ts","./src/api.ts","./src/main.tsx","./src/setuptests.ts","./src/types.ts","./src/components/drawer.tsx","./src/components/flagform.tsx","./src/components/layout.tsx","./src/components/ruleeditor.tsx","./src/components/sidebar.test.tsx","./src/components/sidebar.tsx","./src/components/topbar.tsx","./src/context/authcontext.tsx","./src/context/environmentcontext.tsx","./src/context/projectcontext.tsx","./src/pages/auditlog.tsx","./src/pages/changerequests.tsx","./src/pages/dashboard.tsx","./src/pages/environmentdiff.tsx","./src/pages/environments.tsx","./src/pages/experiments.tsx","./src/pages/exposure.tsx","./src/pages/flaglist.test.tsx","./src/pages/flaglist.tsx","./src/pages/impressions.tsx","./src/pages/login.tsx","./src/pages/projectsettings.tsx","./src/pages/projects.tsx","./src/pages/scheduled.tsx","./src/pages/sdkhealth.tsx","./src/pages/segments.tsx","./src/pages/settings.tsx","./src/pages/setup.tsx","./src/pages/users.tsx","./src/pages/webhooks.tsx"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/app.test.tsx","./src/app.tsx","./src/api.test.ts","./src/api.ts","./src/main.tsx","./src/setuptests.ts","./src/types.ts","./src/components/drawer.tsx","./src/components/flagform.tsx","./src/components/layout.tsx","./src/components/ruleeditor.tsx","./src/components/sidebar.test.tsx","./src/components/sidebar.tsx","./src/components/topbar.tsx","./src/context/authcontext.tsx","./src/context/environmentcontext.tsx","./src/context/projectcontext.tsx","./src/pages/auditlog.tsx","./src/pages/changerequests.tsx","./src/pages/dashboard.tsx","./src/pages/environmentdiff.tsx","./src/pages/environments.tsx","./src/pages/experiments.tsx","./src/pages/exposure.tsx","./src/pages/flaglist.test.tsx","./src/pages/flaglist.tsx","./src/pages/impressions.tsx","./src/pages/integrations.test.tsx","./src/pages/integrations.tsx","./src/pages/login.tsx","./src/pages/projectsettings.tsx","./src/pages/projects.tsx","./src/pages/scheduled.tsx","./src/pages/sdkhealth.tsx","./src/pages/segments.tsx","./src/pages/settings.tsx","./src/pages/setup.tsx","./src/pages/users.tsx","./src/pages/webhooks.tsx"],"version":"5.9.3"} \ No newline at end of file diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index cc6af61..1cea3da 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -82,6 +82,7 @@ export default defineConfig({ { text: "Ecosystem & Integrations", items: [ + { text: "Slack & Teams Alerts", link: "/ecosystem/chat-alerts" }, { text: "Type-Safe Schema CLI", link: "/ecosystem/cli" }, { text: "Edge Evaluation", link: "/ecosystem/edge" }, { text: "SSR / Bootstrap", link: "/ecosystem/ssr" }, diff --git a/docs/.vitepress/theme/FlowDiagram.vue b/docs/.vitepress/theme/FlowDiagram.vue index 80cf8ef..d5dd3ed 100644 --- a/docs/.vitepress/theme/FlowDiagram.vue +++ b/docs/.vitepress/theme/FlowDiagram.vue @@ -49,7 +49,9 @@ const pushPath = (cy) => `M500,190 Q590,${cy} 654,${cy}` {{ c.sub }} · local eval - isEnabled() · ~100 ns, in-process + + isEnabled() · ~100 ns, in-process @@ -135,6 +137,9 @@ const pushPath = (cy) => `M500,190 Q590,${cy} 654,${cy}` text-anchor: middle; font-family: var(--vp-font-family-mono, monospace); } +.cg-edge-end { + text-anchor: end; +} .cg-packet { fill: var(--vp-c-brand-1); } diff --git a/docs/ecosystem/chat-alerts.md b/docs/ecosystem/chat-alerts.md new file mode 100644 index 0000000..6a0fada --- /dev/null +++ b/docs/ecosystem/chat-alerts.md @@ -0,0 +1,149 @@ +--- +title: "Slack & Microsoft Teams Alerts" +description: "Deliver flag changes and change-request activity into Slack or Microsoft Teams. Per-environment configuration, per-event filtering, native Block Kit and MessageCard formatting, and a bounded delivery log." +--- + +# Slack & Teams Alerts + +Flag changes matter to people who never open the dashboard. Chat alerts push flag and +change-request activity into the channel a team already watches — a native **Slack Block Kit** +message or a **Teams MessageCard**, not a raw JSON blob. + +Alerts are configured **per environment**, so Production can be noisy in `#incidents` while +Development stays quiet. + +::: tip Alerts, not approvals +This is one-way: Checkgate posts to your channel. Approving a change request from the message +itself is [on the roadmap](/roadmap) — it needs a publicly reachable signed callback endpoint and a +Slack↔Checkgate identity mapping, so that the "you can't approve your own request" rule stays +enforceable. For now, review happens in the dashboard; the alert tells you it's waiting. +::: + +## Setup + +### 1. Create an incoming webhook + +::: code-group + +```text [Slack] +Slack → Apps → search "Incoming Webhooks" → Add to Workspace +→ pick a channel → copy the Webhook URL + +https://hooks.slack.com/services/// +``` + +```text [Microsoft Teams] +Teams → the channel → ⋯ → Connectors → Incoming Webhook → Configure +→ name it, optionally upload an icon → Create → copy the URL + +https://outlook.office.com/webhook/... +``` + +::: + +### 2. Connect it in Checkgate + +In the dashboard, open **Chat Alerts** (under the Environment section of the sidebar), pick the +provider, paste the URL, and choose which events to receive. Admin role required. + +Then hit **Send test message** on the row — it fires a sample event through the *real* delivery +path, so a message landing in the channel confirms the whole chain, not just that the URL parses. + +## Events + +| Event | Fires when | +| --- | --- | +| `flag.created` | A flag is created or replaced | +| `flag.updated` | A flag is patched (directly, on schedule, or via an approved change request) | +| `flag.deleted` | A flag is deleted | +| `flag.promoted` | A flag's config is copied into another environment | +| `change_request.opened` | A patch is queued for approval and is waiting on a reviewer | +| `change_request.approved` | A reviewer approved a queued patch — records **who** reviewed it | +| `change_request.rejected` | A reviewer rejected it, with the stated reason | + +**Leaving the event selection empty subscribes to everything.** That's the useful default; narrow it +when a channel only cares about one slice — say `change_request.opened` in `#leads`, so reviewers get +pinged without also seeing every Development toggle. + +Messages carry the environment name, flag key, enabled/disabled state, rollout percentage, the acting +user, and — for rejections — the reason. Change-request events are colour-coded so a queued request +(amber) is visually distinct from an approval (green) or a rejection/deletion (red). + +## Security + +The incoming-webhook URL is a **bearer credential** — anyone holding it can post into your channel. +Checkgate treats it that way: + +- **It is never returned by the API after creation.** Listing integrations returns only an elided + preview (`…XXXXXX`), enough to tell two rows apart and useless to an attacker. To change it, write + a new URL; you cannot read the old one back. +- **Plaintext `http://` is rejected** outside loopback, so the credential is never sent over an + unencrypted connection. `http://127.0.0.1`, `http://localhost`, and `http://[::1]` are permitted + for local relays; lookalike hosts such as `http://127.0.0.1.evil.com` are not. +- **Managing integrations is admin-only**, the same tier as webhooks. + +If a URL leaks, revoke it at the provider (Slack: delete the webhook in the app config; Teams: remove +the connector) — that invalidates it immediately, whether or not the Checkgate row is deleted. + +## Delivery & troubleshooting + +Delivery is **fire-and-forget** — it never blocks the API response that triggered it. A failed POST +is retried with 1s / 5s / 15s backoff, and every attempt is recorded: + +```bash +curl -H "Authorization: Bearer $CHECKGATE_TOKEN" \ + "$CHECKGATE_URL/api/environments/$ENV_ID/integrations/$ID/deliveries" +``` + +Each row carries the event, HTTP status, response body, and any transport error. The log retains the +most recent 200 deliveries per integration. + +**Nothing arriving?** Work down this list: + +1. **Is the integration enabled?** The toggle on the row gates delivery entirely. +2. **Does the event filter include what you expect?** A filter set to `flag.deleted` stays silent for + every other event. Empty means all. +3. **Are you watching the right environment?** Integrations are per environment — a flag change in + Development will not notify a channel wired to Production. +4. **Check the delivery log.** A `4xx` means the provider rejected the message (usually a revoked or + mistyped URL); a transport error means we could not reach the host at all. + +## API + +Full reference in the [REST API docs](/api-reference). All routes are environment-scoped: + +| Method | Path | Purpose | +| --- | --- | --- | +| `GET` | `/api/environments/{env}/integrations` | List (URLs elided) | +| `POST` | `/api/environments/{env}/integrations` | Create | +| `PATCH` | `/api/environments/{env}/integrations/{id}` | Update name, URL, events, enabled | +| `DELETE` | `/api/environments/{env}/integrations/{id}` | Disconnect | +| `POST` | `/api/environments/{env}/integrations/{id}/test` | Send a sample message | +| `GET` | `/api/environments/{env}/integrations/{id}/deliveries` | Recent delivery attempts | + +```bash +curl -X POST "$CHECKGATE_URL/api/environments/$ENV_ID/integrations" \ + -H "Authorization: Bearer $CHECKGATE_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "kind": "slack", + "name": "#engineering", + "webhook_url": "https://hooks.slack.com/services///", + "events": ["change_request.opened", "flag.deleted"] + }' +``` + +`kind` is `slack` or `teams`. Unknown providers, non-loopback plaintext URLs, and unrecognised event +names are all rejected with `422`. + +## Raw webhooks vs chat alerts + +Both live under the Environment section, and they are not the same tool: + +- **[Webhooks](/api-reference#webhooks)** post Checkgate's own JSON envelope to any endpoint, signed + with HMAC-SHA256 so the receiver can verify it. Use them to *drive automation* — trigger a + pipeline, sync a system, feed an incident tool. +- **Chat alerts** post a provider-shaped message to Slack or Teams, with no signature, because the + URL itself is the credential. Use them to *inform humans*. + +Both receive the same events, so a channel and a pipeline can react to the same change. diff --git a/docs/guide/governance.md b/docs/guide/governance.md index 3c4bf15..52c3eb6 100644 --- a/docs/guide/governance.md +++ b/docs/guide/governance.md @@ -39,6 +39,10 @@ admin must review (self-approval is blocked). This gives you a pull-request-style gate on your highest-risk changes. See the [Change Requests API](/api-reference#change-requests). +A queued request only helps if someone knows it's waiting — wire the environment up to +[Slack or Teams alerts](/ecosystem/chat-alerts) so `change_request.opened` lands in the channel +your reviewers already watch. + ::: warning Automation & approvals If an environment requires approval, programmatic updates (Terraform, the operator, scripts) can't apply synchronously — the change is queued for review. Point infrastructure-as-code at a diff --git a/docs/roadmap.md b/docs/roadmap.md index ab57487..28d4ffb 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -42,6 +42,8 @@ Every new SDK wraps the **same shared Rust evaluation core** in [`core/`](https: - [ ] **OpenTelemetry export**: Emit evaluation, impression, and experiment metrics/traces to any OTel backend (Grafana, Datadog, Honeycomb). - [ ] **Warehouse sync**: Stream impressions, goal events, and experiment results to BigQuery / Snowflake / Postgres for BI and data-science workflows. - [ ] **Slack / Teams approvals & alerts**: Review and approve change requests, and receive flag-change notifications, where teams already work. + - [x] **Alerts** — flag and change-request activity delivered to Slack (Block Kit) and Teams (MessageCard) via incoming webhooks, configured per environment with per-event filtering. + - [ ] **Approvals** — approve/reject a change request from the message itself. Needs a signed public interactions endpoint and a Slack↔Checkgate identity mapping to keep the "no self-approval" rule enforceable. --- diff --git a/server/migrations/20260718000021_integrations.sql b/server/migrations/20260718000021_integrations.sql new file mode 100644 index 0000000..fdfad65 --- /dev/null +++ b/server/migrations/20260718000021_integrations.sql @@ -0,0 +1,42 @@ +-- Chat integrations: deliver flag and change-request activity to Slack or +-- Microsoft Teams via an incoming-webhook URL. +-- +-- Kept separate from the generic `webhooks` table on purpose. A raw webhook +-- posts our own JSON envelope to an arbitrary endpoint; an integration posts a +-- *provider-shaped* message (Slack Block Kit / Teams Adaptive Card) to that +-- provider's incoming-webhook URL. Different payload contract, different +-- per-provider constraints, and integrations additionally filter on event type. + +CREATE TABLE IF NOT EXISTS integrations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + environment_id UUID NOT NULL REFERENCES environments(id) ON DELETE CASCADE, + -- 'slack' | 'teams'. Constrained here so an unknown provider can never be + -- stored; the formatter matches exhaustively on it. + kind TEXT NOT NULL CHECK (kind IN ('slack', 'teams')), + name TEXT NOT NULL, + webhook_url TEXT NOT NULL, + -- Event types this integration wants. Empty array means "every event", + -- which keeps the common case free of setup. + events TEXT[] NOT NULL DEFAULT '{}', + enabled BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS integrations_env + ON integrations (environment_id); + +-- Rolling delivery log, mirroring webhook_deliveries — retain the last 200 per +-- integration. Older rows are pruned by the delivery writer. +CREATE TABLE IF NOT EXISTS integration_deliveries ( + id BIGSERIAL PRIMARY KEY, + integration_id UUID NOT NULL REFERENCES integrations(id) ON DELETE CASCADE, + event TEXT NOT NULL, + payload JSONB NOT NULL, + status_code INT, + response_body TEXT, + error TEXT, + delivered_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS integration_deliveries_integration_time + ON integration_deliveries (integration_id, delivered_at DESC); diff --git a/server/src/api.rs b/server/src/api.rs index 3ffdbed..8618c6f 100644 --- a/server/src/api.rs +++ b/server/src/api.rs @@ -6,6 +6,7 @@ pub mod experiments; pub mod flags; pub mod health; pub mod impressions; +pub mod integrations; pub mod keys; pub mod projects; pub mod scheduled; @@ -117,6 +118,7 @@ pub fn read_router() -> Router { .merge(audit::read_router()) .merge(segments::read_router()) .merge(webhooks::read_router()) + .merge(integrations::read_router()) .merge(scheduled::read_router()) .merge(health::read_router()) .merge(tokens::read_router()) @@ -159,9 +161,10 @@ pub fn segment_write_router() -> Router { segments::write_router() } -/// Webhook write routes — admin only. +/// Webhook write routes — admin only. Chat integrations ride along: they carry +/// a channel-posting credential, so they belong at the same tier. pub fn webhook_write_router() -> Router { - webhooks::write_router() + webhooks::write_router().merge(integrations::write_router()) } /// Scheduled change write routes — require editor or admin role. diff --git a/server/src/api/change_requests.rs b/server/src/api/change_requests.rs index bfa72b9..d53199a 100644 --- a/server/src/api/change_requests.rs +++ b/server/src/api/change_requests.rs @@ -212,6 +212,21 @@ pub async fn approve_change_request( let applied = super::flags::apply_patch(&state, &env_id, &flag_key, patch, Some(&reviewer)).await?; + // Note `apply_patch` above already emitted `flag.updated`; this second + // event records *who reviewed it*, which the flag event doesn't carry. + crate::notify::notify( + state.clone(), + env_id.clone(), + crate::integrations::change_request_payload( + "change_request.approved", + &env_id, + &flag_key, + id, + Some(&reviewer), + None, + ), + ); + info!(change_request_id = id, env_id = %env_id, flag_key = %flag_key, "Change request approved and applied"); Ok(Json(applied)) } @@ -228,24 +243,41 @@ pub async fn reject_change_request( .map(|c| c.email) .ok_or(StatusCode::UNAUTHORIZED)?; - let result = sqlx::query( + // RETURNING the flag key so the rejection notification can name the flag — + // "change request rejected" alone isn't actionable in a chat channel. + let row = sqlx::query( "UPDATE change_requests SET status = 'rejected', reviewed_by = $1, reviewed_at = NOW(), reason = $2 \ - WHERE id = $3 AND environment_id = $4::uuid AND status = 'pending'", + WHERE id = $3 AND environment_id = $4::uuid AND status = 'pending' \ + RETURNING flag_key", ) .bind(&reviewer) .bind(&req.reason) .bind(id) .bind(&env_id) - .execute(&state.db) + .fetch_optional(&state.db) .await .map_err(|e| { error!(error = %e, "Failed to reject change request"); StatusCode::INTERNAL_SERVER_ERROR })?; - if result.rows_affected() == 0 { + let Some(row) = row else { return Err(StatusCode::NOT_FOUND); - } + }; + let flag_key: String = row.get("flag_key"); + + crate::notify::notify( + state.clone(), + env_id.clone(), + crate::integrations::change_request_payload( + "change_request.rejected", + &env_id, + &flag_key, + id, + Some(&reviewer), + req.reason.as_deref(), + ), + ); info!(change_request_id = id, env_id = %env_id, "Change request rejected"); Ok(StatusCode::NO_CONTENT) diff --git a/server/src/api/flags.rs b/server/src/api/flags.rs index 80904aa..0aaa90b 100644 --- a/server/src/api/flags.rs +++ b/server/src/api/flags.rs @@ -320,7 +320,7 @@ async fn create_flag( actor_email.as_deref(), None, ); - crate::webhook_fire::fire_webhooks(state.clone(), env_id.clone(), wh_payload); + crate::notify::notify(state.clone(), env_id.clone(), wh_payload); info!(env_id = %env_id, "Flag created/replaced"); Ok(Json(FlagWithMetadata { @@ -405,7 +405,7 @@ async fn delete_flag( actor_email.as_deref(), None, ); - crate::webhook_fire::fire_webhooks(state.clone(), path.env_id.clone(), wh_payload); + crate::notify::notify(state.clone(), path.env_id.clone(), wh_payload); info!(env_id = %path.env_id, "Flag deleted"); Ok(StatusCode::NO_CONTENT) @@ -577,7 +577,7 @@ pub(super) async fn apply_patch( actor_email, None, ); - crate::webhook_fire::fire_webhooks(state.clone(), env_id.to_string(), wh_payload); + crate::notify::notify(state.clone(), env_id.to_string(), wh_payload); info!(env_id = %env_id, "Flag patched"); Ok(FlagWithMetadata { @@ -648,6 +648,21 @@ async fn patch_flag( ) .await?; + // A queued request is waiting on a human, so it's the event most worth + // surfacing in chat — nothing applies until someone reviews it. + crate::notify::notify( + state.clone(), + path.env_id.clone(), + crate::integrations::change_request_payload( + "change_request.opened", + &path.env_id, + &path.key, + cr.id, + Some(&requested_by), + None, + ), + ); + info!(env_id = %path.env_id, flag_key = %path.key, "Flag patch queued for approval"); Ok((StatusCode::ACCEPTED, Json(cr)).into_response()) } @@ -745,7 +760,7 @@ async fn promote_flag( actor_email.as_deref(), Some(&promote_meta), ); - crate::webhook_fire::fire_webhooks(state.clone(), target_env_id.clone(), wh_payload); + crate::notify::notify(state.clone(), target_env_id.clone(), wh_payload); info!( from_env = %path.env_id, diff --git a/server/src/api/integrations.rs b/server/src/api/integrations.rs new file mode 100644 index 0000000..cdcdf77 --- /dev/null +++ b/server/src/api/integrations.rs @@ -0,0 +1,490 @@ +//! CRUD for chat integrations (Slack / Microsoft Teams). +//! +//! Mirrors [`super::webhooks`] — same env-scoped access check and shape — but +//! the stored row targets a provider incoming-webhook URL and carries an event +//! subscription filter. Rendering and delivery live in +//! [`crate::integrations`]. + +use crate::auth::AuthContext; +use crate::state::AppState; +use axum::{ + Json, Router, + extract::{Path, State}, + http::StatusCode, + routing::{get, post}, +}; +use serde::{Deserialize, Serialize}; +use sqlx::Row; +use tracing::{error, info}; + +use super::flags::check_env_access; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +#[derive(Debug, Serialize)] +pub struct Integration { + pub id: String, + pub environment_id: String, + pub kind: String, + pub name: String, + /// The incoming-webhook URL is a bearer secret — anyone holding it can post + /// to the channel — so it is never returned after creation. + pub webhook_url_preview: String, + pub events: Vec, + pub enabled: bool, + pub created_at: String, +} + +#[derive(Debug, Serialize)] +pub struct IntegrationDelivery { + pub id: i64, + pub integration_id: String, + pub event: String, + pub status_code: Option, + pub response_body: Option, + pub error: Option, + pub delivered_at: String, +} + +#[derive(Deserialize)] +struct CreateBody { + kind: String, + name: String, + webhook_url: String, + #[serde(default)] + events: Vec, + #[serde(default = "default_true")] + enabled: bool, +} + +#[derive(Deserialize)] +struct PatchBody { + name: Option, + webhook_url: Option, + events: Option>, + enabled: Option, +} + +fn default_true() -> bool { + true +} + +/// Show enough of the URL to tell two integrations apart, without handing back +/// a credential that would let the holder post into the channel. +fn preview_url(url: &str) -> String { + // Provider URLs end in a high-entropy path segment; the tail is the part a + // human recognises, and 6 chars is far too few to replay. + let tail: String = url + .chars() + .rev() + .take(6) + .collect::>() + .into_iter() + .rev() + .collect(); + format!("…{tail}") +} + +fn row_to_integration(r: &sqlx::postgres::PgRow) -> Integration { + Integration { + id: r.get("id"), + environment_id: r.get("environment_id"), + kind: r.get("kind"), + name: r.get("name"), + webhook_url_preview: preview_url(r.get::("webhook_url").as_str()), + events: r.get("events"), + enabled: r.get("enabled"), + created_at: r.get("created_at"), + } +} + +// --------------------------------------------------------------------------- +// Validation +// --------------------------------------------------------------------------- + +const VALID_KINDS: &[&str] = &["slack", "teams"]; + +/// Webhook URLs are bearer credentials, so plaintext is refused — except on +/// loopback, which never leaves the machine and is what self-hosted operators +/// point at a local relay (and what the test suite drives). +/// +/// The host is compared exactly against the loopback set after stripping any +/// port, so lookalikes like `http://127.0.0.1.evil.com/` are rejected: their +/// host is `127.0.0.1.evil.com`, which matches nothing here. +fn is_allowed_url(url: &str) -> bool { + if url.starts_with("https://") { + return true; + } + let Some(rest) = url.strip_prefix("http://") else { + return false; + }; + // Authority is everything before the first '/', '?' or '#'. + let authority = rest.split(['/', '?', '#']).next().unwrap_or_default(); + // Credentials in the authority (`user@host`) would move the real host after + // the '@' — take that side. + let host_port = authority.rsplit('@').next().unwrap_or_default(); + let host = match host_port.strip_prefix('[') { + // IPv6 literal: `[::1]:8080` + Some(after) => after.split(']').next().unwrap_or_default(), + None => host_port.split(':').next().unwrap_or_default(), + }; + matches!(host, "localhost" | "127.0.0.1" | "::1") +} + +/// Rejects unknown providers and unknown event names before they reach the DB. +/// The `kind` CHECK constraint would catch the former as a 500; this turns it +/// into an actionable 422, and catches typo'd event names the DB can't see. +fn validate(kind: &str, url: &str, events: &[String]) -> Result<(), StatusCode> { + if !VALID_KINDS.contains(&kind) { + return Err(StatusCode::UNPROCESSABLE_ENTITY); + } + if url.is_empty() || !is_allowed_url(url) { + return Err(StatusCode::UNPROCESSABLE_ENTITY); + } + for e in events { + if !crate::integrations::SUPPORTED_EVENTS.contains(&e.as_str()) { + return Err(StatusCode::UNPROCESSABLE_ENTITY); + } + } + Ok(()) +} + +// --------------------------------------------------------------------------- +// Router +// --------------------------------------------------------------------------- + +pub fn read_router() -> Router { + Router::new() + .route( + "/environments/{env_id}/integrations", + get(list_integrations), + ) + .route( + "/environments/{env_id}/integrations/{id}/deliveries", + get(list_deliveries), + ) +} + +pub fn write_router() -> Router { + Router::new() + .route( + "/environments/{env_id}/integrations", + post(create_integration), + ) + .route( + "/environments/{env_id}/integrations/{id}", + axum::routing::patch(patch_integration).delete(delete_integration), + ) + .route( + "/environments/{env_id}/integrations/{id}/test", + post(test_integration), + ) +} + +// --------------------------------------------------------------------------- +// Handlers +// --------------------------------------------------------------------------- + +async fn list_integrations( + State(state): State, + jar: AuthContext, + Path(env_id): Path, +) -> Result>, StatusCode> { + check_env_access(&state.db, &jar, &env_id).await?; + + let rows = sqlx::query( + "SELECT id::text, environment_id::text, kind, name, webhook_url, events, enabled, created_at::text \ + FROM integrations WHERE environment_id = $1::uuid ORDER BY created_at ASC", + ) + .bind(&env_id) + .fetch_all(&state.db) + .await + .map_err(|e| { + error!(error = %e, "Failed to list integrations"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + Ok(Json(rows.iter().map(row_to_integration).collect())) +} + +async fn create_integration( + State(state): State, + jar: AuthContext, + Path(env_id): Path, + Json(body): Json, +) -> Result, StatusCode> { + check_env_access(&state.db, &jar, &env_id).await?; + validate(&body.kind, &body.webhook_url, &body.events)?; + + let row = sqlx::query( + "INSERT INTO integrations (environment_id, kind, name, webhook_url, events, enabled) \ + VALUES ($1::uuid, $2, $3, $4, $5, $6) \ + RETURNING id::text, environment_id::text, kind, name, webhook_url, events, enabled, created_at::text", + ) + .bind(&env_id) + .bind(&body.kind) + .bind(&body.name) + .bind(&body.webhook_url) + .bind(&body.events) + .bind(body.enabled) + .fetch_one(&state.db) + .await + .map_err(|e| { + error!(error = %e, "Failed to create integration"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let integration = row_to_integration(&row); + info!(env_id = %env_id, integration_id = %integration.id, kind = %integration.kind, "Integration created"); + Ok(Json(integration)) +} + +async fn patch_integration( + State(state): State, + jar: AuthContext, + Path((env_id, id)): Path<(String, String)>, + Json(body): Json, +) -> Result, StatusCode> { + check_env_access(&state.db, &jar, &env_id).await?; + + if let Some(url) = &body.webhook_url + && (url.is_empty() || !is_allowed_url(url)) + { + return Err(StatusCode::UNPROCESSABLE_ENTITY); + } + if let Some(events) = &body.events { + for e in events { + if !crate::integrations::SUPPORTED_EVENTS.contains(&e.as_str()) { + return Err(StatusCode::UNPROCESSABLE_ENTITY); + } + } + } + + let row = sqlx::query( + "UPDATE integrations SET \ + name = COALESCE($1, name), \ + webhook_url = COALESCE($2, webhook_url), \ + events = COALESCE($3, events), \ + enabled = COALESCE($4, enabled) \ + WHERE id = $5::uuid AND environment_id = $6::uuid \ + RETURNING id::text, environment_id::text, kind, name, webhook_url, events, enabled, created_at::text", + ) + .bind(&body.name) + .bind(&body.webhook_url) + .bind(&body.events) + .bind(body.enabled) + .bind(&id) + .bind(&env_id) + .fetch_optional(&state.db) + .await + .map_err(|e| { + error!(error = %e, "Failed to update integration"); + StatusCode::INTERNAL_SERVER_ERROR + })? + .ok_or(StatusCode::NOT_FOUND)?; + + info!(env_id = %env_id, integration_id = %id, "Integration updated"); + Ok(Json(row_to_integration(&row))) +} + +async fn delete_integration( + State(state): State, + jar: AuthContext, + Path((env_id, id)): Path<(String, String)>, +) -> Result { + check_env_access(&state.db, &jar, &env_id).await?; + + let result = + sqlx::query("DELETE FROM integrations WHERE id = $1::uuid AND environment_id = $2::uuid") + .bind(&id) + .bind(&env_id) + .execute(&state.db) + .await + .map_err(|e| { + error!(error = %e, "Failed to delete integration"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + if result.rows_affected() == 0 { + return Err(StatusCode::NOT_FOUND); + } + + info!(env_id = %env_id, integration_id = %id, "Integration deleted"); + Ok(StatusCode::NO_CONTENT) +} + +/// POST /api/environments/:env_id/integrations/:id/test — send a sample message +/// so the channel wiring can be confirmed before a real event depends on it. +async fn test_integration( + State(state): State, + jar: AuthContext, + Path((env_id, id)): Path<(String, String)>, +) -> Result { + check_env_access(&state.db, &jar, &env_id).await?; + + let exists = sqlx::query( + "SELECT 1 AS ok FROM integrations WHERE id = $1::uuid AND environment_id = $2::uuid", + ) + .bind(&id) + .bind(&env_id) + .fetch_optional(&state.db) + .await + .map_err(|e| { + error!(error = %e, "Failed to look up integration for test"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + if exists.is_none() { + return Err(StatusCode::NOT_FOUND); + } + + // Reuses the normal delivery path, so a passing test exercises exactly what + // a real event will do — including the subscription filter. + let payload = super::webhooks::flag_event_payload( + "flag.updated", + &env_id, + "checkgate_test_flag", + Some(&serde_json::json!({ "is_enabled": true, "rollout_percentage": 100 })), + Some("checkgate"), + None, + ); + crate::integrations::fire_integrations(state.clone(), env_id.clone(), payload); + + info!(env_id = %env_id, integration_id = %id, "Integration test message queued"); + Ok(StatusCode::ACCEPTED) +} + +async fn list_deliveries( + State(state): State, + jar: AuthContext, + Path((env_id, id)): Path<(String, String)>, +) -> Result>, StatusCode> { + check_env_access(&state.db, &jar, &env_id).await?; + + let rows = sqlx::query( + "SELECT d.id, d.integration_id::text, d.event, d.status_code, d.response_body, \ + d.error, d.delivered_at::text \ + FROM integration_deliveries d \ + JOIN integrations i ON i.id = d.integration_id \ + WHERE d.integration_id = $1::uuid AND i.environment_id = $2::uuid \ + ORDER BY d.delivered_at DESC LIMIT 50", + ) + .bind(&id) + .bind(&env_id) + .fetch_all(&state.db) + .await + .map_err(|e| { + error!(error = %e, "Failed to list integration deliveries"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let deliveries = rows + .iter() + .map(|r| IntegrationDelivery { + id: r.get("id"), + integration_id: r.get("integration_id"), + event: r.get("event"), + status_code: r.get("status_code"), + response_body: r.get("response_body"), + error: r.get("error"), + delivered_at: r.get("delivered_at"), + }) + .collect(); + + Ok(Json(deliveries)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn preview_hides_all_but_the_tail() { + let url = "https://hooks.slack.com/services/T000/B000/XXXXsecretXXXX"; + let preview = preview_url(url); + assert_eq!(preview, "…etXXXX"); + assert!(!preview.contains("hooks.slack.com")); + assert!(!preview.contains("secret")); + // Whatever the tail length, the credential-bearing path must not survive. + assert!(preview.len() < url.len() / 4); + } + + #[test] + fn preview_handles_short_urls_without_panicking() { + assert_eq!(preview_url("abc"), "…abc"); + assert_eq!(preview_url(""), "…"); + } + + #[test] + fn rejects_unknown_provider() { + assert_eq!( + validate("irc", "https://example.com/hook", &[]), + Err(StatusCode::UNPROCESSABLE_ENTITY) + ); + } + + #[test] + fn requires_https_off_loopback() { + // These URLs are bearer credentials; posting them over plaintext would + // leak the ability to write into the channel. + assert_eq!( + validate("slack", "http://hooks.slack.com/x", &[]), + Err(StatusCode::UNPROCESSABLE_ENTITY) + ); + assert!(validate("slack", "https://hooks.slack.com/x", &[]).is_ok()); + } + + #[test] + fn allows_plaintext_only_on_loopback() { + assert!(is_allowed_url("http://127.0.0.1:8080/hook")); + assert!(is_allowed_url("http://localhost/hook")); + assert!(is_allowed_url("http://[::1]:3000/hook")); + assert!(is_allowed_url("https://anything.example.com/hook")); + } + + #[test] + fn loopback_lookalikes_are_rejected() { + // Each of these would resolve to an attacker-controlled host while + // *looking* like loopback to a naive prefix/contains check. + for url in [ + "http://127.0.0.1.evil.com/hook", + "http://localhost.evil.com/hook", + "http://evil.com/127.0.0.1", + "http://evil.com#127.0.0.1", + "http://evil.com?x=127.0.0.1", + // Credentials in the authority put the real host after '@'. + "http://127.0.0.1@evil.com/hook", + "http://user:pass@evil.com/hook", + ] { + assert!(!is_allowed_url(url), "should reject {url}"); + } + } + + #[test] + fn non_http_schemes_are_rejected() { + assert!(!is_allowed_url("ftp://127.0.0.1/hook")); + assert!(!is_allowed_url("file:///etc/passwd")); + assert!(!is_allowed_url("127.0.0.1/hook")); + assert!(!is_allowed_url("")); + } + + #[test] + fn rejects_unknown_event_names() { + let events = vec!["flag.exploded".to_string()]; + assert_eq!( + validate("slack", "https://hooks.slack.com/x", &events), + Err(StatusCode::UNPROCESSABLE_ENTITY) + ); + } + + #[test] + fn accepts_every_supported_event() { + let events: Vec = crate::integrations::SUPPORTED_EVENTS + .iter() + .map(|s| s.to_string()) + .collect(); + assert!(validate("teams", "https://outlook.office.com/webhook/x", &events).is_ok()); + } +} diff --git a/server/src/integrations.rs b/server/src/integrations.rs new file mode 100644 index 0000000..faa6160 --- /dev/null +++ b/server/src/integrations.rs @@ -0,0 +1,633 @@ +//! Chat integrations — render Checkgate events as native Slack / Microsoft +//! Teams messages and post them to a configured incoming-webhook URL. +//! +//! Delivery mirrors [`crate::webhook_fire`]: fire-and-forget, retried with +//! backoff, and logged to a bounded per-integration table. The difference is +//! the payload — instead of our own JSON envelope, each provider gets the +//! message shape it renders natively (Slack Block Kit, Teams Adaptive Card). + +use crate::state::AppState; +use serde_json::{Value, json}; +use sqlx::{PgPool, Row}; +use std::time::Duration; +use tracing::{error, info, warn}; + +// --------------------------------------------------------------------------- +// Event vocabulary +// --------------------------------------------------------------------------- + +/// Every event an integration can subscribe to. Exposed to the dashboard so the +/// filter UI never drifts from what the server actually emits. +pub const SUPPORTED_EVENTS: &[&str] = &[ + "flag.created", + "flag.updated", + "flag.deleted", + "flag.promoted", + "change_request.opened", + "change_request.approved", + "change_request.rejected", +]; + +/// Does this integration want `event`? An empty subscription list means "all", +/// so a freshly-created integration is useful without configuring anything. +fn wants_event(subscribed: &[String], event: &str) -> bool { + subscribed.is_empty() || subscribed.iter().any(|e| e == event) +} + +// --------------------------------------------------------------------------- +// Presentation helpers +// --------------------------------------------------------------------------- + +/// Human-readable title for an event, e.g. `flag.created` → "Flag created". +fn event_title(event: &str) -> &str { + match event { + "flag.created" => "Flag created", + "flag.updated" => "Flag updated", + "flag.deleted" => "Flag deleted", + "flag.promoted" => "Flag promoted", + "change_request.opened" => "Change request opened", + "change_request.approved" => "Change request approved", + "change_request.rejected" => "Change request rejected", + other => other, + } +} + +/// Accent colour per event class. Slack takes a hex attachment colour; Teams +/// takes the same value as the card's `themeColor`. +fn event_color(event: &str) -> &'static str { + match event { + "flag.deleted" | "change_request.rejected" => "D64545", // red + "change_request.opened" => "E8A33D", // amber — needs a human + "change_request.approved" => "3DA35D", // green + _ => "10B981", // emerald (brand default) + } +} + +/// `true` when the flag payload says the flag is on. Used to show an at-a-glance +/// enabled/disabled state rather than making people read raw JSON. +fn flag_enabled(payload: &Value) -> Option { + payload + .get("flag")? + .get("is_enabled") + .and_then(Value::as_bool) +} + +/// The `key: value` context lines shared by both providers, in display order. +/// Returned as pairs so each formatter can lay them out in its own idiom. +fn detail_fields(payload: &Value, env_name: &str) -> Vec<(String, String)> { + let mut fields = Vec::new(); + + fields.push(("Environment".to_string(), env_name.to_string())); + + if let Some(key) = payload.get("flag_key").and_then(Value::as_str) { + fields.push(("Flag".to_string(), key.to_string())); + } + if let Some(enabled) = flag_enabled(payload) { + let state = if enabled { "Enabled" } else { "Disabled" }; + fields.push(("State".to_string(), state.to_string())); + } + if let Some(rollout) = payload + .get("flag") + .and_then(|f| f.get("rollout_percentage")) + .and_then(Value::as_i64) + { + fields.push(("Rollout".to_string(), format!("{rollout}%"))); + } + if let Some(actor) = payload.get("actor_email").and_then(Value::as_str) { + fields.push(("By".to_string(), actor.to_string())); + } + if let Some(reason) = payload + .get("metadata") + .and_then(|m| m.get("reason")) + .and_then(Value::as_str) + && !reason.is_empty() + { + fields.push(("Reason".to_string(), reason.to_string())); + } + + fields +} + +// --------------------------------------------------------------------------- +// Slack — Block Kit +// --------------------------------------------------------------------------- + +/// Slack renders `text` as the notification/fallback line and `blocks` as the +/// message body. Both are set so the message is readable in a push notification +/// and in the channel. +pub fn format_slack(payload: &Value, env_name: &str) -> Value { + let event = payload.get("event").and_then(Value::as_str).unwrap_or(""); + let title = event_title(event); + let fields = detail_fields(payload, env_name); + + let fallback = match payload.get("flag_key").and_then(Value::as_str) { + Some(key) => format!("{title}: {key} ({env_name})"), + None => format!("{title} ({env_name})"), + }; + + // Block Kit caps a section at 10 fields; our detail list is well under + // that, but chunking keeps it correct if the list grows. + let field_blocks: Vec = fields + .iter() + .take(10) + .map(|(k, v)| json!({ "type": "mrkdwn", "text": format!("*{k}*\n{v}") })) + .collect(); + + let mut blocks = vec![json!({ + "type": "header", + "text": { "type": "plain_text", "text": title, "emoji": true } + })]; + + if !field_blocks.is_empty() { + blocks.push(json!({ "type": "section", "fields": field_blocks })); + } + + json!({ + "text": fallback, + "attachments": [{ + "color": format!("#{}", event_color(event)), + "blocks": blocks, + }], + }) +} + +// --------------------------------------------------------------------------- +// Microsoft Teams — MessageCard +// --------------------------------------------------------------------------- + +/// Teams incoming webhooks accept the legacy `MessageCard` schema, which is what +/// a plain (non-Bot-Framework) connector renders. Adaptive Cards require a bot +/// registration, which this alerts-only integration deliberately avoids. +pub fn format_teams(payload: &Value, env_name: &str) -> Value { + let event = payload.get("event").and_then(Value::as_str).unwrap_or(""); + let title = event_title(event); + let fields = detail_fields(payload, env_name); + + let facts: Vec = fields + .iter() + .map(|(k, v)| json!({ "name": k, "value": v })) + .collect(); + + let summary = match payload.get("flag_key").and_then(Value::as_str) { + Some(key) => format!("{title}: {key}"), + None => title.to_string(), + }; + + json!({ + "@type": "MessageCard", + "@context": "https://schema.org/extensions", + "themeColor": event_color(event), + "summary": summary, + "sections": [{ + "activityTitle": title, + "facts": facts, + "markdown": false, + }], + }) +} + +/// Render `payload` for `kind`. Returns `None` for an unknown provider rather +/// than guessing — the DB constrains `kind`, so this is defence in depth. +pub fn format_for(kind: &str, payload: &Value, env_name: &str) -> Option { + match kind { + "slack" => Some(format_slack(payload, env_name)), + "teams" => Some(format_teams(payload, env_name)), + _ => None, + } +} + +// --------------------------------------------------------------------------- +// Delivery +// --------------------------------------------------------------------------- + +struct IntegrationRow { + id: String, + kind: String, + webhook_url: String, + events: Vec, +} + +/// Spawn a fire-and-forget task delivering `payload` to every enabled +/// integration for `env_id` that subscribes to the event. Never blocks the +/// request that triggered it. +pub fn fire_integrations(state: AppState, env_id: String, payload: Value) { + tokio::spawn(async move { + deliver_to_env(&state, &env_id, payload).await; + }); +} + +async fn deliver_to_env(state: &AppState, env_id: &str, payload: Value) { + let event = payload + .get("event") + .and_then(Value::as_str) + .unwrap_or("unknown") + .to_string(); + + let rows = match sqlx::query( + "SELECT i.id::text, i.kind, i.webhook_url, i.events, e.name AS env_name \ + FROM integrations i \ + JOIN environments e ON e.id = i.environment_id \ + WHERE i.environment_id = $1::uuid AND i.enabled = true", + ) + .bind(env_id) + .fetch_all(&state.db) + .await + { + Ok(r) => r, + Err(e) => { + error!(error = %e, "Failed to query integrations for delivery"); + return; + } + }; + + if rows.is_empty() { + return; + } + + // Every row carries the same environment name (single-env query). + let env_name: String = rows[0].get("env_name"); + + let integrations: Vec = rows + .iter() + .map(|r| IntegrationRow { + id: r.get("id"), + kind: r.get("kind"), + webhook_url: r.get("webhook_url"), + events: r.get("events"), + }) + .filter(|i| wants_event(&i.events, &event)) + .collect(); + + for integration in integrations { + let Some(message) = format_for(&integration.kind, &payload, &env_name) else { + warn!(kind = %integration.kind, "Unknown integration kind — skipping delivery"); + continue; + }; + + let body = match serde_json::to_vec(&message) { + Ok(b) => b, + Err(e) => { + error!(error = %e, "Failed to serialize integration message"); + continue; + } + }; + + deliver_with_retry( + state, + &integration.id, + &integration.webhook_url, + &body, + &event, + &message, + ) + .await; + } +} + +async fn deliver_with_retry( + state: &AppState, + integration_id: &str, + url: &str, + body: &[u8], + event: &str, + message: &Value, +) { + let backoff_secs = [1u64, 5, 15]; + + for (attempt, &wait) in backoff_secs.iter().enumerate() { + let req = state + .webhook_client + .post(url) + .header("Content-Type", "application/json") + .header("User-Agent", "Checkgate-Integrations/1.0") + .body(body.to_vec()); + + match req.send().await { + Ok(resp) => { + let status = resp.status().as_u16() as i32; + let body_text = resp.text().await.unwrap_or_default(); + let ok = (200..300).contains(&status); + + log_delivery( + &state.db, + integration_id, + event, + message, + Some(status), + Some(&body_text), + None, + ) + .await; + + if ok { + info!(integration_id, status, "Integration message delivered"); + } else { + warn!(integration_id, status, "Integration returned non-2xx"); + } + return; + } + Err(e) => { + let is_last = attempt == backoff_secs.len() - 1; + if is_last { + let err_str = e.to_string(); + warn!(integration_id, error = %e, "Integration delivery failed after all retries"); + log_delivery( + &state.db, + integration_id, + event, + message, + None, + None, + Some(&err_str), + ) + .await; + } else { + warn!(integration_id, attempt, error = %e, "Integration delivery failed — retrying"); + tokio::time::sleep(Duration::from_secs(wait)).await; + } + } + } + } +} + +#[allow(clippy::too_many_arguments)] +async fn log_delivery( + db: &PgPool, + integration_id: &str, + event: &str, + payload: &Value, + status_code: Option, + response_body: Option<&str>, + error: Option<&str>, +) { + if let Err(e) = sqlx::query( + "INSERT INTO integration_deliveries (integration_id, event, payload, status_code, response_body, error) \ + VALUES ($1::uuid, $2, $3, $4, $5, $6)", + ) + .bind(integration_id) + .bind(event) + .bind(payload) + .bind(status_code) + .bind(response_body) + .bind(error) + .execute(db) + .await + { + error!(error = %e, "Failed to log integration delivery"); + } + + // Keep the table bounded — retain the most recent 200 per integration. + if let Err(e) = sqlx::query( + "DELETE FROM integration_deliveries \ + WHERE integration_id = $1::uuid \ + AND id NOT IN ( \ + SELECT id FROM integration_deliveries \ + WHERE integration_id = $1::uuid \ + ORDER BY delivered_at DESC \ + LIMIT 200 \ + )", + ) + .bind(integration_id) + .execute(db) + .await + { + warn!(error = %e, "Failed to prune old integration deliveries"); + } +} + +/// Payload for change-request lifecycle events. Mirrors the shape of +/// [`crate::api::webhooks::flag_event_payload`] so both event families render +/// through the same formatter. +pub fn change_request_payload( + event: &str, + env_id: &str, + flag_key: &str, + change_request_id: i64, + actor_email: Option<&str>, + reason: Option<&str>, +) -> Value { + json!({ + "event": event, + "environment_id": env_id, + "flag_key": flag_key, + "change_request_id": change_request_id, + "actor_email": actor_email, + "metadata": { "reason": reason }, + }) +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + fn flag_payload(event: &str) -> Value { + json!({ + "event": event, + "environment_id": "env-1", + "flag_key": "dark_mode", + "flag": { "is_enabled": true, "rollout_percentage": 25 }, + "actor_email": "dev@example.com", + }) + } + + #[test] + fn empty_subscription_means_all_events() { + assert!(wants_event(&[], "flag.created")); + assert!(wants_event(&[], "change_request.opened")); + } + + #[test] + fn subscription_filters_to_listed_events() { + let subscribed = vec!["flag.deleted".to_string()]; + assert!(wants_event(&subscribed, "flag.deleted")); + assert!(!wants_event(&subscribed, "flag.created")); + } + + #[test] + fn slack_message_has_fallback_text_and_blocks() { + let msg = format_slack(&flag_payload("flag.created"), "Production"); + + // The fallback line is what shows in a push notification, so it has to + // carry the essentials on its own. + let text = msg["text"].as_str().unwrap(); + assert!(text.contains("Flag created")); + assert!(text.contains("dark_mode")); + assert!(text.contains("Production")); + + let blocks = msg["attachments"][0]["blocks"].as_array().unwrap(); + assert_eq!(blocks[0]["type"], "header"); + assert_eq!(blocks[0]["text"]["text"], "Flag created"); + } + + #[test] + fn slack_section_stays_within_block_kit_field_limit() { + let msg = format_slack(&flag_payload("flag.updated"), "Production"); + let fields = msg["attachments"][0]["blocks"][1]["fields"] + .as_array() + .unwrap(); + assert!( + fields.len() <= 10, + "Block Kit rejects a section with more than 10 fields, got {}", + fields.len() + ); + } + + #[test] + fn slack_renders_flag_state_and_rollout() { + let msg = format_slack(&flag_payload("flag.updated"), "Staging"); + let rendered = msg.to_string(); + assert!(rendered.contains("Enabled")); + assert!(rendered.contains("25%")); + assert!(rendered.contains("dev@example.com")); + } + + #[test] + fn slack_shows_disabled_state() { + let mut payload = flag_payload("flag.updated"); + payload["flag"]["is_enabled"] = json!(false); + let msg = format_slack(&payload, "Staging"); + assert!(msg.to_string().contains("Disabled")); + } + + #[test] + fn teams_message_uses_messagecard_schema() { + let msg = format_teams(&flag_payload("flag.created"), "Production"); + + assert_eq!(msg["@type"], "MessageCard"); + assert_eq!(msg["@context"], "https://schema.org/extensions"); + // themeColor must be a bare hex string — Teams rejects a leading '#'. + let theme = msg["themeColor"].as_str().unwrap(); + assert!(!theme.starts_with('#'), "got {theme}"); + + let facts = msg["sections"][0]["facts"].as_array().unwrap(); + assert!( + facts + .iter() + .any(|f| f["name"] == "Flag" && f["value"] == "dark_mode") + ); + assert!( + facts + .iter() + .any(|f| f["name"] == "Environment" && f["value"] == "Production") + ); + } + + #[test] + fn slack_theme_color_is_prefixed_but_teams_is_not() { + // Slack wants '#RRGGBB' in an attachment colour; Teams wants 'RRGGBB'. + // Getting these backwards silently renders wrong, so pin both. + let slack = format_slack(&flag_payload("flag.deleted"), "Prod"); + let teams = format_teams(&flag_payload("flag.deleted"), "Prod"); + assert!( + slack["attachments"][0]["color"] + .as_str() + .unwrap() + .starts_with('#') + ); + assert!(!teams["themeColor"].as_str().unwrap().starts_with('#')); + } + + #[test] + fn deleted_and_rejected_events_are_red() { + assert_eq!(event_color("flag.deleted"), "D64545"); + assert_eq!(event_color("change_request.rejected"), "D64545"); + assert_eq!(event_color("change_request.approved"), "3DA35D"); + } + + #[test] + fn change_request_payload_renders_without_a_flag_body() { + // Change-request events carry no `flag` object — the formatter must not + // assume one is present. + let payload = change_request_payload( + "change_request.opened", + "env-1", + "checkout_v2", + 42, + Some("dev@example.com"), + None, + ); + + let slack = format_slack(&payload, "Production"); + assert!( + slack["text"] + .as_str() + .unwrap() + .contains("Change request opened") + ); + assert!(slack.to_string().contains("checkout_v2")); + + let teams = format_teams(&payload, "Production"); + assert_eq!(teams["@type"], "MessageCard"); + assert!(teams.to_string().contains("checkout_v2")); + } + + #[test] + fn rejection_reason_is_surfaced() { + let payload = change_request_payload( + "change_request.rejected", + "env-1", + "checkout_v2", + 42, + Some("lead@example.com"), + Some("Needs a rollout plan"), + ); + assert!( + format_slack(&payload, "Prod") + .to_string() + .contains("Needs a rollout plan") + ); + assert!( + format_teams(&payload, "Prod") + .to_string() + .contains("Needs a rollout plan") + ); + } + + #[test] + fn empty_reason_is_omitted_rather_than_shown_blank() { + let payload = change_request_payload( + "change_request.rejected", + "env-1", + "checkout_v2", + 42, + Some("lead@example.com"), + Some(""), + ); + let fields = detail_fields(&payload, "Prod"); + assert!(!fields.iter().any(|(k, _)| k == "Reason")); + } + + #[test] + fn unknown_provider_formats_to_none() { + assert!(format_for("irc", &flag_payload("flag.created"), "Prod").is_none()); + assert!(format_for("slack", &flag_payload("flag.created"), "Prod").is_some()); + assert!(format_for("teams", &flag_payload("flag.created"), "Prod").is_some()); + } + + #[test] + fn missing_optional_fields_do_not_panic() { + // A minimal payload — no flag body, no actor, no metadata. + let bare = json!({ "event": "flag.updated", "flag_key": "x" }); + let slack = format_slack(&bare, "Prod"); + let teams = format_teams(&bare, "Prod"); + assert!(slack["text"].as_str().unwrap().contains("Flag updated")); + assert_eq!(teams["@type"], "MessageCard"); + } + + #[test] + fn supported_events_all_have_titles() { + // Guards against adding an event constant without a display title, + // which would leak the raw dotted name into a chat message. + for event in SUPPORTED_EVENTS { + assert_ne!( + event_title(event), + *event, + "{event} has no human-readable title" + ); + } + } +} diff --git a/server/src/main.rs b/server/src/main.rs index 32f1a55..35c03c9 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,5 +1,7 @@ mod api; mod auth; +mod integrations; +mod notify; mod rate_limit; mod scheduler; mod state; diff --git a/server/src/notify.rs b/server/src/notify.rs new file mode 100644 index 0000000..a666682 --- /dev/null +++ b/server/src/notify.rs @@ -0,0 +1,16 @@ +//! Single fan-out point for outbound event notifications. +//! +//! There are two sinks — raw webhooks ([`crate::webhook_fire`]) and chat +//! integrations ([`crate::integrations`]) — and every event should reach both. +//! Call sites go through here rather than invoking each sink directly, so +//! adding an event (or a third sink) can't silently miss one. + +use crate::state::AppState; +use serde_json::Value; + +/// Fan an event out to every configured sink for `env_id`. Non-blocking: each +/// sink spawns its own delivery task. +pub fn notify(state: AppState, env_id: String, payload: Value) { + crate::webhook_fire::fire_webhooks(state.clone(), env_id.clone(), payload.clone()); + crate::integrations::fire_integrations(state, env_id, payload); +} diff --git a/server/src/scheduler.rs b/server/src/scheduler.rs index 2238bf7..f7fd793 100644 --- a/server/src/scheduler.rs +++ b/server/src/scheduler.rs @@ -150,7 +150,7 @@ async fn apply_change(state: &AppState, id: &str, env_id: &str, flag_key: &str, None, Some(&serde_json::json!({"scheduled_change_id": id})), ); - crate::webhook_fire::fire_webhooks(state.clone(), env_id.to_string(), wh_payload); + crate::notify::notify(state.clone(), env_id.to_string(), wh_payload); // Audit log. crate::api::audit::log_audit_event( diff --git a/server/tests/integration.rs b/server/tests/integration.rs index 8c168ad..4ad9e0d 100644 --- a/server/tests/integration.rs +++ b/server/tests/integration.rs @@ -15,6 +15,7 @@ use serde_json::{Value, json}; use std::process::{Child, Command, Stdio}; +use std::sync::{Arc, Mutex}; use std::time::Duration; /// Kills the spawned server when the test ends (including on panic). @@ -133,6 +134,10 @@ async fn full_api_flow() { analytics_and_experiments(&ctx, &sdk_key, &prod_env).await; snapshot(&ctx, &sdk_key).await; rbac_and_change_requests(&ctx, &prod_env).await; + // Runs last on purpose: `rbac_and_change_requests` leaves Production in + // approval-required mode, so a PATCH here queues a change request and + // exercises the change_request.* notifications alongside the flag ones. + chat_integrations(&ctx, &prod_env).await; eprintln!("integration: full_api_flow passed"); } @@ -603,3 +608,389 @@ async fn rbac_and_change_requests(ctx: &Ctx, env: &str) { "a pending change request exists: {crs}" ); } + +// --- Chat integrations (Slack / Teams) ------------------------------------- + +/// A stand-in for a Slack/Teams incoming webhook: accepts POSTs, records the +/// JSON body, and always answers 200. Lets the test assert on the exact message +/// the server produced, over a real socket, rather than trusting the formatter +/// in isolation. +struct MockChat { + url: String, + received: Arc>>, +} + +impl MockChat { + /// Waits until at least `n` messages have arrived, or fails after ~5s. + async fn wait_for(&self, n: usize, what: &str) -> Vec { + for _ in 0..50 { + { + let got = self.received.lock().unwrap(); + if got.len() >= n { + return got.clone(); + } + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + let got = self.received.lock().unwrap(); + panic!( + "timed out waiting for {n} message(s) [{what}]; got {}: {:?}", + got.len(), + *got + ); + } + + fn count(&self) -> usize { + self.received.lock().unwrap().len() + } +} + +/// Splits a raw HTTP request into (header_len, content_length). +fn parse_head(buf: &[u8]) -> Option<(usize, usize)> { + let pos = buf.windows(4).position(|w| w == b"\r\n\r\n")? + 4; + let head = String::from_utf8_lossy(&buf[..pos]).to_lowercase(); + let len = head + .lines() + .find_map(|l| l.strip_prefix("content-length:")) + .and_then(|v| v.trim().parse::().ok()) + .unwrap_or(0); + Some((pos, len)) +} + +async fn start_mock_chat() -> MockChat { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let received: Arc>> = Arc::new(Mutex::new(Vec::new())); + let sink = received.clone(); + + tokio::spawn(async move { + loop { + let Ok((mut socket, _)) = listener.accept().await else { + continue; + }; + let sink = sink.clone(); + tokio::spawn(async move { + let mut buf = Vec::new(); + let mut tmp = [0u8; 4096]; + loop { + match socket.read(&mut tmp).await { + Ok(0) => break, + Ok(n) => { + buf.extend_from_slice(&tmp[..n]); + if let Some((head_len, body_len)) = parse_head(&buf) + && buf.len() >= head_len + body_len + { + if let Ok(v) = serde_json::from_slice::( + &buf[head_len..head_len + body_len], + ) { + sink.lock().unwrap().push(v); + } + break; + } + } + Err(_) => break, + } + } + let _ = socket + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok") + .await; + }); + } + }); + + MockChat { + url: format!("http://127.0.0.1:{port}/hook"), + received, + } +} + +/// The display name the API reports for `env_id` — the notification is expected +/// to carry exactly this, which is what proves the delivery path resolves the +/// environment rather than echoing an id. +async fn env_name(ctx: &Ctx, env_id: &str) -> String { + let projects: Value = ctx + .sess + .get(ctx.url("/api/projects")) + .send() + .await + .unwrap() + .json() + .await + .unwrap(); + let project_id = projects[0]["id"].as_str().unwrap().to_string(); + + let envs: Value = ctx + .sess + .get(ctx.url(&format!("/api/projects/{project_id}/environments"))) + .send() + .await + .unwrap() + .json() + .await + .unwrap(); + + envs.as_array() + .unwrap() + .iter() + .find(|e| e["id"] == env_id) + .unwrap_or_else(|| panic!("environment {env_id} not found in {envs}"))["name"] + .as_str() + .unwrap() + .to_string() +} + +async fn chat_integrations(ctx: &Ctx, env: &str) { + let env_display = env_name(ctx, env).await; + + // --- validation --------------------------------------------------------- + + let bad_kind = ctx + .sess + .post(ctx.url(&format!("/api/environments/{env}/integrations"))) + .header("X-Checkgate-Request", "true") + .json(&json!({ "kind": "irc", "name": "x", "webhook_url": "https://example.com/h" })) + .send() + .await + .unwrap(); + assert_eq!(bad_kind.status(), 422, "unknown provider rejected"); + + let plaintext = ctx + .sess + .post(ctx.url(&format!("/api/environments/{env}/integrations"))) + .header("X-Checkgate-Request", "true") + .json(&json!({ "kind": "slack", "name": "x", "webhook_url": "http://evil.example.com/h" })) + .send() + .await + .unwrap(); + assert_eq!(plaintext.status(), 422, "non-loopback plaintext rejected"); + + let bad_event = ctx + .sess + .post(ctx.url(&format!("/api/environments/{env}/integrations"))) + .header("X-Checkgate-Request", "true") + .json(&json!({ + "kind": "slack", + "name": "x", + "webhook_url": "https://hooks.slack.com/h", + "events": ["flag.exploded"], + })) + .send() + .await + .unwrap(); + assert_eq!(bad_event.status(), 422, "unknown event name rejected"); + + // --- create against a live mock endpoint -------------------------------- + + let slack = start_mock_chat().await; + let teams = start_mock_chat().await; + // Subscribes to a single event, to prove the filter actually filters. + let filtered = start_mock_chat().await; + + let r = ctx + .sess + .post(ctx.url(&format!("/api/environments/{env}/integrations"))) + .header("X-Checkgate-Request", "true") + .json(&json!({ "kind": "slack", "name": "Eng channel", "webhook_url": slack.url })) + .send() + .await + .unwrap(); + assert_eq!(r.status(), 200, "create slack integration"); + let slack_row: Value = r.json().await.unwrap(); + let slack_id = slack_row["id"].as_str().unwrap().to_string(); + + // The webhook URL is a channel-posting credential — it must not come back. + assert!( + slack_row.get("webhook_url").is_none(), + "raw webhook_url must never be returned: {slack_row}" + ); + let preview = slack_row["webhook_url_preview"].as_str().unwrap(); + assert!(preview.starts_with('…'), "preview is elided: {preview}"); + assert!( + !preview.contains("127.0.0.1"), + "preview leaks host: {preview}" + ); + + ctx.sess + .post(ctx.url(&format!("/api/environments/{env}/integrations"))) + .header("X-Checkgate-Request", "true") + .json(&json!({ "kind": "teams", "name": "Ops channel", "webhook_url": teams.url })) + .send() + .await + .unwrap(); + + ctx.sess + .post(ctx.url(&format!("/api/environments/{env}/integrations"))) + .header("X-Checkgate-Request", "true") + .json(&json!({ + "kind": "slack", + "name": "Deletes only", + "webhook_url": filtered.url, + "events": ["flag.deleted"], + })) + .send() + .await + .unwrap(); + + // --- flag.created reaches both providers -------------------------------- + + let r = ctx + .sess + .post(ctx.url(&format!("/api/environments/{env}/flags"))) + .header("X-Checkgate-Request", "true") + .json(&json!({ + "key": "chat_notify", + "is_enabled": true, + "flag_type": "boolean", + "rollout_percentage": 25, + "rules": [], + })) + .send() + .await + .unwrap(); + assert!(r.status().is_success(), "create flag: {}", r.status()); + + let slack_msgs = slack.wait_for(1, "slack flag.created").await; + let msg = &slack_msgs[0]; + // Block Kit shape — a fallback line plus a coloured attachment with blocks. + assert!( + msg["text"].as_str().unwrap().contains("Flag created"), + "slack fallback text: {msg}" + ); + assert!(msg["text"].as_str().unwrap().contains("chat_notify")); + assert_eq!(msg["attachments"][0]["blocks"][0]["type"], "header"); + let rendered = msg.to_string(); + assert!( + rendered.contains(&env_display), + "names the env ({env_display}): {rendered}" + ); + assert!(rendered.contains("25%"), "carries rollout: {rendered}"); + + let teams_msgs = teams.wait_for(1, "teams flag.created").await; + let card = &teams_msgs[0]; + assert_eq!(card["@type"], "MessageCard", "teams card shape: {card}"); + let facts = card["sections"][0]["facts"].as_array().unwrap(); + assert!( + facts + .iter() + .any(|f| f["name"] == "Flag" && f["value"] == "chat_notify"), + "teams facts name the flag: {card}" + ); + + // The delete-only integration must have stayed silent. + assert_eq!( + filtered.count(), + 0, + "event filter must suppress unsubscribed events" + ); + + // --- change_request.opened (Production requires approval by now) --------- + + let r = ctx + .sess + .patch(ctx.url(&format!("/api/environments/{env}/flags/chat_notify"))) + .header("X-Checkgate-Request", "true") + .json(&json!({ "is_enabled": false })) + .send() + .await + .unwrap(); + assert_eq!(r.status(), 202, "patch is queued for approval"); + + let slack_msgs = slack.wait_for(2, "slack change_request.opened").await; + let cr_msg = slack_msgs + .iter() + .find(|m| { + m["text"] + .as_str() + .unwrap_or_default() + .contains("Change request opened") + }) + .unwrap_or_else(|| panic!("no change-request message in {slack_msgs:?}")); + assert!( + cr_msg.to_string().contains("chat_notify"), + "change-request message names the flag: {cr_msg}" + ); + + // --- delivery log ------------------------------------------------------- + + let deliveries: Value = ctx + .sess + .get(ctx.url(&format!( + "/api/environments/{env}/integrations/{slack_id}/deliveries" + ))) + .send() + .await + .unwrap() + .json() + .await + .unwrap(); + let rows = deliveries.as_array().unwrap(); + assert!(!rows.is_empty(), "deliveries were logged: {deliveries}"); + assert_eq!(rows[0]["status_code"], 200, "mock answered 200: {rows:?}"); + + // --- disable stops delivery --------------------------------------------- + + let r = ctx + .sess + .patch(ctx.url(&format!("/api/environments/{env}/integrations/{slack_id}"))) + .header("X-Checkgate-Request", "true") + .json(&json!({ "enabled": false })) + .send() + .await + .unwrap(); + assert_eq!(r.status(), 200, "disable integration"); + + let before = slack.count(); + ctx.sess + .post(ctx.url(&format!("/api/environments/{env}/flags"))) + .header("X-Checkgate-Request", "true") + .json(&json!({ + "key": "chat_notify_two", + "is_enabled": true, + "flag_type": "boolean", + "rules": [], + })) + .send() + .await + .unwrap(); + // Teams is still enabled, so waiting on it gives the disabled Slack hook a + // fair chance to have (wrongly) fired before we assert it didn't. + teams.wait_for(2, "teams still receiving").await; + assert_eq!( + slack.count(), + before, + "a disabled integration must not receive events" + ); + + // --- delete ------------------------------------------------------------- + + let r = ctx + .sess + .delete(ctx.url(&format!("/api/environments/{env}/integrations/{slack_id}"))) + .header("X-Checkgate-Request", "true") + .send() + .await + .unwrap(); + assert_eq!(r.status(), 204, "delete integration"); + + let list: Value = ctx + .sess + .get(ctx.url(&format!("/api/environments/{env}/integrations"))) + .send() + .await + .unwrap() + .json() + .await + .unwrap(); + assert!( + !list + .as_array() + .unwrap() + .iter() + .any(|i| i["id"] == slack_id.as_str()), + "deleted integration is gone: {list}" + ); + + eprintln!("integration: chat_integrations passed"); +} From dea8a6091916177debb1a94cf304be92d7a53772 Mon Sep 17 00:00:00 2001 From: Dennis Paler Date: Sat, 18 Jul 2026 22:13:35 +0800 Subject: [PATCH 4/4] chore: stop tracking TypeScript build artifacts --- .gitignore | 8 ++++++++ dashboard/tsconfig.app.tsbuildinfo | 1 - dashboard/tsconfig.node.tsbuildinfo | 1 - 3 files changed, 8 insertions(+), 2 deletions(-) delete mode 100644 dashboard/tsconfig.app.tsbuildinfo delete mode 100644 dashboard/tsconfig.node.tsbuildinfo diff --git a/.gitignore b/.gitignore index 0b20a22..f45c362 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,14 @@ docs/node_modules/ docs/.vitepress/dist/ server/public/ +# TypeScript incremental build state — regenerated by every `tsc -b`, so +# tracking it churns the diff on each commit. +*.tsbuildinfo + +# The docs site is a pnpm project (docs/pnpm-lock.yaml); a stray bun.lock here +# means two competing lockfiles. +docs/bun.lock + # macOS .DS_Store diff --git a/dashboard/tsconfig.app.tsbuildinfo b/dashboard/tsconfig.app.tsbuildinfo deleted file mode 100644 index 163bb98..0000000 --- a/dashboard/tsconfig.app.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./src/app.test.tsx","./src/app.tsx","./src/api.test.ts","./src/api.ts","./src/main.tsx","./src/setuptests.ts","./src/types.ts","./src/components/drawer.tsx","./src/components/flagform.tsx","./src/components/layout.tsx","./src/components/ruleeditor.tsx","./src/components/sidebar.test.tsx","./src/components/sidebar.tsx","./src/components/topbar.tsx","./src/context/authcontext.tsx","./src/context/environmentcontext.tsx","./src/context/projectcontext.tsx","./src/pages/auditlog.tsx","./src/pages/changerequests.tsx","./src/pages/dashboard.tsx","./src/pages/environmentdiff.tsx","./src/pages/environments.tsx","./src/pages/experiments.tsx","./src/pages/exposure.tsx","./src/pages/flaglist.test.tsx","./src/pages/flaglist.tsx","./src/pages/impressions.tsx","./src/pages/integrations.test.tsx","./src/pages/integrations.tsx","./src/pages/login.tsx","./src/pages/projectsettings.tsx","./src/pages/projects.tsx","./src/pages/scheduled.tsx","./src/pages/sdkhealth.tsx","./src/pages/segments.tsx","./src/pages/settings.tsx","./src/pages/setup.tsx","./src/pages/users.tsx","./src/pages/webhooks.tsx"],"version":"5.9.3"} \ No newline at end of file diff --git a/dashboard/tsconfig.node.tsbuildinfo b/dashboard/tsconfig.node.tsbuildinfo deleted file mode 100644 index 385c59a..0000000 --- a/dashboard/tsconfig.node.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./vite.config.ts","./tailwind.config.ts"],"version":"5.9.3"} \ No newline at end of file