diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0852523 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,77 @@ +# Documentation +README.md +LICENSE +donate.md +screenshots/ +docs/ + +# Development files +.github/ +electron/ +scripts/ +Makefile + +# Build artifacts and cache +build/ +dist/ +public/ +meshchatx/public/ +node_modules/ +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +*.egg +python-dist/ + +# Virtual environments +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.venv/ + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git/ +.gitignore + +# Docker files +Dockerfile* +docker-compose*.yml +.dockerignore + +# Local storage and runtime data +storage/ +testing/ +telemetry_test_lxmf/ + +# Logs +*.log + +# Temporary files +*.tmp +*.temp + +# Environment variables +.env +.env.local +.env.*.local \ No newline at end of file diff --git a/.gitea/workflows/bearer-pr.yml b/.gitea/workflows/bearer-pr.yml new file mode 100644 index 0000000..f8fe964 --- /dev/null +++ b/.gitea/workflows/bearer-pr.yml @@ -0,0 +1,20 @@ +name: Bearer PR Check + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + security-events: write + +jobs: + rule_check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Bearer + uses: bearer/bearer-action@828eeb928ce2f4a7ca5ed57fb8b59508cb8c79bc # v2 + with: + diff: true diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..6cdbc4a --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,343 @@ +name: Build and Release + +on: + push: + tags: + - "*" + workflow_dispatch: + inputs: + build_windows: + description: 'Build Windows' + required: false + default: 'true' + type: boolean + build_mac: + description: 'Build macOS' + required: false + default: 'true' + type: boolean + build_linux: + description: 'Build Linux' + required: false + default: 'true' + type: boolean + build_docker: + description: 'Build Docker' + required: false + default: 'true' + type: boolean + +permissions: + contents: read + +jobs: + build_frontend: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Clone Repo + uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1 + + - name: Install NodeJS + uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e # v1 + with: + node-version: 22 + + - name: Install Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.12" + + - name: Sync versions + run: python scripts/sync_version.py + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install NodeJS Deps + run: pnpm install + + - name: Build Frontend + run: pnpm run build-frontend + + - name: Upload frontend artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: frontend-build + path: meshchatx/public + if-no-files-found: error + + build_desktop: + name: Build Desktop (${{ matrix.name }}) + needs: build_frontend + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - name: windows + os: windows-latest + node: 22 + python: "3.13" + release_artifacts: "dist/*-win-installer.exe,dist/*-win-portable.exe" + build_input: build_windows + dist_script: dist-prebuilt + variant: standard + electron_version: "39.2.4" + - name: mac + os: macos-14 + node: 22 + python: "3.13" + release_artifacts: "dist/*-mac-*.dmg" + build_input: build_mac + dist_script: dist:mac-universal + variant: standard + electron_version: "39.2.4" + - name: linux + os: ubuntu-latest + node: 22 + python: "3.13" + release_artifacts: "dist/*-linux.AppImage,dist/*-linux.deb,python-dist/*.whl" + build_input: build_linux + dist_script: dist-prebuilt + variant: standard + electron_version: "39.2.4" + - name: windows-legacy + os: windows-latest + node: 18 + python: "3.11" + release_artifacts: "dist/*-win-installer*.exe,dist/*-win-portable*.exe" + build_input: build_windows + dist_script: dist-prebuilt + variant: legacy + electron_version: "30.0.8" + - name: linux-legacy + os: ubuntu-latest + node: 18 + python: "3.11" + release_artifacts: "dist/*-linux*.AppImage,dist/*-linux*.deb,python-dist/*.whl" + build_input: build_linux + dist_script: dist-prebuilt + variant: legacy + electron_version: "30.0.8" + permissions: + contents: write + steps: + - name: Clone Repo + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true) + uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1 + + - name: Set legacy Electron version + if: | + matrix.variant == 'legacy' && + (github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true)) + shell: bash + run: | + node -e "const fs=require('fs');const pkg=require('./package.json');pkg.devDependencies.electron='${{ matrix.electron_version }}';fs.writeFileSync('package.json', JSON.stringify(pkg,null,2));" + if [ -f pnpm-lock.yaml ]; then rm pnpm-lock.yaml; fi + + - name: Install NodeJS + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true) + uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e # v1 + with: + node-version: ${{ matrix.node }} + + - name: Install Python + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true) + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ matrix.python }} + + - name: Install Poetry + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true) + run: python -m pip install --upgrade pip poetry + + - name: Sync versions + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true) + run: python scripts/sync_version.py + + - name: Install Python Deps + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true) + run: python -m poetry install + + - name: Install pnpm + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true) + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install NodeJS Deps + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true) + run: pnpm install + + - name: Prepare frontend directory + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true) + run: python scripts/prepare_frontend_dir.py + + - name: Download frontend artifact + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true) + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: frontend-build + path: meshchatx/public + + - name: Install patchelf + if: | + startsWith(matrix.name, 'linux') && + (github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true)) + run: sudo apt-get update && sudo apt-get install -y patchelf + + - name: Build Python wheel + if: | + startsWith(matrix.name, 'linux') && + (github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true)) + run: | + python -m poetry build -f wheel + mkdir -p python-dist + mv dist/*.whl python-dist/ + rm -rf dist + + - name: Build Electron App (Universal) + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true) + run: pnpm run ${{ matrix.dist_script }} + + - name: Rename artifacts for legacy build + if: | + matrix.variant == 'legacy' && + (github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true)) + run: ./scripts/rename_legacy_artifacts.sh + + - name: Upload build artifacts + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs[matrix.build_input] == true) + uses: actions/upload-artifact@v4 + with: + name: build-${{ matrix.name }} + path: | + dist/*-win-installer*.exe + dist/*-win-portable*.exe + dist/*-mac-*.dmg + dist/*-linux*.AppImage + dist/*-linux*.deb + python-dist/*.whl + if-no-files-found: ignore + + create_release: + name: Create Release + needs: build_desktop + runs-on: ubuntu-latest + if: github.event_name == 'push' + permissions: + contents: write + steps: + - name: Download all artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + path: artifacts + + - name: Display structure of downloaded files + run: ls -R artifacts + + - name: Prepare release assets + run: | + mkdir -p release-assets + find artifacts -type f \( -name "*.exe" -o -name "*.dmg" -o -name "*.AppImage" -o -name "*.deb" -o -name "*.whl" \) -exec cp {} release-assets/ \; + ls -lh release-assets/ + + - name: Generate SHA256 checksums + run: | + cd release-assets + echo "## SHA256 Checksums" > release-body.md + echo "" >> release-body.md + + for file in *.exe *.dmg *.AppImage *.deb *.whl; do + if [ -f "$file" ]; then + sha256sum "$file" | tee "${file}.sha256" + echo "\`$(cat "${file}.sha256")\`" >> release-body.md + fi + done + + echo "" >> release-body.md + echo "Individual \`.sha256\` files are included for each artifact." >> release-body.md + + cat release-body.md + echo "" + echo "Generated .sha256 files:" + ls -1 *.sha256 2>/dev/null || echo "No .sha256 files found" + + - name: Create Release + uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1 + with: + draft: true + artifacts: "release-assets/*" + bodyFile: "release-assets/release-body.md" + + build_docker: + runs-on: ubuntu-latest + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.build_docker == 'true') + permissions: + packages: write + contents: read + steps: + - name: Clone Repo + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + + - name: Set lowercase repository owner + run: echo "REPO_OWNER_LC=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_ENV + + - name: Set up QEMU + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 + + - name: Log in to the GitHub Container registry + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker images + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: >- + ghcr.io/${{ env.REPO_OWNER_LC }}/reticulum-meshchatx:latest, + ghcr.io/${{ env.REPO_OWNER_LC }}/reticulum-meshchatx:${{ github.ref_name }} + labels: >- + org.opencontainers.image.title=Reticulum MeshChatX, + org.opencontainers.image.description=Docker image for Reticulum MeshChatX, + org.opencontainers.image.url=https://github.com/${{ github.repository }}/pkgs/container/reticulum-meshchatx/ diff --git a/.gitea/workflows/dependency-review.yml b/.gitea/workflows/dependency-review.yml new file mode 100644 index 0000000..af9b511 --- /dev/null +++ b/.gitea/workflows/dependency-review.yml @@ -0,0 +1,22 @@ +name: 'Dependency review' + +on: + pull_request: + branches: [ "master" ] + +permissions: + contents: read + pull-requests: write + +jobs: + dependency-review: + runs-on: ubuntu-latest + + steps: + - name: 'Checkout repository' + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: 'Dependency Review' + uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4 + with: + comment-summary-in-pr: always \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec7cbda --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# IDE and editor files +.idea +.vscode/ +*.swp +*.swo +*~ + +# Dependencies +node_modules/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +*.egg + +# Virtual environments +venv/ +env/ +ENV/ +env.bak/ +venv.bak/ +.venv/ + +# Build files +/build/ +/dist/ +/meshchatx/public/ +public/ +/electron/build/exe/ +python-dist/ + +# Local storage and runtime data +storage/ +testing/ +telemetry_test_lxmf/ + +# Logs +*.log + +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Environment variables +.env +.env.local +.env.*.local \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..d1a2a0f --- /dev/null +++ b/.npmrc @@ -0,0 +1,6 @@ +registry=https://registry.npmjs.org/ +fetch-retries=5 +fetch-retry-mintimeout=20000 +fetch-retry-maxtimeout=120000 +fetch-timeout=300000 + diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..048274e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +dist +node_modules +build +electron/assets +meshchatx/public +pnpm-lock.yaml +poetry.lock +*.log + diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..d6974cc --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "tabWidth": 4, + "singleQuote": false, + "printWidth": 120, + "trailingComma": "es5", + "endOfLine": "auto" +} + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f4890a9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +# Build arguments +ARG NODE_VERSION=20 +ARG NODE_ALPINE_SHA256=sha256:6a91081a440be0b57336fbc4ee87f3dab1a2fd6f80cdb355dcf960e13bda3b59 +ARG PYTHON_VERSION=3.11 +ARG PYTHON_ALPINE_SHA256=sha256:822ceb965f026bc47ee667e50a44309d2d81087780bbbf64f2005521781a3621 + +# Build the frontend +FROM node:${NODE_VERSION}-alpine@${NODE_ALPINE_SHA256} AS build-frontend + +WORKDIR /src + +# Copy required source files +COPY package.json vite.config.js ./ +COPY pnpm-lock.yaml ./ +COPY meshchatx ./meshchatx + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +# Install NodeJS deps, exluding electron +RUN pnpm install --prod && \ + pnpm run build-frontend + +# Main app build +FROM python:${PYTHON_VERSION}-alpine@${PYTHON_ALPINE_SHA256} + +WORKDIR /app + +# Install Python deps +COPY ./requirements.txt . +RUN apk add --no-cache --virtual .build-deps \ + gcc \ + musl-dev \ + linux-headers \ + python3-dev && \ + pip install -r requirements.txt && \ + apk del .build-deps + +# Copy prebuilt frontend +COPY --from=build-frontend /src/meshchatx/public meshchatx/public + +# Copy other required source files +COPY meshchatx ./meshchatx +COPY pyproject.toml poetry.lock ./ + +CMD ["python", "-m", "meshchatx.meshchat", "--host=0.0.0.0", "--reticulum-config-dir=/config/.reticulum", "--storage-dir=/config/.meshchat", "--headless"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..70290ae --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Liam Cottle +Copyright (c) 2026 Sudo-Ivan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 1b52b63..4e7707c 100644 --- a/README.md +++ b/README.md @@ -4,40 +4,26 @@ A heavily customized and updated fork of [Reticulum MeshChat](https://github.com ## Features of this Fork -| Feature | Description | -|-------------------------------------------------------------------|-----------------------------------------------------------------------------| -| Custom UI/UX | Modern, improved interface | -| Inbound LXMF & local node stamps | Configure inbound messaging and node propogation addresses | -| Improved config parsing | More accurate and flexible parsing of config files | -| Automatic HTTPS | Generates self-signed certificates for secure access | -| Cancelable fetches & downloads | Cancel page fetching or downloading | -| Built-in page archiving | Archive pages; background crawler automatically archives nodes that announces| -| Translator tool | Translate messages via Argos Translate or LibreTranslate API | -| Network visualization | Faster, improved visualization page | -| User & node blocking | Block specific users or nodes | -| Database insights | Advanced settings and raw database access | -| Multi-language support | Internationalization (i18n) provided | -| Offline maps (OpenLayers + MBTiles) | Interactive map using OpenLayers with MBTiles offline support | -| Extra tools | RNCP, RNStatus, RNProbe, Translator, Message Forwarding | -| Major codebase reorganization | Cleaner, refactored architecture | -| Better Dependency management | Poetry for Python, PNPM for Node.js packages | -| Increased statistics | More network and usage stats (About page) | -| Supply Chain Protection | Actions and docker images use full SHA hashes | -| Docker optimizations | Smaller sizes, more secure | -| Electron improvements | Security, ASAR packaging | -| Updated dependencies | Latest PNPM and Python package versions | -| Linting & SAST cleanup | Improved code quality and security | -| Performance improvements | Faster and more efficient operation | -| SQLite backend | Raw SQLite database backend (replaces Peewee ORM) | -| Map | OpenLayers and MBTiles support | +### Major + +- Full LXST support. +- Map (w/ MBTiles support for offline) +- Security improvements +- Custom UI/UX +- More Tools +- Built-in page archiving and automatic crawler (no multi-page support yet). +- Block LXMF users and NomadNet Nodes +- Toast system for notifications +- i18n support (En, De, Ru) +- Raw SQLite database backend (replaced Peewee ORM) ## TODO - [ ] Tests and proper CI/CD pipeline. - [ ] RNS hot reload fix - [ ] Backup/Import identities, messages and interfaces. -- [ ] Full LXST support. - [ ] Offline Reticulum documentation tool +- [ ] LXMF Telemtry for map - [ ] Spam filter (based on keywords) - [ ] Multi-identity support. - [ ] TAK tool/integration diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..f4fc95a --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,153 @@ +version: '3' + +vars: + PYTHON: + sh: echo "${PYTHON:-python}" + NPM: + sh: echo "${NPM:-pnpm}" + LEGACY_ELECTRON_VERSION: + sh: echo "${LEGACY_ELECTRON_VERSION:-30.0.8}" + DOCKER_COMPOSE_CMD: + sh: echo "${DOCKER_COMPOSE_CMD:-docker compose}" + DOCKER_COMPOSE_FILE: + sh: echo "${DOCKER_COMPOSE_FILE:-docker-compose.yml}" + DOCKER_IMAGE: + sh: echo "${DOCKER_IMAGE:-reticulum-meshchatx:local}" + DOCKER_BUILDER: + sh: echo "${DOCKER_BUILDER:-meshchatx-builder}" + DOCKER_PLATFORMS: + sh: echo "${DOCKER_PLATFORMS:-linux/amd64}" + DOCKER_BUILD_FLAGS: + sh: echo "${DOCKER_BUILD_FLAGS:---load}" + DOCKER_BUILD_ARGS: + sh: echo "${DOCKER_BUILD_ARGS:-}" + DOCKER_CONTEXT: + sh: echo "${DOCKER_CONTEXT:-.}" + DOCKERFILE: + sh: echo "${DOCKERFILE:-Dockerfile}" + +tasks: + default: + desc: Show available tasks + cmds: + - task --list + + install: + desc: Install all dependencies (syncs version, installs node modules and python deps) + deps: [sync-version, node_modules, python] + + node_modules: + desc: Install Node.js dependencies + cmds: + - '{{.NPM}} install' + + python: + desc: Install Python dependencies using Poetry + cmds: + - '{{.PYTHON}} -m poetry install' + + run: + desc: Run the application + deps: [install] + cmds: + - '{{.PYTHON}} -m poetry run meshchat' + + develop: + desc: Run the application in development mode + cmds: + - task: run + + build: + desc: Build the application (frontend and backend) + deps: [install] + cmds: + - '{{.NPM}} run build' + + build-frontend: + desc: Build only the frontend + deps: [node_modules] + cmds: + - '{{.NPM}} run build-frontend' + + wheel: + desc: Build Python wheel package + deps: [install] + cmds: + - '{{.PYTHON}} -m poetry build -f wheel' + - '{{.PYTHON}} scripts/move_wheels.py' + + build-appimage: + desc: Build Linux AppImage + deps: [build] + cmds: + - '{{.NPM}} run electron-postinstall' + - '{{.NPM}} run dist -- --linux AppImage' + + build-exe: + desc: Build Windows portable executable + deps: [build] + cmds: + - '{{.NPM}} run electron-postinstall' + - '{{.NPM}} run dist -- --win portable' + + dist: + desc: Build distribution (defaults to AppImage) + cmds: + - task: build-appimage + + electron-legacy: + desc: Install legacy Electron version + cmds: + - '{{.NPM}} install --no-save electron@{{.LEGACY_ELECTRON_VERSION}}' + + build-appimage-legacy: + desc: Build Linux AppImage with legacy Electron version + deps: [build, electron-legacy] + cmds: + - '{{.NPM}} run electron-postinstall' + - '{{.NPM}} run dist -- --linux AppImage' + - './scripts/rename_legacy_artifacts.sh' + + build-exe-legacy: + desc: Build Windows portable executable with legacy Electron version + deps: [build, electron-legacy] + cmds: + - '{{.NPM}} run electron-postinstall' + - '{{.NPM}} run dist -- --win portable' + - './scripts/rename_legacy_artifacts.sh' + + clean: + desc: Clean build artifacts and dependencies + cmds: + - rm -rf node_modules + - rm -rf build + - rm -rf dist + - rm -rf python-dist + - rm -rf meshchatx/public + + sync-version: + desc: Sync version numbers across project files + cmds: + - '{{.PYTHON}} scripts/sync_version.py' + + build-docker: + desc: Build Docker image using buildx + cmds: + - | + if ! docker buildx inspect {{.DOCKER_BUILDER}} >/dev/null 2>&1; then + docker buildx create --name {{.DOCKER_BUILDER}} --use >/dev/null + else + docker buildx use {{.DOCKER_BUILDER}} + fi + - | + docker buildx build --builder {{.DOCKER_BUILDER}} --platform {{.DOCKER_PLATFORMS}} \ + {{.DOCKER_BUILD_FLAGS}} \ + -t {{.DOCKER_IMAGE}} \ + {{.DOCKER_BUILD_ARGS}} \ + -f {{.DOCKERFILE}} \ + {{.DOCKER_CONTEXT}} + + run-docker: + desc: Run Docker container using docker-compose + cmds: + - 'MESHCHAT_IMAGE="{{.DOCKER_IMAGE}}" {{.DOCKER_COMPOSE_CMD}} -f {{.DOCKER_COMPOSE_FILE}} up --remove-orphans --pull never reticulum-meshchatx' diff --git a/cx_setup.py b/cx_setup.py new file mode 100644 index 0000000..ec354b0 --- /dev/null +++ b/cx_setup.py @@ -0,0 +1,57 @@ +import sys +from pathlib import Path + +from cx_Freeze import Executable, setup + +from meshchatx.src.version import __version__ + +ROOT = Path(__file__).resolve().parent +PUBLIC_DIR = ROOT / "meshchatx" / "public" + +include_files = [ + (str(PUBLIC_DIR), "public"), + ("logo", "logo"), +] + +packages = [ + "RNS", + "RNS.Interfaces", + "LXMF", + "LXST", + "pycparser", + "cffi", + "ply", +] + +if sys.version_info >= (3, 13): + packages.append("audioop") + +setup( + name="ReticulumMeshChatX", + version=__version__, + description="A simple mesh network communications app powered by the Reticulum Network Stack", + executables=[ + Executable( + script="meshchatx/meshchat.py", + base=None, + target_name="ReticulumMeshChatX", + shortcut_name="ReticulumMeshChatX", + shortcut_dir="ProgramMenuFolder", + icon="logo/icon.ico", + ), + ], + options={ + "build_exe": { + "packages": packages, + "include_files": include_files, + "excludes": [ + "PIL", + ], + "optimize": 1, + "build_exe": "build/exe", + "replace_paths": [ + ("*", ""), + ], + }, + }, +) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d513770 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +services: + reticulum-meshchatx: + container_name: reticulum-meshchatx + image: ${MESHCHAT_IMAGE:-ghcr.io/sudo-ivan/reticulum-meshchatx:latest} + pull_policy: always + restart: unless-stopped + # Make the meshchat web interface accessible from the host on port 8000 + ports: + - 127.0.0.1:8000:8000 + volumes: + - meshchat-config:/config + # Uncomment if you have a USB device connected, such as an RNode + # devices: + # - /dev/ttyUSB0:/dev/ttyUSB0 + +volumes: + meshchat-config: diff --git a/docs/meshchat_on_android_with_termux.md b/docs/meshchat_on_android_with_termux.md new file mode 100644 index 0000000..14eedcc --- /dev/null +++ b/docs/meshchat_on_android_with_termux.md @@ -0,0 +1,31 @@ +# MeshChat on Android + +It's possible to run on Android from source, using [Termux](https://termux.dev/). + +You will need to install a few extra dependencies and make a change to `requirements.txt`. + +``` +pkg upgrade +pkg install git +pkg install nodejs-lts +pkg install python-pip +pkg install rust +pkg install binutils +pkg install build-essential +``` + +You should now be able to follow the [how to use it](../README.md#how-to-use-it) instructions above. + +Before running `pip install -r requirements.txt`, you will need to comment out the `cx_freeze` dependency. It failed to build on my Android tablet, and is not actually required for running from source. + +``` +nano requirements.txt +``` + +Ensure the `cx_freeze` line is updated to `#cx_freeze` + +> Note: Building wheel for cryptography may take a while on Android. + +Once MeshChat is running via Termux, open your favourite Android web browser, and navigate to http://localhost:8000 + +> Note: The default `AutoInterface` may not work on your Android device. You will need to configure another interface such as `TCPClientInterface`. diff --git a/docs/meshchat_on_docker.md b/docs/meshchat_on_docker.md new file mode 100644 index 0000000..d82ddee --- /dev/null +++ b/docs/meshchat_on_docker.md @@ -0,0 +1,11 @@ +# MeshChat on Docker + +A docker image is automatically built by GitHub actions, and can be downloaded from the GitHub container registry. + +``` +docker pull ghcr.io/liamcottle/reticulum-meshchat:latest +``` + +Additionally, an example [docker-compose.yml](../docker-compose.yml) is available. + +The example automatically generates a new reticulum config file in the `meshchat-config` volume. The MeshChat database is also stored in this volume. diff --git a/docs/meshchat_on_raspberry_pi.md b/docs/meshchat_on_raspberry_pi.md new file mode 100644 index 0000000..767b74b --- /dev/null +++ b/docs/meshchat_on_raspberry_pi.md @@ -0,0 +1,99 @@ +# MeshChat on a Raspberry Pi + +A simple guide to install [MeshChat](https://github.com/liamcottle/reticulum-meshchat) on a Raspberry Pi. + +This would allow you to connect an [RNode](https://github.com/markqvist/RNode_Firmware) (such as a Heltec v3) to the Rasbperry Pi via USB, and then access the MeshChat Web UI from another machine on your network. + +My intended use case is to run the Pi + RNode combo from my solar-powered shed, and access the MeshChat Web UI via WiFi. + +> Note: This has been tested on a Raspberry Pi 4 Model B + +## Install Raspberry Pi OS + +If you haven't already done so, the first step is to install Raspberry Pi OS onto an sdcard, and then boot up the Pi. Once booted, follow the below commands. + +## Update System + +``` +sudo apt update +sudo apt upgrade +``` + +## Install System Dependencies + +``` +sudo apt install git +sudo apt install python3-pip +``` + +## Install NodeJS v22 + +``` +curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/nodesource.gpg +NODE_MAJOR=22 +echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list +sudo apt update +sudo apt install nodejs +``` + +## Install pnpm + +``` +corepack enable +corepack prepare pnpm@latest --activate +``` + +## Install MeshChat + +``` +git clone https://github.com/liamcottle/reticulum-meshchat +cd reticulum-meshchat +pip install -r requirements.txt --break-system-packages +pnpm install --prod +pnpm run build-frontend +``` + +## Run MeshChat + +``` +python meshchat.py --headless --host 0.0.0.0 +``` + +## Configure Service + +Adding a `systemd` service will allow MeshChat to run in the background when you disconnect from the Pi's terminal. + +``` +sudo nano /etc/systemd/system/reticulum-meshchat.service +``` + +``` +[Unit] +Description=reticulum-meshchat +After=network.target +StartLimitIntervalSec=0 + +[Service] +Type=simple +Restart=always +RestartSec=1 +User=liamcottle +Group=liamcottle +WorkingDirectory=/home/liamcottle/reticulum-meshchat +ExecStart=/usr/bin/env python /home/liamcottle/reticulum-meshchat/meshchat.py --headless --host 0.0.0.0 + +[Install] +WantedBy=multi-user.target +``` + +> Note: Make sure to update the usernames in the service file if needed. + +``` +sudo systemctl enable reticulum-meshchat.service +sudo systemctl start reticulum-meshchat.service +sudo systemctl status reticulum-meshchat.service +``` + +You should now be able to access MeshChat via your Pi's IP address. + +> Note: Don't forget to include the default port `8000` \ No newline at end of file diff --git a/electron/assets/images/logo.png b/electron/assets/images/logo.png new file mode 100644 index 0000000..aea1c67 Binary files /dev/null and b/electron/assets/images/logo.png differ diff --git a/electron/assets/js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js b/electron/assets/js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js new file mode 100644 index 0000000..8369aad --- /dev/null +++ b/electron/assets/js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js @@ -0,0 +1,62 @@ +(()=>{var Rb=Object.create;var ci=Object.defineProperty;var Mb=Object.getOwnPropertyDescriptor;var Bb=Object.getOwnPropertyNames;var Fb=Object.getPrototypeOf,Lb=Object.prototype.hasOwnProperty;var gu=r=>ci(r,"__esModule",{value:!0});var yu=r=>{if(typeof require!="undefined")return require(r);throw new Error('Dynamic require of "'+r+'" is not supported')};var C=(r,e)=>()=>(r&&(e=r(r=0)),e);var v=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports),Ae=(r,e)=>{gu(r);for(var t in e)ci(r,t,{get:e[t],enumerable:!0})},Nb=(r,e,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Bb(e))!Lb.call(r,i)&&i!=="default"&&ci(r,i,{get:()=>e[i],enumerable:!(t=Mb(e,i))||t.enumerable});return r},X=r=>Nb(gu(ci(r!=null?Rb(Fb(r)):{},"default",r&&r.__esModule&&"default"in r?{get:()=>r.default,enumerable:!0}:{value:r,enumerable:!0})),r);var h,l=C(()=>{h={platform:"",env:{},versions:{node:"14.17.6"}}});var $b,te,ze=C(()=>{l();$b=0,te={readFileSync:r=>self[r]||"",statSync:()=>({mtimeMs:$b++}),promises:{readFile:r=>Promise.resolve(self[r]||"")}}});var rs=v((eE,bu)=>{l();"use strict";var wu=class{constructor(e={}){if(!(e.maxSize&&e.maxSize>0))throw new TypeError("`maxSize` must be a number greater than 0");if(typeof e.maxAge=="number"&&e.maxAge===0)throw new TypeError("`maxAge` must be a number greater than 0");this.maxSize=e.maxSize,this.maxAge=e.maxAge||1/0,this.onEviction=e.onEviction,this.cache=new Map,this.oldCache=new Map,this._size=0}_emitEvictions(e){if(typeof this.onEviction=="function")for(let[t,i]of e)this.onEviction(t,i.value)}_deleteIfExpired(e,t){return typeof t.expiry=="number"&&t.expiry<=Date.now()?(typeof this.onEviction=="function"&&this.onEviction(e,t.value),this.delete(e)):!1}_getOrDeleteIfExpired(e,t){if(this._deleteIfExpired(e,t)===!1)return t.value}_getItemValue(e,t){return t.expiry?this._getOrDeleteIfExpired(e,t):t.value}_peek(e,t){let i=t.get(e);return this._getItemValue(e,i)}_set(e,t){this.cache.set(e,t),this._size++,this._size>=this.maxSize&&(this._size=0,this._emitEvictions(this.oldCache),this.oldCache=this.cache,this.cache=new Map)}_moveToRecent(e,t){this.oldCache.delete(e),this._set(e,t)}*_entriesAscending(){for(let e of this.oldCache){let[t,i]=e;this.cache.has(t)||this._deleteIfExpired(t,i)===!1&&(yield e)}for(let e of this.cache){let[t,i]=e;this._deleteIfExpired(t,i)===!1&&(yield e)}}get(e){if(this.cache.has(e)){let t=this.cache.get(e);return this._getItemValue(e,t)}if(this.oldCache.has(e)){let t=this.oldCache.get(e);if(this._deleteIfExpired(e,t)===!1)return this._moveToRecent(e,t),t.value}}set(e,t,{maxAge:i=this.maxAge===1/0?void 0:Date.now()+this.maxAge}={}){this.cache.has(e)?this.cache.set(e,{value:t,maxAge:i}):this._set(e,{value:t,expiry:i})}has(e){return this.cache.has(e)?!this._deleteIfExpired(e,this.cache.get(e)):this.oldCache.has(e)?!this._deleteIfExpired(e,this.oldCache.get(e)):!1}peek(e){if(this.cache.has(e))return this._peek(e,this.cache);if(this.oldCache.has(e))return this._peek(e,this.oldCache)}delete(e){let t=this.cache.delete(e);return t&&this._size--,this.oldCache.delete(e)||t}clear(){this.cache.clear(),this.oldCache.clear(),this._size=0}resize(e){if(!(e&&e>0))throw new TypeError("`maxSize` must be a number greater than 0");let t=[...this._entriesAscending()],i=t.length-e;i<0?(this.cache=new Map(t),this.oldCache=new Map,this._size=t.length):(i>0&&this._emitEvictions(t.slice(0,i)),this.oldCache=new Map(t.slice(i)),this.cache=new Map,this._size=0),this.maxSize=e}*keys(){for(let[e]of this)yield e}*values(){for(let[,e]of this)yield e}*[Symbol.iterator](){for(let e of this.cache){let[t,i]=e;this._deleteIfExpired(t,i)===!1&&(yield[t,i.value])}for(let e of this.oldCache){let[t,i]=e;this.cache.has(t)||this._deleteIfExpired(t,i)===!1&&(yield[t,i.value])}}*entriesDescending(){let e=[...this.cache];for(let t=e.length-1;t>=0;--t){let i=e[t],[n,a]=i;this._deleteIfExpired(n,a)===!1&&(yield[n,a.value])}e=[...this.oldCache];for(let t=e.length-1;t>=0;--t){let i=e[t],[n,a]=i;this.cache.has(n)||this._deleteIfExpired(n,a)===!1&&(yield[n,a.value])}}*entriesAscending(){for(let[e,t]of this._entriesAscending())yield[e,t.value]}get size(){if(!this._size)return this.oldCache.size;let e=0;for(let t of this.oldCache.keys())this.cache.has(t)||e++;return Math.min(this._size+e,this.maxSize)}};bu.exports=wu});var vu,xu=C(()=>{l();vu=r=>r&&r._hash});function pi(r){return vu(r,{ignoreUnknown:!0})}var ku=C(()=>{l();xu()});function Ke(r){if(r=`${r}`,r==="0")return"0";if(/^[+-]?(\d+|\d*\.\d+)(e[+-]?\d+)?(%|\w+)?$/.test(r))return r.replace(/^[+-]?/,t=>t==="-"?"":"-");let e=["var","calc","min","max","clamp"];for(let t of e)if(r.includes(`${t}(`))return`calc(${r} * -1)`}var di=C(()=>{l()});var Su,Cu=C(()=>{l();Su=["preflight","container","accessibility","pointerEvents","visibility","position","inset","isolation","zIndex","order","gridColumn","gridColumnStart","gridColumnEnd","gridRow","gridRowStart","gridRowEnd","float","clear","margin","boxSizing","lineClamp","display","aspectRatio","size","height","maxHeight","minHeight","width","minWidth","maxWidth","flex","flexShrink","flexGrow","flexBasis","tableLayout","captionSide","borderCollapse","borderSpacing","transformOrigin","translate","rotate","skew","scale","transform","animation","cursor","touchAction","userSelect","resize","scrollSnapType","scrollSnapAlign","scrollSnapStop","scrollMargin","scrollPadding","listStylePosition","listStyleType","listStyleImage","appearance","columns","breakBefore","breakInside","breakAfter","gridAutoColumns","gridAutoFlow","gridAutoRows","gridTemplateColumns","gridTemplateRows","flexDirection","flexWrap","placeContent","placeItems","alignContent","alignItems","justifyContent","justifyItems","gap","space","divideWidth","divideStyle","divideColor","divideOpacity","placeSelf","alignSelf","justifySelf","overflow","overscrollBehavior","scrollBehavior","textOverflow","hyphens","whitespace","textWrap","wordBreak","borderRadius","borderWidth","borderStyle","borderColor","borderOpacity","backgroundColor","backgroundOpacity","backgroundImage","gradientColorStops","boxDecorationBreak","backgroundSize","backgroundAttachment","backgroundClip","backgroundPosition","backgroundRepeat","backgroundOrigin","fill","stroke","strokeWidth","objectFit","objectPosition","padding","textAlign","textIndent","verticalAlign","fontFamily","fontSize","fontWeight","textTransform","fontStyle","fontVariantNumeric","lineHeight","letterSpacing","textColor","textOpacity","textDecoration","textDecorationColor","textDecorationStyle","textDecorationThickness","textUnderlineOffset","fontSmoothing","placeholderColor","placeholderOpacity","caretColor","accentColor","opacity","backgroundBlendMode","mixBlendMode","boxShadow","boxShadowColor","outlineStyle","outlineWidth","outlineOffset","outlineColor","ringWidth","ringColor","ringOpacity","ringOffsetWidth","ringOffsetColor","blur","brightness","contrast","dropShadow","grayscale","hueRotate","invert","saturate","sepia","filter","backdropBlur","backdropBrightness","backdropContrast","backdropGrayscale","backdropHueRotate","backdropInvert","backdropOpacity","backdropSaturate","backdropSepia","backdropFilter","transitionProperty","transitionDelay","transitionDuration","transitionTimingFunction","willChange","contain","content","forcedColorAdjust"]});function Au(r,e){return r===void 0?e:Array.isArray(r)?r:[...new Set(e.filter(i=>r!==!1&&r[i]!==!1).concat(Object.keys(r).filter(i=>r[i]!==!1)))]}var _u=C(()=>{l()});var Ou={};Ae(Ou,{default:()=>_e});var _e,hi=C(()=>{l();_e=new Proxy({},{get:()=>String})});function is(r,e,t){typeof h!="undefined"&&h.env.JEST_WORKER_ID||t&&Eu.has(t)||(t&&Eu.add(t),console.warn(""),e.forEach(i=>console.warn(r,"-",i)))}function ns(r){return _e.dim(r)}var Eu,F,Oe=C(()=>{l();hi();Eu=new Set;F={info(r,e){is(_e.bold(_e.cyan("info")),...Array.isArray(r)?[r]:[e,r])},warn(r,e){["content-problems"].includes(r)||is(_e.bold(_e.yellow("warn")),...Array.isArray(r)?[r]:[e,r])},risk(r,e){is(_e.bold(_e.magenta("risk")),...Array.isArray(r)?[r]:[e,r])}}});var as={};Ae(as,{default:()=>ss});function lr({version:r,from:e,to:t}){F.warn(`${e}-color-renamed`,[`As of Tailwind CSS ${r}, \`${e}\` has been renamed to \`${t}\`.`,"Update your configuration file to silence this warning."])}var ss,mi=C(()=>{l();Oe();ss={inherit:"inherit",current:"currentColor",transparent:"transparent",black:"#000",white:"#fff",slate:{50:"#f8fafc",100:"#f1f5f9",200:"#e2e8f0",300:"#cbd5e1",400:"#94a3b8",500:"#64748b",600:"#475569",700:"#334155",800:"#1e293b",900:"#0f172a",950:"#020617"},gray:{50:"#f9fafb",100:"#f3f4f6",200:"#e5e7eb",300:"#d1d5db",400:"#9ca3af",500:"#6b7280",600:"#4b5563",700:"#374151",800:"#1f2937",900:"#111827",950:"#030712"},zinc:{50:"#fafafa",100:"#f4f4f5",200:"#e4e4e7",300:"#d4d4d8",400:"#a1a1aa",500:"#71717a",600:"#52525b",700:"#3f3f46",800:"#27272a",900:"#18181b",950:"#09090b"},neutral:{50:"#fafafa",100:"#f5f5f5",200:"#e5e5e5",300:"#d4d4d4",400:"#a3a3a3",500:"#737373",600:"#525252",700:"#404040",800:"#262626",900:"#171717",950:"#0a0a0a"},stone:{50:"#fafaf9",100:"#f5f5f4",200:"#e7e5e4",300:"#d6d3d1",400:"#a8a29e",500:"#78716c",600:"#57534e",700:"#44403c",800:"#292524",900:"#1c1917",950:"#0c0a09"},red:{50:"#fef2f2",100:"#fee2e2",200:"#fecaca",300:"#fca5a5",400:"#f87171",500:"#ef4444",600:"#dc2626",700:"#b91c1c",800:"#991b1b",900:"#7f1d1d",950:"#450a0a"},orange:{50:"#fff7ed",100:"#ffedd5",200:"#fed7aa",300:"#fdba74",400:"#fb923c",500:"#f97316",600:"#ea580c",700:"#c2410c",800:"#9a3412",900:"#7c2d12",950:"#431407"},amber:{50:"#fffbeb",100:"#fef3c7",200:"#fde68a",300:"#fcd34d",400:"#fbbf24",500:"#f59e0b",600:"#d97706",700:"#b45309",800:"#92400e",900:"#78350f",950:"#451a03"},yellow:{50:"#fefce8",100:"#fef9c3",200:"#fef08a",300:"#fde047",400:"#facc15",500:"#eab308",600:"#ca8a04",700:"#a16207",800:"#854d0e",900:"#713f12",950:"#422006"},lime:{50:"#f7fee7",100:"#ecfccb",200:"#d9f99d",300:"#bef264",400:"#a3e635",500:"#84cc16",600:"#65a30d",700:"#4d7c0f",800:"#3f6212",900:"#365314",950:"#1a2e05"},green:{50:"#f0fdf4",100:"#dcfce7",200:"#bbf7d0",300:"#86efac",400:"#4ade80",500:"#22c55e",600:"#16a34a",700:"#15803d",800:"#166534",900:"#14532d",950:"#052e16"},emerald:{50:"#ecfdf5",100:"#d1fae5",200:"#a7f3d0",300:"#6ee7b7",400:"#34d399",500:"#10b981",600:"#059669",700:"#047857",800:"#065f46",900:"#064e3b",950:"#022c22"},teal:{50:"#f0fdfa",100:"#ccfbf1",200:"#99f6e4",300:"#5eead4",400:"#2dd4bf",500:"#14b8a6",600:"#0d9488",700:"#0f766e",800:"#115e59",900:"#134e4a",950:"#042f2e"},cyan:{50:"#ecfeff",100:"#cffafe",200:"#a5f3fc",300:"#67e8f9",400:"#22d3ee",500:"#06b6d4",600:"#0891b2",700:"#0e7490",800:"#155e75",900:"#164e63",950:"#083344"},sky:{50:"#f0f9ff",100:"#e0f2fe",200:"#bae6fd",300:"#7dd3fc",400:"#38bdf8",500:"#0ea5e9",600:"#0284c7",700:"#0369a1",800:"#075985",900:"#0c4a6e",950:"#082f49"},blue:{50:"#eff6ff",100:"#dbeafe",200:"#bfdbfe",300:"#93c5fd",400:"#60a5fa",500:"#3b82f6",600:"#2563eb",700:"#1d4ed8",800:"#1e40af",900:"#1e3a8a",950:"#172554"},indigo:{50:"#eef2ff",100:"#e0e7ff",200:"#c7d2fe",300:"#a5b4fc",400:"#818cf8",500:"#6366f1",600:"#4f46e5",700:"#4338ca",800:"#3730a3",900:"#312e81",950:"#1e1b4b"},violet:{50:"#f5f3ff",100:"#ede9fe",200:"#ddd6fe",300:"#c4b5fd",400:"#a78bfa",500:"#8b5cf6",600:"#7c3aed",700:"#6d28d9",800:"#5b21b6",900:"#4c1d95",950:"#2e1065"},purple:{50:"#faf5ff",100:"#f3e8ff",200:"#e9d5ff",300:"#d8b4fe",400:"#c084fc",500:"#a855f7",600:"#9333ea",700:"#7e22ce",800:"#6b21a8",900:"#581c87",950:"#3b0764"},fuchsia:{50:"#fdf4ff",100:"#fae8ff",200:"#f5d0fe",300:"#f0abfc",400:"#e879f9",500:"#d946ef",600:"#c026d3",700:"#a21caf",800:"#86198f",900:"#701a75",950:"#4a044e"},pink:{50:"#fdf2f8",100:"#fce7f3",200:"#fbcfe8",300:"#f9a8d4",400:"#f472b6",500:"#ec4899",600:"#db2777",700:"#be185d",800:"#9d174d",900:"#831843",950:"#500724"},rose:{50:"#fff1f2",100:"#ffe4e6",200:"#fecdd3",300:"#fda4af",400:"#fb7185",500:"#f43f5e",600:"#e11d48",700:"#be123c",800:"#9f1239",900:"#881337",950:"#4c0519"},get lightBlue(){return lr({version:"v2.2",from:"lightBlue",to:"sky"}),this.sky},get warmGray(){return lr({version:"v3.0",from:"warmGray",to:"stone"}),this.stone},get trueGray(){return lr({version:"v3.0",from:"trueGray",to:"neutral"}),this.neutral},get coolGray(){return lr({version:"v3.0",from:"coolGray",to:"gray"}),this.gray},get blueGray(){return lr({version:"v3.0",from:"blueGray",to:"slate"}),this.slate}}});function os(r,...e){for(let t of e){for(let i in t)r?.hasOwnProperty?.(i)||(r[i]=t[i]);for(let i of Object.getOwnPropertySymbols(t))r?.hasOwnProperty?.(i)||(r[i]=t[i])}return r}var Tu=C(()=>{l()});function Ze(r){if(Array.isArray(r))return r;let e=r.split("[").length-1,t=r.split("]").length-1;if(e!==t)throw new Error(`Path is invalid. Has unbalanced brackets: ${r}`);return r.split(/\.(?![^\[]*\])|[\[\]]/g).filter(Boolean)}var gi=C(()=>{l()});function K(r,e){return yi.future.includes(e)?r.future==="all"||(r?.future?.[e]??Pu[e]??!1):yi.experimental.includes(e)?r.experimental==="all"||(r?.experimental?.[e]??Pu[e]??!1):!1}function Du(r){return r.experimental==="all"?yi.experimental:Object.keys(r?.experimental??{}).filter(e=>yi.experimental.includes(e)&&r.experimental[e])}function Iu(r){if(h.env.JEST_WORKER_ID===void 0&&Du(r).length>0){let e=Du(r).map(t=>_e.yellow(t)).join(", ");F.warn("experimental-flags-enabled",[`You have enabled experimental features: ${e}`,"Experimental features in Tailwind CSS are not covered by semver, may introduce breaking changes, and can change at any time."])}}var Pu,yi,je=C(()=>{l();hi();Oe();Pu={optimizeUniversalDefaults:!1,generalizedModifiers:!0,disableColorOpacityUtilitiesByDefault:!1,relativeContentPathsByDefault:!1},yi={future:["hoverOnlyWhenSupported","respectDefaultRingColorOpacity","disableColorOpacityUtilitiesByDefault","relativeContentPathsByDefault"],experimental:["optimizeUniversalDefaults","generalizedModifiers"]}});function qu(r){(()=>{if(r.purge||!r.content||!Array.isArray(r.content)&&!(typeof r.content=="object"&&r.content!==null))return!1;if(Array.isArray(r.content))return r.content.every(t=>typeof t=="string"?!0:!(typeof t?.raw!="string"||t?.extension&&typeof t?.extension!="string"));if(typeof r.content=="object"&&r.content!==null){if(Object.keys(r.content).some(t=>!["files","relative","extract","transform"].includes(t)))return!1;if(Array.isArray(r.content.files)){if(!r.content.files.every(t=>typeof t=="string"?!0:!(typeof t?.raw!="string"||t?.extension&&typeof t?.extension!="string")))return!1;if(typeof r.content.extract=="object"){for(let t of Object.values(r.content.extract))if(typeof t!="function")return!1}else if(!(r.content.extract===void 0||typeof r.content.extract=="function"))return!1;if(typeof r.content.transform=="object"){for(let t of Object.values(r.content.transform))if(typeof t!="function")return!1}else if(!(r.content.transform===void 0||typeof r.content.transform=="function"))return!1;if(typeof r.content.relative!="boolean"&&typeof r.content.relative!="undefined")return!1}return!0}return!1})()||F.warn("purge-deprecation",["The `purge`/`content` options have changed in Tailwind CSS v3.0.","Update your configuration file to eliminate this warning.","https://tailwindcss.com/docs/upgrade-guide#configure-content-sources"]),r.safelist=(()=>{let{content:t,purge:i,safelist:n}=r;return Array.isArray(n)?n:Array.isArray(t?.safelist)?t.safelist:Array.isArray(i?.safelist)?i.safelist:Array.isArray(i?.options?.safelist)?i.options.safelist:[]})(),r.blocklist=(()=>{let{blocklist:t}=r;if(Array.isArray(t)){if(t.every(i=>typeof i=="string"))return t;F.warn("blocklist-invalid",["The `blocklist` option must be an array of strings.","https://tailwindcss.com/docs/content-configuration#discarding-classes"])}return[]})(),typeof r.prefix=="function"?(F.warn("prefix-function",["As of Tailwind CSS v3.0, `prefix` cannot be a function.","Update `prefix` in your configuration to be a string to eliminate this warning.","https://tailwindcss.com/docs/upgrade-guide#prefix-cannot-be-a-function"]),r.prefix=""):r.prefix=r.prefix??"",r.content={relative:(()=>{let{content:t}=r;return t?.relative?t.relative:K(r,"relativeContentPathsByDefault")})(),files:(()=>{let{content:t,purge:i}=r;return Array.isArray(i)?i:Array.isArray(i?.content)?i.content:Array.isArray(t)?t:Array.isArray(t?.content)?t.content:Array.isArray(t?.files)?t.files:[]})(),extract:(()=>{let t=(()=>r.purge?.extract?r.purge.extract:r.content?.extract?r.content.extract:r.purge?.extract?.DEFAULT?r.purge.extract.DEFAULT:r.content?.extract?.DEFAULT?r.content.extract.DEFAULT:r.purge?.options?.extractors?r.purge.options.extractors:r.content?.options?.extractors?r.content.options.extractors:{})(),i={},n=(()=>{if(r.purge?.options?.defaultExtractor)return r.purge.options.defaultExtractor;if(r.content?.options?.defaultExtractor)return r.content.options.defaultExtractor})();if(n!==void 0&&(i.DEFAULT=n),typeof t=="function")i.DEFAULT=t;else if(Array.isArray(t))for(let{extensions:a,extractor:s}of t??[])for(let o of a)i[o]=s;else typeof t=="object"&&t!==null&&Object.assign(i,t);return i})(),transform:(()=>{let t=(()=>r.purge?.transform?r.purge.transform:r.content?.transform?r.content.transform:r.purge?.transform?.DEFAULT?r.purge.transform.DEFAULT:r.content?.transform?.DEFAULT?r.content.transform.DEFAULT:{})(),i={};return typeof t=="function"&&(i.DEFAULT=t),typeof t=="object"&&t!==null&&Object.assign(i,t),i})()};for(let t of r.content.files)if(typeof t=="string"&&/{([^,]*?)}/g.test(t)){F.warn("invalid-glob-braces",[`The glob pattern ${ns(t)} in your Tailwind CSS configuration is invalid.`,`Update it to ${ns(t.replace(/{([^,]*?)}/g,"$1"))} to silence this warning.`]);break}return r}var Ru=C(()=>{l();je();Oe()});function ie(r){if(Object.prototype.toString.call(r)!=="[object Object]")return!1;let e=Object.getPrototypeOf(r);return e===null||Object.getPrototypeOf(e)===null}var At=C(()=>{l()});function et(r){return Array.isArray(r)?r.map(e=>et(e)):typeof r=="object"&&r!==null?Object.fromEntries(Object.entries(r).map(([e,t])=>[e,et(t)])):r}var wi=C(()=>{l()});function wt(r){return r.replace(/\\,/g,"\\2c ")}var bi=C(()=>{l()});var ls,Mu=C(()=>{l();ls={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}});function ur(r,{loose:e=!1}={}){if(typeof r!="string")return null;if(r=r.trim(),r==="transparent")return{mode:"rgb",color:["0","0","0"],alpha:"0"};if(r in ls)return{mode:"rgb",color:ls[r].map(a=>a.toString())};let t=r.replace(jb,(a,s,o,u,c)=>["#",s,s,o,o,u,u,c?c+c:""].join("")).match(zb);if(t!==null)return{mode:"rgb",color:[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)].map(a=>a.toString()),alpha:t[4]?(parseInt(t[4],16)/255).toString():void 0};let i=r.match(Vb)??r.match(Ub);if(i===null)return null;let n=[i[2],i[3],i[4]].filter(Boolean).map(a=>a.toString());return n.length===2&&n[0].startsWith("var(")?{mode:i[1],color:[n[0]],alpha:n[1]}:!e&&n.length!==3||n.length<3&&!n.some(a=>/^var\(.*?\)$/.test(a))?null:{mode:i[1],color:n,alpha:i[5]?.toString?.()}}function us({mode:r,color:e,alpha:t}){let i=t!==void 0;return r==="rgba"||r==="hsla"?`${r}(${e.join(", ")}${i?`, ${t}`:""})`:`${r}(${e.join(" ")}${i?` / ${t}`:""})`}var zb,jb,tt,vi,Bu,rt,Vb,Ub,fs=C(()=>{l();Mu();zb=/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i,jb=/^#([a-f\d])([a-f\d])([a-f\d])([a-f\d])?$/i,tt=/(?:\d+|\d*\.\d+)%?/,vi=/(?:\s*,\s*|\s+)/,Bu=/\s*[,/]\s*/,rt=/var\(--(?:[^ )]*?)(?:,(?:[^ )]*?|var\(--[^ )]*?\)))?\)/,Vb=new RegExp(`^(rgba?)\\(\\s*(${tt.source}|${rt.source})(?:${vi.source}(${tt.source}|${rt.source}))?(?:${vi.source}(${tt.source}|${rt.source}))?(?:${Bu.source}(${tt.source}|${rt.source}))?\\s*\\)$`),Ub=new RegExp(`^(hsla?)\\(\\s*((?:${tt.source})(?:deg|rad|grad|turn)?|${rt.source})(?:${vi.source}(${tt.source}|${rt.source}))?(?:${vi.source}(${tt.source}|${rt.source}))?(?:${Bu.source}(${tt.source}|${rt.source}))?\\s*\\)$`)});function Ie(r,e,t){if(typeof r=="function")return r({opacityValue:e});let i=ur(r,{loose:!0});return i===null?t:us({...i,alpha:e})}function se({color:r,property:e,variable:t}){let i=[].concat(e);if(typeof r=="function")return{[t]:"1",...Object.fromEntries(i.map(a=>[a,r({opacityVariable:t,opacityValue:`var(${t})`})]))};let n=ur(r);return n===null?Object.fromEntries(i.map(a=>[a,r])):n.alpha!==void 0?Object.fromEntries(i.map(a=>[a,r])):{[t]:"1",...Object.fromEntries(i.map(a=>[a,us({...n,alpha:`var(${t})`})]))}}var fr=C(()=>{l();fs()});function ae(r,e){let t=[],i=[],n=0,a=!1;for(let s=0;s{l()});function xi(r){return ae(r,",").map(t=>{let i=t.trim(),n={raw:i},a=i.split(Gb),s=new Set;for(let o of a)Fu.lastIndex=0,!s.has("KEYWORD")&&Wb.has(o)?(n.keyword=o,s.add("KEYWORD")):Fu.test(o)?s.has("X")?s.has("Y")?s.has("BLUR")?s.has("SPREAD")||(n.spread=o,s.add("SPREAD")):(n.blur=o,s.add("BLUR")):(n.y=o,s.add("Y")):(n.x=o,s.add("X")):n.color?(n.unknown||(n.unknown=[]),n.unknown.push(o)):n.color=o;return n.valid=n.x!==void 0&&n.y!==void 0,n})}function Lu(r){return r.map(e=>e.valid?[e.keyword,e.x,e.y,e.blur,e.spread,e.color].filter(Boolean).join(" "):e.raw).join(", ")}var Wb,Gb,Fu,cs=C(()=>{l();_t();Wb=new Set(["inset","inherit","initial","revert","unset"]),Gb=/\ +(?![^(]*\))/g,Fu=/^-?(\d+|\.\d+)(.*?)$/g});function ps(r){return Hb.some(e=>new RegExp(`^${e}\\(.*\\)`).test(r))}function L(r,e=null,t=!0){let i=e&&Yb.has(e.property);return r.startsWith("--")&&!i?`var(${r})`:r.includes("url(")?r.split(/(url\(.*?\))/g).filter(Boolean).map(n=>/^url\(.*?\)$/.test(n)?n:L(n,e,!1)).join(""):(r=r.replace(/([^\\])_+/g,(n,a)=>a+" ".repeat(n.length-1)).replace(/^_/g," ").replace(/\\_/g,"_"),t&&(r=r.trim()),r=Qb(r),r)}function Qb(r){let e=["theme"],t=["min-content","max-content","fit-content","safe-area-inset-top","safe-area-inset-right","safe-area-inset-bottom","safe-area-inset-left","titlebar-area-x","titlebar-area-y","titlebar-area-width","titlebar-area-height","keyboard-inset-top","keyboard-inset-right","keyboard-inset-bottom","keyboard-inset-left","keyboard-inset-width","keyboard-inset-height","radial-gradient","linear-gradient","conic-gradient","repeating-radial-gradient","repeating-linear-gradient","repeating-conic-gradient"];return r.replace(/(calc|min|max|clamp)\(.+\)/g,i=>{let n="";function a(){let s=n.trimEnd();return s[s.length-1]}for(let s=0;si[s+p]===d)},u=function(f){let d=1/0;for(let m of f){let b=i.indexOf(m,s);b!==-1&&bo(f))){let f=t.find(d=>o(d));n+=f,s+=f.length-1}else e.some(f=>o(f))?n+=u([")"]):o("[")?n+=u(["]"]):["+","-","*","/"].includes(c)&&!["(","+","-","*","/",","].includes(a())?n+=` ${c} `:n+=c}return n.replace(/\s+/g," ")})}function ds(r){return r.startsWith("url(")}function hs(r){return!isNaN(Number(r))||ps(r)}function cr(r){return r.endsWith("%")&&hs(r.slice(0,-1))||ps(r)}function pr(r){return r==="0"||new RegExp(`^[+-]?[0-9]*.?[0-9]+(?:[eE][+-]?[0-9]+)?${Xb}$`).test(r)||ps(r)}function Nu(r){return Kb.has(r)}function $u(r){let e=xi(L(r));for(let t of e)if(!t.valid)return!1;return!0}function zu(r){let e=0;return ae(r,"_").every(i=>(i=L(i),i.startsWith("var(")?!0:ur(i,{loose:!0})!==null?(e++,!0):!1))?e>0:!1}function ju(r){let e=0;return ae(r,",").every(i=>(i=L(i),i.startsWith("var(")?!0:ds(i)||e0(i)||["element(","image(","cross-fade(","image-set("].some(n=>i.startsWith(n))?(e++,!0):!1))?e>0:!1}function e0(r){r=L(r);for(let e of Zb)if(r.startsWith(`${e}(`))return!0;return!1}function Vu(r){let e=0;return ae(r,"_").every(i=>(i=L(i),i.startsWith("var(")?!0:t0.has(i)||pr(i)||cr(i)?(e++,!0):!1))?e>0:!1}function Uu(r){let e=0;return ae(r,",").every(i=>(i=L(i),i.startsWith("var(")?!0:i.includes(" ")&&!/(['"])([^"']+)\1/g.test(i)||/^\d/g.test(i)?!1:(e++,!0)))?e>0:!1}function Wu(r){return r0.has(r)}function Gu(r){return i0.has(r)}function Hu(r){return n0.has(r)}var Hb,Yb,Jb,Xb,Kb,Zb,t0,r0,i0,n0,dr=C(()=>{l();fs();cs();_t();Hb=["min","max","clamp","calc"];Yb=new Set(["scroll-timeline-name","timeline-scope","view-timeline-name","font-palette","scroll-timeline","animation-timeline","view-timeline"]);Jb=["cm","mm","Q","in","pc","pt","px","em","ex","ch","rem","lh","rlh","vw","vh","vmin","vmax","vb","vi","svw","svh","lvw","lvh","dvw","dvh","cqw","cqh","cqi","cqb","cqmin","cqmax"],Xb=`(?:${Jb.join("|")})`;Kb=new Set(["thin","medium","thick"]);Zb=new Set(["conic-gradient","linear-gradient","radial-gradient","repeating-conic-gradient","repeating-linear-gradient","repeating-radial-gradient"]);t0=new Set(["center","top","right","bottom","left"]);r0=new Set(["serif","sans-serif","monospace","cursive","fantasy","system-ui","ui-serif","ui-sans-serif","ui-monospace","ui-rounded","math","emoji","fangsong"]);i0=new Set(["xx-small","x-small","small","medium","large","x-large","xx-large","xxx-large"]);n0=new Set(["larger","smaller"])});function Yu(r){let e=["cover","contain"];return ae(r,",").every(t=>{let i=ae(t,"_").filter(Boolean);return i.length===1&&e.includes(i[0])?!0:i.length!==1&&i.length!==2?!1:i.every(n=>pr(n)||cr(n)||n==="auto")})}var Qu=C(()=>{l();dr();_t()});function Ju(r,e){r.walkClasses(t=>{t.value=e(t.value),t.raws&&t.raws.value&&(t.raws.value=wt(t.raws.value))})}function Xu(r,e){if(!it(r))return;let t=r.slice(1,-1);if(!!e(t))return L(t)}function s0(r,e={},t){let i=e[r];if(i!==void 0)return Ke(i);if(it(r)){let n=Xu(r,t);return n===void 0?void 0:Ke(n)}}function ki(r,e={},{validate:t=()=>!0}={}){let i=e.values?.[r];return i!==void 0?i:e.supportsNegativeValues&&r.startsWith("-")?s0(r.slice(1),e.values,t):Xu(r,t)}function it(r){return r.startsWith("[")&&r.endsWith("]")}function Ku(r){let e=r.lastIndexOf("/"),t=r.lastIndexOf("[",e),i=r.indexOf("]",e);return r[e-1]==="]"||r[e+1]==="["||t!==-1&&i!==-1&&t")){let e=r;return({opacityValue:t=1})=>e.replace("",t)}return r}function Zu(r){return L(r.slice(1,-1))}function a0(r,e={},{tailwindConfig:t={}}={}){if(e.values?.[r]!==void 0)return Ot(e.values?.[r]);let[i,n]=Ku(r);if(n!==void 0){let a=e.values?.[i]??(it(i)?i.slice(1,-1):void 0);return a===void 0?void 0:(a=Ot(a),it(n)?Ie(a,Zu(n)):t.theme?.opacity?.[n]===void 0?void 0:Ie(a,t.theme.opacity[n]))}return ki(r,e,{validate:zu})}function o0(r,e={}){return e.values?.[r]}function me(r){return(e,t)=>ki(e,t,{validate:r})}function l0(r,e){let t=r.indexOf(e);return t===-1?[void 0,r]:[r.slice(0,t),r.slice(t+1)]}function gs(r,e,t,i){if(t.values&&e in t.values)for(let{type:a}of r??[]){let s=ms[a](e,t,{tailwindConfig:i});if(s!==void 0)return[s,a,null]}if(it(e)){let a=e.slice(1,-1),[s,o]=l0(a,":");if(!/^[\w-_]+$/g.test(s))o=a;else if(s!==void 0&&!ef.includes(s))return[];if(o.length>0&&ef.includes(s))return[ki(`[${o}]`,t),s,null]}let n=ys(r,e,t,i);for(let a of n)return a;return[]}function*ys(r,e,t,i){let n=K(i,"generalizedModifiers"),[a,s]=Ku(e);if(n&&t.modifiers!=null&&(t.modifiers==="any"||typeof t.modifiers=="object"&&(s&&it(s)||s in t.modifiers))||(a=e,s=void 0),s!==void 0&&a===""&&(a="DEFAULT"),s!==void 0&&typeof t.modifiers=="object"){let u=t.modifiers?.[s]??null;u!==null?s=u:it(s)&&(s=Zu(s))}for(let{type:u}of r??[]){let c=ms[u](a,t,{tailwindConfig:i});c!==void 0&&(yield[c,u,s??null])}}var ms,ef,hr=C(()=>{l();bi();fr();dr();di();Qu();je();ms={any:ki,color:a0,url:me(ds),image:me(ju),length:me(pr),percentage:me(cr),position:me(Vu),lookup:o0,"generic-name":me(Wu),"family-name":me(Uu),number:me(hs),"line-width":me(Nu),"absolute-size":me(Gu),"relative-size":me(Hu),shadow:me($u),size:me(Yu)},ef=Object.keys(ms)});function N(r){return typeof r=="function"?r({}):r}var ws=C(()=>{l()});function Et(r){return typeof r=="function"}function mr(r,...e){let t=e.pop();for(let i of e)for(let n in i){let a=t(r[n],i[n]);a===void 0?ie(r[n])&&ie(i[n])?r[n]=mr({},r[n],i[n],t):r[n]=i[n]:r[n]=a}return r}function u0(r,...e){return Et(r)?r(...e):r}function f0(r){return r.reduce((e,{extend:t})=>mr(e,t,(i,n)=>i===void 0?[n]:Array.isArray(i)?[n,...i]:[n,i]),{})}function c0(r){return{...r.reduce((e,t)=>os(e,t),{}),extend:f0(r)}}function tf(r,e){if(Array.isArray(r)&&ie(r[0]))return r.concat(e);if(Array.isArray(e)&&ie(e[0])&&ie(r))return[r,...e];if(Array.isArray(e))return e}function p0({extend:r,...e}){return mr(e,r,(t,i)=>!Et(t)&&!i.some(Et)?mr({},t,...i,tf):(n,a)=>mr({},...[t,...i].map(s=>u0(s,n,a)),tf))}function*d0(r){let e=Ze(r);if(e.length===0||(yield e,Array.isArray(r)))return;let t=/^(.*?)\s*\/\s*([^/]+)$/,i=r.match(t);if(i!==null){let[,n,a]=i,s=Ze(n);s.alpha=a,yield s}}function h0(r){let e=(t,i)=>{for(let n of d0(t)){let a=0,s=r;for(;s!=null&&a(t[i]=Et(r[i])?r[i](e,bs):r[i],t),{})}function rf(r){let e=[];return r.forEach(t=>{e=[...e,t];let i=t?.plugins??[];i.length!==0&&i.forEach(n=>{n.__isOptionsFunction&&(n=n()),e=[...e,...rf([n?.config??{}])]})}),e}function m0(r){return[...r].reduceRight((t,i)=>Et(i)?i({corePlugins:t}):Au(i,t),Su)}function g0(r){return[...r].reduceRight((t,i)=>[...t,...i],[])}function vs(r){let e=[...rf(r),{prefix:"",important:!1,separator:":"}];return qu(os({theme:h0(p0(c0(e.map(t=>t?.theme??{})))),corePlugins:m0(e.map(t=>t.corePlugins)),plugins:g0(r.map(t=>t?.plugins??[]))},...e))}var bs,nf=C(()=>{l();di();Cu();_u();mi();Tu();gi();Ru();At();wi();hr();fr();ws();bs={colors:ss,negative(r){return Object.keys(r).filter(e=>r[e]!=="0").reduce((e,t)=>{let i=Ke(r[t]);return i!==void 0&&(e[`-${t}`]=i),e},{})},breakpoints(r){return Object.keys(r).filter(e=>typeof r[e]=="string").reduce((e,t)=>({...e,[`screen-${t}`]:r[t]}),{})}}});var Si=v((i3,sf)=>{l();sf.exports={content:[],presets:[],darkMode:"media",theme:{accentColor:({theme:r})=>({...r("colors"),auto:"auto"}),animation:{none:"none",spin:"spin 1s linear infinite",ping:"ping 1s cubic-bezier(0, 0, 0.2, 1) infinite",pulse:"pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",bounce:"bounce 1s infinite"},aria:{busy:'busy="true"',checked:'checked="true"',disabled:'disabled="true"',expanded:'expanded="true"',hidden:'hidden="true"',pressed:'pressed="true"',readonly:'readonly="true"',required:'required="true"',selected:'selected="true"'},aspectRatio:{auto:"auto",square:"1 / 1",video:"16 / 9"},backdropBlur:({theme:r})=>r("blur"),backdropBrightness:({theme:r})=>r("brightness"),backdropContrast:({theme:r})=>r("contrast"),backdropGrayscale:({theme:r})=>r("grayscale"),backdropHueRotate:({theme:r})=>r("hueRotate"),backdropInvert:({theme:r})=>r("invert"),backdropOpacity:({theme:r})=>r("opacity"),backdropSaturate:({theme:r})=>r("saturate"),backdropSepia:({theme:r})=>r("sepia"),backgroundColor:({theme:r})=>r("colors"),backgroundImage:{none:"none","gradient-to-t":"linear-gradient(to top, var(--tw-gradient-stops))","gradient-to-tr":"linear-gradient(to top right, var(--tw-gradient-stops))","gradient-to-r":"linear-gradient(to right, var(--tw-gradient-stops))","gradient-to-br":"linear-gradient(to bottom right, var(--tw-gradient-stops))","gradient-to-b":"linear-gradient(to bottom, var(--tw-gradient-stops))","gradient-to-bl":"linear-gradient(to bottom left, var(--tw-gradient-stops))","gradient-to-l":"linear-gradient(to left, var(--tw-gradient-stops))","gradient-to-tl":"linear-gradient(to top left, var(--tw-gradient-stops))"},backgroundOpacity:({theme:r})=>r("opacity"),backgroundPosition:{bottom:"bottom",center:"center",left:"left","left-bottom":"left bottom","left-top":"left top",right:"right","right-bottom":"right bottom","right-top":"right top",top:"top"},backgroundSize:{auto:"auto",cover:"cover",contain:"contain"},blur:{0:"0",none:"0",sm:"4px",DEFAULT:"8px",md:"12px",lg:"16px",xl:"24px","2xl":"40px","3xl":"64px"},borderColor:({theme:r})=>({...r("colors"),DEFAULT:r("colors.gray.200","currentColor")}),borderOpacity:({theme:r})=>r("opacity"),borderRadius:{none:"0px",sm:"0.125rem",DEFAULT:"0.25rem",md:"0.375rem",lg:"0.5rem",xl:"0.75rem","2xl":"1rem","3xl":"1.5rem",full:"9999px"},borderSpacing:({theme:r})=>({...r("spacing")}),borderWidth:{DEFAULT:"1px",0:"0px",2:"2px",4:"4px",8:"8px"},boxShadow:{sm:"0 1px 2px 0 rgb(0 0 0 / 0.05)",DEFAULT:"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",md:"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",lg:"0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",xl:"0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)","2xl":"0 25px 50px -12px rgb(0 0 0 / 0.25)",inner:"inset 0 2px 4px 0 rgb(0 0 0 / 0.05)",none:"none"},boxShadowColor:({theme:r})=>r("colors"),brightness:{0:"0",50:".5",75:".75",90:".9",95:".95",100:"1",105:"1.05",110:"1.1",125:"1.25",150:"1.5",200:"2"},caretColor:({theme:r})=>r("colors"),colors:({colors:r})=>({inherit:r.inherit,current:r.current,transparent:r.transparent,black:r.black,white:r.white,slate:r.slate,gray:r.gray,zinc:r.zinc,neutral:r.neutral,stone:r.stone,red:r.red,orange:r.orange,amber:r.amber,yellow:r.yellow,lime:r.lime,green:r.green,emerald:r.emerald,teal:r.teal,cyan:r.cyan,sky:r.sky,blue:r.blue,indigo:r.indigo,violet:r.violet,purple:r.purple,fuchsia:r.fuchsia,pink:r.pink,rose:r.rose}),columns:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12","3xs":"16rem","2xs":"18rem",xs:"20rem",sm:"24rem",md:"28rem",lg:"32rem",xl:"36rem","2xl":"42rem","3xl":"48rem","4xl":"56rem","5xl":"64rem","6xl":"72rem","7xl":"80rem"},container:{},content:{none:"none"},contrast:{0:"0",50:".5",75:".75",100:"1",125:"1.25",150:"1.5",200:"2"},cursor:{auto:"auto",default:"default",pointer:"pointer",wait:"wait",text:"text",move:"move",help:"help","not-allowed":"not-allowed",none:"none","context-menu":"context-menu",progress:"progress",cell:"cell",crosshair:"crosshair","vertical-text":"vertical-text",alias:"alias",copy:"copy","no-drop":"no-drop",grab:"grab",grabbing:"grabbing","all-scroll":"all-scroll","col-resize":"col-resize","row-resize":"row-resize","n-resize":"n-resize","e-resize":"e-resize","s-resize":"s-resize","w-resize":"w-resize","ne-resize":"ne-resize","nw-resize":"nw-resize","se-resize":"se-resize","sw-resize":"sw-resize","ew-resize":"ew-resize","ns-resize":"ns-resize","nesw-resize":"nesw-resize","nwse-resize":"nwse-resize","zoom-in":"zoom-in","zoom-out":"zoom-out"},divideColor:({theme:r})=>r("borderColor"),divideOpacity:({theme:r})=>r("borderOpacity"),divideWidth:({theme:r})=>r("borderWidth"),dropShadow:{sm:"0 1px 1px rgb(0 0 0 / 0.05)",DEFAULT:["0 1px 2px rgb(0 0 0 / 0.1)","0 1px 1px rgb(0 0 0 / 0.06)"],md:["0 4px 3px rgb(0 0 0 / 0.07)","0 2px 2px rgb(0 0 0 / 0.06)"],lg:["0 10px 8px rgb(0 0 0 / 0.04)","0 4px 3px rgb(0 0 0 / 0.1)"],xl:["0 20px 13px rgb(0 0 0 / 0.03)","0 8px 5px rgb(0 0 0 / 0.08)"],"2xl":"0 25px 25px rgb(0 0 0 / 0.15)",none:"0 0 #0000"},fill:({theme:r})=>({none:"none",...r("colors")}),flex:{1:"1 1 0%",auto:"1 1 auto",initial:"0 1 auto",none:"none"},flexBasis:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%","1/12":"8.333333%","2/12":"16.666667%","3/12":"25%","4/12":"33.333333%","5/12":"41.666667%","6/12":"50%","7/12":"58.333333%","8/12":"66.666667%","9/12":"75%","10/12":"83.333333%","11/12":"91.666667%",full:"100%"}),flexGrow:{0:"0",DEFAULT:"1"},flexShrink:{0:"0",DEFAULT:"1"},fontFamily:{sans:["ui-sans-serif","system-ui","sans-serif",'"Apple Color Emoji"','"Segoe UI Emoji"','"Segoe UI Symbol"','"Noto Color Emoji"'],serif:["ui-serif","Georgia","Cambria",'"Times New Roman"',"Times","serif"],mono:["ui-monospace","SFMono-Regular","Menlo","Monaco","Consolas",'"Liberation Mono"','"Courier New"',"monospace"]},fontSize:{xs:["0.75rem",{lineHeight:"1rem"}],sm:["0.875rem",{lineHeight:"1.25rem"}],base:["1rem",{lineHeight:"1.5rem"}],lg:["1.125rem",{lineHeight:"1.75rem"}],xl:["1.25rem",{lineHeight:"1.75rem"}],"2xl":["1.5rem",{lineHeight:"2rem"}],"3xl":["1.875rem",{lineHeight:"2.25rem"}],"4xl":["2.25rem",{lineHeight:"2.5rem"}],"5xl":["3rem",{lineHeight:"1"}],"6xl":["3.75rem",{lineHeight:"1"}],"7xl":["4.5rem",{lineHeight:"1"}],"8xl":["6rem",{lineHeight:"1"}],"9xl":["8rem",{lineHeight:"1"}]},fontWeight:{thin:"100",extralight:"200",light:"300",normal:"400",medium:"500",semibold:"600",bold:"700",extrabold:"800",black:"900"},gap:({theme:r})=>r("spacing"),gradientColorStops:({theme:r})=>r("colors"),gradientColorStopPositions:{"0%":"0%","5%":"5%","10%":"10%","15%":"15%","20%":"20%","25%":"25%","30%":"30%","35%":"35%","40%":"40%","45%":"45%","50%":"50%","55%":"55%","60%":"60%","65%":"65%","70%":"70%","75%":"75%","80%":"80%","85%":"85%","90%":"90%","95%":"95%","100%":"100%"},grayscale:{0:"0",DEFAULT:"100%"},gridAutoColumns:{auto:"auto",min:"min-content",max:"max-content",fr:"minmax(0, 1fr)"},gridAutoRows:{auto:"auto",min:"min-content",max:"max-content",fr:"minmax(0, 1fr)"},gridColumn:{auto:"auto","span-1":"span 1 / span 1","span-2":"span 2 / span 2","span-3":"span 3 / span 3","span-4":"span 4 / span 4","span-5":"span 5 / span 5","span-6":"span 6 / span 6","span-7":"span 7 / span 7","span-8":"span 8 / span 8","span-9":"span 9 / span 9","span-10":"span 10 / span 10","span-11":"span 11 / span 11","span-12":"span 12 / span 12","span-full":"1 / -1"},gridColumnEnd:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridColumnStart:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridRow:{auto:"auto","span-1":"span 1 / span 1","span-2":"span 2 / span 2","span-3":"span 3 / span 3","span-4":"span 4 / span 4","span-5":"span 5 / span 5","span-6":"span 6 / span 6","span-7":"span 7 / span 7","span-8":"span 8 / span 8","span-9":"span 9 / span 9","span-10":"span 10 / span 10","span-11":"span 11 / span 11","span-12":"span 12 / span 12","span-full":"1 / -1"},gridRowEnd:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridRowStart:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridTemplateColumns:{none:"none",subgrid:"subgrid",1:"repeat(1, minmax(0, 1fr))",2:"repeat(2, minmax(0, 1fr))",3:"repeat(3, minmax(0, 1fr))",4:"repeat(4, minmax(0, 1fr))",5:"repeat(5, minmax(0, 1fr))",6:"repeat(6, minmax(0, 1fr))",7:"repeat(7, minmax(0, 1fr))",8:"repeat(8, minmax(0, 1fr))",9:"repeat(9, minmax(0, 1fr))",10:"repeat(10, minmax(0, 1fr))",11:"repeat(11, minmax(0, 1fr))",12:"repeat(12, minmax(0, 1fr))"},gridTemplateRows:{none:"none",subgrid:"subgrid",1:"repeat(1, minmax(0, 1fr))",2:"repeat(2, minmax(0, 1fr))",3:"repeat(3, minmax(0, 1fr))",4:"repeat(4, minmax(0, 1fr))",5:"repeat(5, minmax(0, 1fr))",6:"repeat(6, minmax(0, 1fr))",7:"repeat(7, minmax(0, 1fr))",8:"repeat(8, minmax(0, 1fr))",9:"repeat(9, minmax(0, 1fr))",10:"repeat(10, minmax(0, 1fr))",11:"repeat(11, minmax(0, 1fr))",12:"repeat(12, minmax(0, 1fr))"},height:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%",full:"100%",screen:"100vh",svh:"100svh",lvh:"100lvh",dvh:"100dvh",min:"min-content",max:"max-content",fit:"fit-content"}),hueRotate:{0:"0deg",15:"15deg",30:"30deg",60:"60deg",90:"90deg",180:"180deg"},inset:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%",full:"100%"}),invert:{0:"0",DEFAULT:"100%"},keyframes:{spin:{to:{transform:"rotate(360deg)"}},ping:{"75%, 100%":{transform:"scale(2)",opacity:"0"}},pulse:{"50%":{opacity:".5"}},bounce:{"0%, 100%":{transform:"translateY(-25%)",animationTimingFunction:"cubic-bezier(0.8,0,1,1)"},"50%":{transform:"none",animationTimingFunction:"cubic-bezier(0,0,0.2,1)"}}},letterSpacing:{tighter:"-0.05em",tight:"-0.025em",normal:"0em",wide:"0.025em",wider:"0.05em",widest:"0.1em"},lineHeight:{none:"1",tight:"1.25",snug:"1.375",normal:"1.5",relaxed:"1.625",loose:"2",3:".75rem",4:"1rem",5:"1.25rem",6:"1.5rem",7:"1.75rem",8:"2rem",9:"2.25rem",10:"2.5rem"},listStyleType:{none:"none",disc:"disc",decimal:"decimal"},listStyleImage:{none:"none"},margin:({theme:r})=>({auto:"auto",...r("spacing")}),lineClamp:{1:"1",2:"2",3:"3",4:"4",5:"5",6:"6"},maxHeight:({theme:r})=>({...r("spacing"),none:"none",full:"100%",screen:"100vh",svh:"100svh",lvh:"100lvh",dvh:"100dvh",min:"min-content",max:"max-content",fit:"fit-content"}),maxWidth:({theme:r,breakpoints:e})=>({...r("spacing"),none:"none",xs:"20rem",sm:"24rem",md:"28rem",lg:"32rem",xl:"36rem","2xl":"42rem","3xl":"48rem","4xl":"56rem","5xl":"64rem","6xl":"72rem","7xl":"80rem",full:"100%",min:"min-content",max:"max-content",fit:"fit-content",prose:"65ch",...e(r("screens"))}),minHeight:({theme:r})=>({...r("spacing"),full:"100%",screen:"100vh",svh:"100svh",lvh:"100lvh",dvh:"100dvh",min:"min-content",max:"max-content",fit:"fit-content"}),minWidth:({theme:r})=>({...r("spacing"),full:"100%",min:"min-content",max:"max-content",fit:"fit-content"}),objectPosition:{bottom:"bottom",center:"center",left:"left","left-bottom":"left bottom","left-top":"left top",right:"right","right-bottom":"right bottom","right-top":"right top",top:"top"},opacity:{0:"0",5:"0.05",10:"0.1",15:"0.15",20:"0.2",25:"0.25",30:"0.3",35:"0.35",40:"0.4",45:"0.45",50:"0.5",55:"0.55",60:"0.6",65:"0.65",70:"0.7",75:"0.75",80:"0.8",85:"0.85",90:"0.9",95:"0.95",100:"1"},order:{first:"-9999",last:"9999",none:"0",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12"},outlineColor:({theme:r})=>r("colors"),outlineOffset:{0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},outlineWidth:{0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},padding:({theme:r})=>r("spacing"),placeholderColor:({theme:r})=>r("colors"),placeholderOpacity:({theme:r})=>r("opacity"),ringColor:({theme:r})=>({DEFAULT:r("colors.blue.500","#3b82f6"),...r("colors")}),ringOffsetColor:({theme:r})=>r("colors"),ringOffsetWidth:{0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},ringOpacity:({theme:r})=>({DEFAULT:"0.5",...r("opacity")}),ringWidth:{DEFAULT:"3px",0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},rotate:{0:"0deg",1:"1deg",2:"2deg",3:"3deg",6:"6deg",12:"12deg",45:"45deg",90:"90deg",180:"180deg"},saturate:{0:"0",50:".5",100:"1",150:"1.5",200:"2"},scale:{0:"0",50:".5",75:".75",90:".9",95:".95",100:"1",105:"1.05",110:"1.1",125:"1.25",150:"1.5"},screens:{sm:"640px",md:"768px",lg:"1024px",xl:"1280px","2xl":"1536px"},scrollMargin:({theme:r})=>({...r("spacing")}),scrollPadding:({theme:r})=>r("spacing"),sepia:{0:"0",DEFAULT:"100%"},skew:{0:"0deg",1:"1deg",2:"2deg",3:"3deg",6:"6deg",12:"12deg"},space:({theme:r})=>({...r("spacing")}),spacing:{px:"1px",0:"0px",.5:"0.125rem",1:"0.25rem",1.5:"0.375rem",2:"0.5rem",2.5:"0.625rem",3:"0.75rem",3.5:"0.875rem",4:"1rem",5:"1.25rem",6:"1.5rem",7:"1.75rem",8:"2rem",9:"2.25rem",10:"2.5rem",11:"2.75rem",12:"3rem",14:"3.5rem",16:"4rem",20:"5rem",24:"6rem",28:"7rem",32:"8rem",36:"9rem",40:"10rem",44:"11rem",48:"12rem",52:"13rem",56:"14rem",60:"15rem",64:"16rem",72:"18rem",80:"20rem",96:"24rem"},stroke:({theme:r})=>({none:"none",...r("colors")}),strokeWidth:{0:"0",1:"1",2:"2"},supports:{},data:{},textColor:({theme:r})=>r("colors"),textDecorationColor:({theme:r})=>r("colors"),textDecorationThickness:{auto:"auto","from-font":"from-font",0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},textIndent:({theme:r})=>({...r("spacing")}),textOpacity:({theme:r})=>r("opacity"),textUnderlineOffset:{auto:"auto",0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},transformOrigin:{center:"center",top:"top","top-right":"top right",right:"right","bottom-right":"bottom right",bottom:"bottom","bottom-left":"bottom left",left:"left","top-left":"top left"},transitionDelay:{0:"0s",75:"75ms",100:"100ms",150:"150ms",200:"200ms",300:"300ms",500:"500ms",700:"700ms",1e3:"1000ms"},transitionDuration:{DEFAULT:"150ms",0:"0s",75:"75ms",100:"100ms",150:"150ms",200:"200ms",300:"300ms",500:"500ms",700:"700ms",1e3:"1000ms"},transitionProperty:{none:"none",all:"all",DEFAULT:"color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter",colors:"color, background-color, border-color, text-decoration-color, fill, stroke",opacity:"opacity",shadow:"box-shadow",transform:"transform"},transitionTimingFunction:{DEFAULT:"cubic-bezier(0.4, 0, 0.2, 1)",linear:"linear",in:"cubic-bezier(0.4, 0, 1, 1)",out:"cubic-bezier(0, 0, 0.2, 1)","in-out":"cubic-bezier(0.4, 0, 0.2, 1)"},translate:({theme:r})=>({...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%",full:"100%"}),size:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%","1/12":"8.333333%","2/12":"16.666667%","3/12":"25%","4/12":"33.333333%","5/12":"41.666667%","6/12":"50%","7/12":"58.333333%","8/12":"66.666667%","9/12":"75%","10/12":"83.333333%","11/12":"91.666667%",full:"100%",min:"min-content",max:"max-content",fit:"fit-content"}),width:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%","1/12":"8.333333%","2/12":"16.666667%","3/12":"25%","4/12":"33.333333%","5/12":"41.666667%","6/12":"50%","7/12":"58.333333%","8/12":"66.666667%","9/12":"75%","10/12":"83.333333%","11/12":"91.666667%",full:"100%",screen:"100vw",svw:"100svw",lvw:"100lvw",dvw:"100dvw",min:"min-content",max:"max-content",fit:"fit-content"}),willChange:{auto:"auto",scroll:"scroll-position",contents:"contents",transform:"transform"},zIndex:{auto:"auto",0:"0",10:"10",20:"20",30:"30",40:"40",50:"50"}},plugins:[]}});function Ci(r){let e=(r?.presets??[af.default]).slice().reverse().flatMap(n=>Ci(n instanceof Function?n():n)),t={respectDefaultRingColorOpacity:{theme:{ringColor:({theme:n})=>({DEFAULT:"#3b82f67f",...n("colors")})}},disableColorOpacityUtilitiesByDefault:{corePlugins:{backgroundOpacity:!1,borderOpacity:!1,divideOpacity:!1,placeholderOpacity:!1,ringOpacity:!1,textOpacity:!1}}},i=Object.keys(t).filter(n=>K(r,n)).map(n=>t[n]);return[r,...i,...e]}var af,of=C(()=>{l();af=X(Si());je()});var lf={};Ae(lf,{default:()=>gr});function gr(...r){let[,...e]=Ci(r[0]);return vs([...r,...e])}var xs=C(()=>{l();nf();of()});var uf={};Ae(uf,{default:()=>Z});var Z,bt=C(()=>{l();Z={resolve:r=>r,extname:r=>"."+r.split(".").pop()}});function Ai(r){return typeof r=="object"&&r!==null}function w0(r){return Object.keys(r).length===0}function ff(r){return typeof r=="string"||r instanceof String}function ks(r){return Ai(r)&&r.config===void 0&&!w0(r)?null:Ai(r)&&r.config!==void 0&&ff(r.config)?Z.resolve(r.config):Ai(r)&&r.config!==void 0&&Ai(r.config)?null:ff(r)?Z.resolve(r):b0()}function b0(){for(let r of y0)try{let e=Z.resolve(r);return te.accessSync(e),e}catch(e){}return null}var y0,cf=C(()=>{l();ze();bt();y0=["./tailwind.config.js","./tailwind.config.cjs","./tailwind.config.mjs","./tailwind.config.ts"]});var pf={};Ae(pf,{default:()=>Ss});var Ss,Cs=C(()=>{l();Ss={parse:r=>({href:r})}});var As=v(()=>{l()});var _i=v((d3,mf)=>{l();"use strict";var df=(hi(),Ou),hf=As(),Tt=class extends Error{constructor(e,t,i,n,a,s){super(e);this.name="CssSyntaxError",this.reason=e,a&&(this.file=a),n&&(this.source=n),s&&(this.plugin=s),typeof t!="undefined"&&typeof i!="undefined"&&(typeof t=="number"?(this.line=t,this.column=i):(this.line=t.line,this.column=t.column,this.endLine=i.line,this.endColumn=i.column)),this.setMessage(),Error.captureStackTrace&&Error.captureStackTrace(this,Tt)}setMessage(){this.message=this.plugin?this.plugin+": ":"",this.message+=this.file?this.file:"",typeof this.line!="undefined"&&(this.message+=":"+this.line+":"+this.column),this.message+=": "+this.reason}showSourceCode(e){if(!this.source)return"";let t=this.source;e==null&&(e=df.isColorSupported),hf&&e&&(t=hf(t));let i=t.split(/\r?\n/),n=Math.max(this.line-3,0),a=Math.min(this.line+2,i.length),s=String(a).length,o,u;if(e){let{bold:c,red:f,gray:d}=df.createColors(!0);o=p=>c(f(p)),u=p=>d(p)}else o=u=c=>c;return i.slice(n,a).map((c,f)=>{let d=n+1+f,p=" "+(" "+d).slice(-s)+" | ";if(d===this.line){let m=u(p.replace(/\d/g," "))+c.slice(0,this.column-1).replace(/[^\t]/g," ");return o(">")+u(p)+c+` + `+m+o("^")}return" "+u(p)+c}).join(` +`)}toString(){let e=this.showSourceCode();return e&&(e=` + +`+e+` +`),this.name+": "+this.message+e}};mf.exports=Tt;Tt.default=Tt});var Oi=v((h3,_s)=>{l();"use strict";_s.exports.isClean=Symbol("isClean");_s.exports.my=Symbol("my")});var Os=v((m3,yf)=>{l();"use strict";var gf={colon:": ",indent:" ",beforeDecl:` +`,beforeRule:` +`,beforeOpen:" ",beforeClose:` +`,beforeComment:` +`,after:` +`,emptyBody:"",commentLeft:" ",commentRight:" ",semicolon:!1};function v0(r){return r[0].toUpperCase()+r.slice(1)}var Ei=class{constructor(e){this.builder=e}stringify(e,t){if(!this[e.type])throw new Error("Unknown AST node type "+e.type+". Maybe you need to change PostCSS stringifier.");this[e.type](e,t)}document(e){this.body(e)}root(e){this.body(e),e.raws.after&&this.builder(e.raws.after)}comment(e){let t=this.raw(e,"left","commentLeft"),i=this.raw(e,"right","commentRight");this.builder("/*"+t+e.text+i+"*/",e)}decl(e,t){let i=this.raw(e,"between","colon"),n=e.prop+i+this.rawValue(e,"value");e.important&&(n+=e.raws.important||" !important"),t&&(n+=";"),this.builder(n,e)}rule(e){this.block(e,this.rawValue(e,"selector")),e.raws.ownSemicolon&&this.builder(e.raws.ownSemicolon,e,"end")}atrule(e,t){let i="@"+e.name,n=e.params?this.rawValue(e,"params"):"";if(typeof e.raws.afterName!="undefined"?i+=e.raws.afterName:n&&(i+=" "),e.nodes)this.block(e,i+n);else{let a=(e.raws.between||"")+(t?";":"");this.builder(i+n+a,e)}}body(e){let t=e.nodes.length-1;for(;t>0&&e.nodes[t].type==="comment";)t-=1;let i=this.raw(e,"semicolon");for(let n=0;n{if(n=u.raws[t],typeof n!="undefined")return!1})}return typeof n=="undefined"&&(n=gf[i]),s.rawCache[i]=n,n}rawSemicolon(e){let t;return e.walk(i=>{if(i.nodes&&i.nodes.length&&i.last.type==="decl"&&(t=i.raws.semicolon,typeof t!="undefined"))return!1}),t}rawEmptyBody(e){let t;return e.walk(i=>{if(i.nodes&&i.nodes.length===0&&(t=i.raws.after,typeof t!="undefined"))return!1}),t}rawIndent(e){if(e.raws.indent)return e.raws.indent;let t;return e.walk(i=>{let n=i.parent;if(n&&n!==e&&n.parent&&n.parent===e&&typeof i.raws.before!="undefined"){let a=i.raws.before.split(` +`);return t=a[a.length-1],t=t.replace(/\S/g,""),!1}}),t}rawBeforeComment(e,t){let i;return e.walkComments(n=>{if(typeof n.raws.before!="undefined")return i=n.raws.before,i.includes(` +`)&&(i=i.replace(/[^\n]+$/,"")),!1}),typeof i=="undefined"?i=this.raw(t,null,"beforeDecl"):i&&(i=i.replace(/\S/g,"")),i}rawBeforeDecl(e,t){let i;return e.walkDecls(n=>{if(typeof n.raws.before!="undefined")return i=n.raws.before,i.includes(` +`)&&(i=i.replace(/[^\n]+$/,"")),!1}),typeof i=="undefined"?i=this.raw(t,null,"beforeRule"):i&&(i=i.replace(/\S/g,"")),i}rawBeforeRule(e){let t;return e.walk(i=>{if(i.nodes&&(i.parent!==e||e.first!==i)&&typeof i.raws.before!="undefined")return t=i.raws.before,t.includes(` +`)&&(t=t.replace(/[^\n]+$/,"")),!1}),t&&(t=t.replace(/\S/g,"")),t}rawBeforeClose(e){let t;return e.walk(i=>{if(i.nodes&&i.nodes.length>0&&typeof i.raws.after!="undefined")return t=i.raws.after,t.includes(` +`)&&(t=t.replace(/[^\n]+$/,"")),!1}),t&&(t=t.replace(/\S/g,"")),t}rawBeforeOpen(e){let t;return e.walk(i=>{if(i.type!=="decl"&&(t=i.raws.between,typeof t!="undefined"))return!1}),t}rawColon(e){let t;return e.walkDecls(i=>{if(typeof i.raws.between!="undefined")return t=i.raws.between.replace(/[^\s:]/g,""),!1}),t}beforeAfter(e,t){let i;e.type==="decl"?i=this.raw(e,null,"beforeDecl"):e.type==="comment"?i=this.raw(e,null,"beforeComment"):t==="before"?i=this.raw(e,null,"beforeRule"):i=this.raw(e,null,"beforeClose");let n=e.parent,a=0;for(;n&&n.type!=="root";)a+=1,n=n.parent;if(i.includes(` +`)){let s=this.raw(e,null,"indent");if(s.length)for(let o=0;o{l();"use strict";var x0=Os();function Es(r,e){new x0(e).stringify(r)}wf.exports=Es;Es.default=Es});var wr=v((y3,bf)=>{l();"use strict";var{isClean:Ti,my:k0}=Oi(),S0=_i(),C0=Os(),A0=yr();function Ts(r,e){let t=new r.constructor;for(let i in r){if(!Object.prototype.hasOwnProperty.call(r,i)||i==="proxyCache")continue;let n=r[i],a=typeof n;i==="parent"&&a==="object"?e&&(t[i]=e):i==="source"?t[i]=n:Array.isArray(n)?t[i]=n.map(s=>Ts(s,t)):(a==="object"&&n!==null&&(n=Ts(n)),t[i]=n)}return t}var Pi=class{constructor(e={}){this.raws={},this[Ti]=!1,this[k0]=!0;for(let t in e)if(t==="nodes"){this.nodes=[];for(let i of e[t])typeof i.clone=="function"?this.append(i.clone()):this.append(i)}else this[t]=e[t]}error(e,t={}){if(this.source){let{start:i,end:n}=this.rangeBy(t);return this.source.input.error(e,{line:i.line,column:i.column},{line:n.line,column:n.column},t)}return new S0(e)}warn(e,t,i){let n={node:this};for(let a in i)n[a]=i[a];return e.warn(t,n)}remove(){return this.parent&&this.parent.removeChild(this),this.parent=void 0,this}toString(e=A0){e.stringify&&(e=e.stringify);let t="";return e(this,i=>{t+=i}),t}assign(e={}){for(let t in e)this[t]=e[t];return this}clone(e={}){let t=Ts(this);for(let i in e)t[i]=e[i];return t}cloneBefore(e={}){let t=this.clone(e);return this.parent.insertBefore(this,t),t}cloneAfter(e={}){let t=this.clone(e);return this.parent.insertAfter(this,t),t}replaceWith(...e){if(this.parent){let t=this,i=!1;for(let n of e)n===this?i=!0:i?(this.parent.insertAfter(t,n),t=n):this.parent.insertBefore(t,n);i||this.remove()}return this}next(){if(!this.parent)return;let e=this.parent.index(this);return this.parent.nodes[e+1]}prev(){if(!this.parent)return;let e=this.parent.index(this);return this.parent.nodes[e-1]}before(e){return this.parent.insertBefore(this,e),this}after(e){return this.parent.insertAfter(this,e),this}root(){let e=this;for(;e.parent&&e.parent.type!=="document";)e=e.parent;return e}raw(e,t){return new C0().raw(this,e,t)}cleanRaws(e){delete this.raws.before,delete this.raws.after,e||delete this.raws.between}toJSON(e,t){let i={},n=t==null;t=t||new Map;let a=0;for(let s in this){if(!Object.prototype.hasOwnProperty.call(this,s)||s==="parent"||s==="proxyCache")continue;let o=this[s];if(Array.isArray(o))i[s]=o.map(u=>typeof u=="object"&&u.toJSON?u.toJSON(null,t):u);else if(typeof o=="object"&&o.toJSON)i[s]=o.toJSON(null,t);else if(s==="source"){let u=t.get(o.input);u==null&&(u=a,t.set(o.input,a),a++),i[s]={inputId:u,start:o.start,end:o.end}}else i[s]=o}return n&&(i.inputs=[...t.keys()].map(s=>s.toJSON())),i}positionInside(e){let t=this.toString(),i=this.source.start.column,n=this.source.start.line;for(let a=0;ae.root().toProxy():e[t]}}}toProxy(){return this.proxyCache||(this.proxyCache=new Proxy(this,this.getProxyProcessor())),this.proxyCache}addToError(e){if(e.postcssNode=this,e.stack&&this.source&&/\n\s{4}at /.test(e.stack)){let t=this.source;e.stack=e.stack.replace(/\n\s{4}at /,`$&${t.input.from}:${t.start.line}:${t.start.column}$&`)}return e}markDirty(){if(this[Ti]){this[Ti]=!1;let e=this;for(;e=e.parent;)e[Ti]=!1}}get proxyOf(){return this}};bf.exports=Pi;Pi.default=Pi});var br=v((w3,vf)=>{l();"use strict";var _0=wr(),Di=class extends _0{constructor(e){e&&typeof e.value!="undefined"&&typeof e.value!="string"&&(e={...e,value:String(e.value)});super(e);this.type="decl"}get variable(){return this.prop.startsWith("--")||this.prop[0]==="$"}};vf.exports=Di;Di.default=Di});var Ps=v((b3,xf)=>{l();xf.exports=function(r,e){return{generate:()=>{let t="";return r(e,i=>{t+=i}),[t]}}}});var vr=v((v3,kf)=>{l();"use strict";var O0=wr(),Ii=class extends O0{constructor(e){super(e);this.type="comment"}};kf.exports=Ii;Ii.default=Ii});var nt=v((x3,Df)=>{l();"use strict";var{isClean:Sf,my:Cf}=Oi(),Af=br(),_f=vr(),E0=wr(),Of,Ds,Is,Ef;function Tf(r){return r.map(e=>(e.nodes&&(e.nodes=Tf(e.nodes)),delete e.source,e))}function Pf(r){if(r[Sf]=!1,r.proxyOf.nodes)for(let e of r.proxyOf.nodes)Pf(e)}var we=class extends E0{push(e){return e.parent=this,this.proxyOf.nodes.push(e),this}each(e){if(!this.proxyOf.nodes)return;let t=this.getIterator(),i,n;for(;this.indexes[t]{let n;try{n=e(t,i)}catch(a){throw t.addToError(a)}return n!==!1&&t.walk&&(n=t.walk(e)),n})}walkDecls(e,t){return t?e instanceof RegExp?this.walk((i,n)=>{if(i.type==="decl"&&e.test(i.prop))return t(i,n)}):this.walk((i,n)=>{if(i.type==="decl"&&i.prop===e)return t(i,n)}):(t=e,this.walk((i,n)=>{if(i.type==="decl")return t(i,n)}))}walkRules(e,t){return t?e instanceof RegExp?this.walk((i,n)=>{if(i.type==="rule"&&e.test(i.selector))return t(i,n)}):this.walk((i,n)=>{if(i.type==="rule"&&i.selector===e)return t(i,n)}):(t=e,this.walk((i,n)=>{if(i.type==="rule")return t(i,n)}))}walkAtRules(e,t){return t?e instanceof RegExp?this.walk((i,n)=>{if(i.type==="atrule"&&e.test(i.name))return t(i,n)}):this.walk((i,n)=>{if(i.type==="atrule"&&i.name===e)return t(i,n)}):(t=e,this.walk((i,n)=>{if(i.type==="atrule")return t(i,n)}))}walkComments(e){return this.walk((t,i)=>{if(t.type==="comment")return e(t,i)})}append(...e){for(let t of e){let i=this.normalize(t,this.last);for(let n of i)this.proxyOf.nodes.push(n)}return this.markDirty(),this}prepend(...e){e=e.reverse();for(let t of e){let i=this.normalize(t,this.first,"prepend").reverse();for(let n of i)this.proxyOf.nodes.unshift(n);for(let n in this.indexes)this.indexes[n]=this.indexes[n]+i.length}return this.markDirty(),this}cleanRaws(e){if(super.cleanRaws(e),this.nodes)for(let t of this.nodes)t.cleanRaws(e)}insertBefore(e,t){let i=this.index(e),n=i===0?"prepend":!1,a=this.normalize(t,this.proxyOf.nodes[i],n).reverse();i=this.index(e);for(let o of a)this.proxyOf.nodes.splice(i,0,o);let s;for(let o in this.indexes)s=this.indexes[o],i<=s&&(this.indexes[o]=s+a.length);return this.markDirty(),this}insertAfter(e,t){let i=this.index(e),n=this.normalize(t,this.proxyOf.nodes[i]).reverse();i=this.index(e);for(let s of n)this.proxyOf.nodes.splice(i+1,0,s);let a;for(let s in this.indexes)a=this.indexes[s],i=e&&(this.indexes[i]=t-1);return this.markDirty(),this}removeAll(){for(let e of this.proxyOf.nodes)e.parent=void 0;return this.proxyOf.nodes=[],this.markDirty(),this}replaceValues(e,t,i){return i||(i=t,t={}),this.walkDecls(n=>{t.props&&!t.props.includes(n.prop)||t.fast&&!n.value.includes(t.fast)||(n.value=n.value.replace(e,i))}),this.markDirty(),this}every(e){return this.nodes.every(e)}some(e){return this.nodes.some(e)}index(e){return typeof e=="number"?e:(e.proxyOf&&(e=e.proxyOf),this.proxyOf.nodes.indexOf(e))}get first(){if(!!this.proxyOf.nodes)return this.proxyOf.nodes[0]}get last(){if(!!this.proxyOf.nodes)return this.proxyOf.nodes[this.proxyOf.nodes.length-1]}normalize(e,t){if(typeof e=="string")e=Tf(Of(e).nodes);else if(Array.isArray(e)){e=e.slice(0);for(let n of e)n.parent&&n.parent.removeChild(n,"ignore")}else if(e.type==="root"&&this.type!=="document"){e=e.nodes.slice(0);for(let n of e)n.parent&&n.parent.removeChild(n,"ignore")}else if(e.type)e=[e];else if(e.prop){if(typeof e.value=="undefined")throw new Error("Value field is missed in node creation");typeof e.value!="string"&&(e.value=String(e.value)),e=[new Af(e)]}else if(e.selector)e=[new Ds(e)];else if(e.name)e=[new Is(e)];else if(e.text)e=[new _f(e)];else throw new Error("Unknown node type in node creation");return e.map(n=>(n[Cf]||we.rebuild(n),n=n.proxyOf,n.parent&&n.parent.removeChild(n),n[Sf]&&Pf(n),typeof n.raws.before=="undefined"&&t&&typeof t.raws.before!="undefined"&&(n.raws.before=t.raws.before.replace(/\S/g,"")),n.parent=this.proxyOf,n))}getProxyProcessor(){return{set(e,t,i){return e[t]===i||(e[t]=i,(t==="name"||t==="params"||t==="selector")&&e.markDirty()),!0},get(e,t){return t==="proxyOf"?e:e[t]?t==="each"||typeof t=="string"&&t.startsWith("walk")?(...i)=>e[t](...i.map(n=>typeof n=="function"?(a,s)=>n(a.toProxy(),s):n)):t==="every"||t==="some"?i=>e[t]((n,...a)=>i(n.toProxy(),...a)):t==="root"?()=>e.root().toProxy():t==="nodes"?e.nodes.map(i=>i.toProxy()):t==="first"||t==="last"?e[t].toProxy():e[t]:e[t]}}}getIterator(){this.lastEach||(this.lastEach=0),this.indexes||(this.indexes={}),this.lastEach+=1;let e=this.lastEach;return this.indexes[e]=0,e}};we.registerParse=r=>{Of=r};we.registerRule=r=>{Ds=r};we.registerAtRule=r=>{Is=r};we.registerRoot=r=>{Ef=r};Df.exports=we;we.default=we;we.rebuild=r=>{r.type==="atrule"?Object.setPrototypeOf(r,Is.prototype):r.type==="rule"?Object.setPrototypeOf(r,Ds.prototype):r.type==="decl"?Object.setPrototypeOf(r,Af.prototype):r.type==="comment"?Object.setPrototypeOf(r,_f.prototype):r.type==="root"&&Object.setPrototypeOf(r,Ef.prototype),r[Cf]=!0,r.nodes&&r.nodes.forEach(e=>{we.rebuild(e)})}});var qi=v((k3,Rf)=>{l();"use strict";var T0=nt(),If,qf,Pt=class extends T0{constructor(e){super({type:"document",...e});this.nodes||(this.nodes=[])}toResult(e={}){return new If(new qf,this,e).stringify()}};Pt.registerLazyResult=r=>{If=r};Pt.registerProcessor=r=>{qf=r};Rf.exports=Pt;Pt.default=Pt});var qs=v((S3,Bf)=>{l();"use strict";var Mf={};Bf.exports=function(e){Mf[e]||(Mf[e]=!0,typeof console!="undefined"&&console.warn&&console.warn(e))}});var Rs=v((C3,Ff)=>{l();"use strict";var Ri=class{constructor(e,t={}){if(this.type="warning",this.text=e,t.node&&t.node.source){let i=t.node.rangeBy(t);this.line=i.start.line,this.column=i.start.column,this.endLine=i.end.line,this.endColumn=i.end.column}for(let i in t)this[i]=t[i]}toString(){return this.node?this.node.error(this.text,{plugin:this.plugin,index:this.index,word:this.word}).message:this.plugin?this.plugin+": "+this.text:this.text}};Ff.exports=Ri;Ri.default=Ri});var Bi=v((A3,Lf)=>{l();"use strict";var P0=Rs(),Mi=class{constructor(e,t,i){this.processor=e,this.messages=[],this.root=t,this.opts=i,this.css=void 0,this.map=void 0}toString(){return this.css}warn(e,t={}){t.plugin||this.lastPlugin&&this.lastPlugin.postcssPlugin&&(t.plugin=this.lastPlugin.postcssPlugin);let i=new P0(e,t);return this.messages.push(i),i}warnings(){return this.messages.filter(e=>e.type==="warning")}get content(){return this.css}};Lf.exports=Mi;Mi.default=Mi});var Vf=v((_3,jf)=>{l();"use strict";var Ms="'".charCodeAt(0),Nf='"'.charCodeAt(0),Fi="\\".charCodeAt(0),$f="/".charCodeAt(0),Li=` +`.charCodeAt(0),xr=" ".charCodeAt(0),Ni="\f".charCodeAt(0),$i=" ".charCodeAt(0),zi="\r".charCodeAt(0),D0="[".charCodeAt(0),I0="]".charCodeAt(0),q0="(".charCodeAt(0),R0=")".charCodeAt(0),M0="{".charCodeAt(0),B0="}".charCodeAt(0),F0=";".charCodeAt(0),L0="*".charCodeAt(0),N0=":".charCodeAt(0),$0="@".charCodeAt(0),ji=/[\t\n\f\r "#'()/;[\\\]{}]/g,Vi=/[\t\n\f\r !"#'():;@[\\\]{}]|\/(?=\*)/g,z0=/.[\n"'(/\\]/,zf=/[\da-f]/i;jf.exports=function(e,t={}){let i=e.css.valueOf(),n=t.ignoreErrors,a,s,o,u,c,f,d,p,m,b,x=i.length,y=0,w=[],k=[];function S(){return y}function _(R){throw e.error("Unclosed "+R,y)}function E(){return k.length===0&&y>=x}function I(R){if(k.length)return k.pop();if(y>=x)return;let J=R?R.ignoreUnclosed:!1;switch(a=i.charCodeAt(y),a){case Li:case xr:case $i:case zi:case Ni:{s=y;do s+=1,a=i.charCodeAt(s);while(a===xr||a===Li||a===$i||a===zi||a===Ni);b=["space",i.slice(y,s)],y=s-1;break}case D0:case I0:case M0:case B0:case N0:case F0:case R0:{let ue=String.fromCharCode(a);b=[ue,ue,y];break}case q0:{if(p=w.length?w.pop()[1]:"",m=i.charCodeAt(y+1),p==="url"&&m!==Ms&&m!==Nf&&m!==xr&&m!==Li&&m!==$i&&m!==Ni&&m!==zi){s=y;do{if(f=!1,s=i.indexOf(")",s+1),s===-1)if(n||J){s=y;break}else _("bracket");for(d=s;i.charCodeAt(d-1)===Fi;)d-=1,f=!f}while(f);b=["brackets",i.slice(y,s+1),y,s],y=s}else s=i.indexOf(")",y+1),u=i.slice(y,s+1),s===-1||z0.test(u)?b=["(","(",y]:(b=["brackets",u,y,s],y=s);break}case Ms:case Nf:{o=a===Ms?"'":'"',s=y;do{if(f=!1,s=i.indexOf(o,s+1),s===-1)if(n||J){s=y+1;break}else _("string");for(d=s;i.charCodeAt(d-1)===Fi;)d-=1,f=!f}while(f);b=["string",i.slice(y,s+1),y,s],y=s;break}case $0:{ji.lastIndex=y+1,ji.test(i),ji.lastIndex===0?s=i.length-1:s=ji.lastIndex-2,b=["at-word",i.slice(y,s+1),y,s],y=s;break}case Fi:{for(s=y,c=!0;i.charCodeAt(s+1)===Fi;)s+=1,c=!c;if(a=i.charCodeAt(s+1),c&&a!==$f&&a!==xr&&a!==Li&&a!==$i&&a!==zi&&a!==Ni&&(s+=1,zf.test(i.charAt(s)))){for(;zf.test(i.charAt(s+1));)s+=1;i.charCodeAt(s+1)===xr&&(s+=1)}b=["word",i.slice(y,s+1),y,s],y=s;break}default:{a===$f&&i.charCodeAt(y+1)===L0?(s=i.indexOf("*/",y+2)+1,s===0&&(n||J?s=i.length:_("comment")),b=["comment",i.slice(y,s+1),y,s],y=s):(Vi.lastIndex=y+1,Vi.test(i),Vi.lastIndex===0?s=i.length-1:s=Vi.lastIndex-2,b=["word",i.slice(y,s+1),y,s],w.push(b),y=s);break}}return y++,b}function q(R){k.push(R)}return{back:q,nextToken:I,endOfFile:E,position:S}}});var Ui=v((O3,Wf)=>{l();"use strict";var Uf=nt(),kr=class extends Uf{constructor(e){super(e);this.type="atrule"}append(...e){return this.proxyOf.nodes||(this.nodes=[]),super.append(...e)}prepend(...e){return this.proxyOf.nodes||(this.nodes=[]),super.prepend(...e)}};Wf.exports=kr;kr.default=kr;Uf.registerAtRule(kr)});var Dt=v((E3,Qf)=>{l();"use strict";var Gf=nt(),Hf,Yf,vt=class extends Gf{constructor(e){super(e);this.type="root",this.nodes||(this.nodes=[])}removeChild(e,t){let i=this.index(e);return!t&&i===0&&this.nodes.length>1&&(this.nodes[1].raws.before=this.nodes[i].raws.before),super.removeChild(e)}normalize(e,t,i){let n=super.normalize(e);if(t){if(i==="prepend")this.nodes.length>1?t.raws.before=this.nodes[1].raws.before:delete t.raws.before;else if(this.first!==t)for(let a of n)a.raws.before=t.raws.before}return n}toResult(e={}){return new Hf(new Yf,this,e).stringify()}};vt.registerLazyResult=r=>{Hf=r};vt.registerProcessor=r=>{Yf=r};Qf.exports=vt;vt.default=vt;Gf.registerRoot(vt)});var Bs=v((T3,Jf)=>{l();"use strict";var Sr={split(r,e,t){let i=[],n="",a=!1,s=0,o=!1,u="",c=!1;for(let f of r)c?c=!1:f==="\\"?c=!0:o?f===u&&(o=!1):f==='"'||f==="'"?(o=!0,u=f):f==="("?s+=1:f===")"?s>0&&(s-=1):s===0&&e.includes(f)&&(a=!0),a?(n!==""&&i.push(n.trim()),n="",a=!1):n+=f;return(t||n!=="")&&i.push(n.trim()),i},space(r){let e=[" ",` +`," "];return Sr.split(r,e)},comma(r){return Sr.split(r,[","],!0)}};Jf.exports=Sr;Sr.default=Sr});var Wi=v((P3,Kf)=>{l();"use strict";var Xf=nt(),j0=Bs(),Cr=class extends Xf{constructor(e){super(e);this.type="rule",this.nodes||(this.nodes=[])}get selectors(){return j0.comma(this.selector)}set selectors(e){let t=this.selector?this.selector.match(/,\s*/):null,i=t?t[0]:","+this.raw("between","beforeOpen");this.selector=e.join(i)}};Kf.exports=Cr;Cr.default=Cr;Xf.registerRule(Cr)});var ic=v((D3,rc)=>{l();"use strict";var V0=br(),U0=Vf(),W0=vr(),G0=Ui(),H0=Dt(),Zf=Wi(),ec={empty:!0,space:!0};function Y0(r){for(let e=r.length-1;e>=0;e--){let t=r[e],i=t[3]||t[2];if(i)return i}}var tc=class{constructor(e){this.input=e,this.root=new H0,this.current=this.root,this.spaces="",this.semicolon=!1,this.customProperty=!1,this.createTokenizer(),this.root.source={input:e,start:{offset:0,line:1,column:1}}}createTokenizer(){this.tokenizer=U0(this.input)}parse(){let e;for(;!this.tokenizer.endOfFile();)switch(e=this.tokenizer.nextToken(),e[0]){case"space":this.spaces+=e[1];break;case";":this.freeSemicolon(e);break;case"}":this.end(e);break;case"comment":this.comment(e);break;case"at-word":this.atrule(e);break;case"{":this.emptyRule(e);break;default:this.other(e);break}this.endFile()}comment(e){let t=new W0;this.init(t,e[2]),t.source.end=this.getPosition(e[3]||e[2]);let i=e[1].slice(2,-2);if(/^\s*$/.test(i))t.text="",t.raws.left=i,t.raws.right="";else{let n=i.match(/^(\s*)([^]*\S)(\s*)$/);t.text=n[2],t.raws.left=n[1],t.raws.right=n[3]}}emptyRule(e){let t=new Zf;this.init(t,e[2]),t.selector="",t.raws.between="",this.current=t}other(e){let t=!1,i=null,n=!1,a=null,s=[],o=e[1].startsWith("--"),u=[],c=e;for(;c;){if(i=c[0],u.push(c),i==="("||i==="[")a||(a=c),s.push(i==="("?")":"]");else if(o&&n&&i==="{")a||(a=c),s.push("}");else if(s.length===0)if(i===";")if(n){this.decl(u,o);return}else break;else if(i==="{"){this.rule(u);return}else if(i==="}"){this.tokenizer.back(u.pop()),t=!0;break}else i===":"&&(n=!0);else i===s[s.length-1]&&(s.pop(),s.length===0&&(a=null));c=this.tokenizer.nextToken()}if(this.tokenizer.endOfFile()&&(t=!0),s.length>0&&this.unclosedBracket(a),t&&n){if(!o)for(;u.length&&(c=u[u.length-1][0],!(c!=="space"&&c!=="comment"));)this.tokenizer.back(u.pop());this.decl(u,o)}else this.unknownWord(u)}rule(e){e.pop();let t=new Zf;this.init(t,e[0][2]),t.raws.between=this.spacesAndCommentsFromEnd(e),this.raw(t,"selector",e),this.current=t}decl(e,t){let i=new V0;this.init(i,e[0][2]);let n=e[e.length-1];for(n[0]===";"&&(this.semicolon=!0,e.pop()),i.source.end=this.getPosition(n[3]||n[2]||Y0(e));e[0][0]!=="word";)e.length===1&&this.unknownWord(e),i.raws.before+=e.shift()[1];for(i.source.start=this.getPosition(e[0][2]),i.prop="";e.length;){let c=e[0][0];if(c===":"||c==="space"||c==="comment")break;i.prop+=e.shift()[1]}i.raws.between="";let a;for(;e.length;)if(a=e.shift(),a[0]===":"){i.raws.between+=a[1];break}else a[0]==="word"&&/\w/.test(a[1])&&this.unknownWord([a]),i.raws.between+=a[1];(i.prop[0]==="_"||i.prop[0]==="*")&&(i.raws.before+=i.prop[0],i.prop=i.prop.slice(1));let s=[],o;for(;e.length&&(o=e[0][0],!(o!=="space"&&o!=="comment"));)s.push(e.shift());this.precheckMissedSemicolon(e);for(let c=e.length-1;c>=0;c--){if(a=e[c],a[1].toLowerCase()==="!important"){i.important=!0;let f=this.stringFrom(e,c);f=this.spacesFromEnd(e)+f,f!==" !important"&&(i.raws.important=f);break}else if(a[1].toLowerCase()==="important"){let f=e.slice(0),d="";for(let p=c;p>0;p--){let m=f[p][0];if(d.trim().indexOf("!")===0&&m!=="space")break;d=f.pop()[1]+d}d.trim().indexOf("!")===0&&(i.important=!0,i.raws.important=d,e=f)}if(a[0]!=="space"&&a[0]!=="comment")break}e.some(c=>c[0]!=="space"&&c[0]!=="comment")&&(i.raws.between+=s.map(c=>c[1]).join(""),s=[]),this.raw(i,"value",s.concat(e),t),i.value.includes(":")&&!t&&this.checkMissedSemicolon(e)}atrule(e){let t=new G0;t.name=e[1].slice(1),t.name===""&&this.unnamedAtrule(t,e),this.init(t,e[2]);let i,n,a,s=!1,o=!1,u=[],c=[];for(;!this.tokenizer.endOfFile();){if(e=this.tokenizer.nextToken(),i=e[0],i==="("||i==="["?c.push(i==="("?")":"]"):i==="{"&&c.length>0?c.push("}"):i===c[c.length-1]&&c.pop(),c.length===0)if(i===";"){t.source.end=this.getPosition(e[2]),this.semicolon=!0;break}else if(i==="{"){o=!0;break}else if(i==="}"){if(u.length>0){for(a=u.length-1,n=u[a];n&&n[0]==="space";)n=u[--a];n&&(t.source.end=this.getPosition(n[3]||n[2]))}this.end(e);break}else u.push(e);else u.push(e);if(this.tokenizer.endOfFile()){s=!0;break}}t.raws.between=this.spacesAndCommentsFromEnd(u),u.length?(t.raws.afterName=this.spacesAndCommentsFromStart(u),this.raw(t,"params",u),s&&(e=u[u.length-1],t.source.end=this.getPosition(e[3]||e[2]),this.spaces=t.raws.between,t.raws.between="")):(t.raws.afterName="",t.params=""),o&&(t.nodes=[],this.current=t)}end(e){this.current.nodes&&this.current.nodes.length&&(this.current.raws.semicolon=this.semicolon),this.semicolon=!1,this.current.raws.after=(this.current.raws.after||"")+this.spaces,this.spaces="",this.current.parent?(this.current.source.end=this.getPosition(e[2]),this.current=this.current.parent):this.unexpectedClose(e)}endFile(){this.current.parent&&this.unclosedBlock(),this.current.nodes&&this.current.nodes.length&&(this.current.raws.semicolon=this.semicolon),this.current.raws.after=(this.current.raws.after||"")+this.spaces}freeSemicolon(e){if(this.spaces+=e[1],this.current.nodes){let t=this.current.nodes[this.current.nodes.length-1];t&&t.type==="rule"&&!t.raws.ownSemicolon&&(t.raws.ownSemicolon=this.spaces,this.spaces="")}}getPosition(e){let t=this.input.fromOffset(e);return{offset:e,line:t.line,column:t.col}}init(e,t){this.current.push(e),e.source={start:this.getPosition(t),input:this.input},e.raws.before=this.spaces,this.spaces="",e.type!=="comment"&&(this.semicolon=!1)}raw(e,t,i,n){let a,s,o=i.length,u="",c=!0,f,d;for(let p=0;pm+b[1],"");e.raws[t]={value:u,raw:p}}e[t]=u}spacesAndCommentsFromEnd(e){let t,i="";for(;e.length&&(t=e[e.length-1][0],!(t!=="space"&&t!=="comment"));)i=e.pop()[1]+i;return i}spacesAndCommentsFromStart(e){let t,i="";for(;e.length&&(t=e[0][0],!(t!=="space"&&t!=="comment"));)i+=e.shift()[1];return i}spacesFromEnd(e){let t,i="";for(;e.length&&(t=e[e.length-1][0],t==="space");)i=e.pop()[1]+i;return i}stringFrom(e,t){let i="";for(let n=t;n=0&&(n=e[a],!(n[0]!=="space"&&(i+=1,i===2)));a--);throw this.input.error("Missed semicolon",n[0]==="word"?n[3]+1:n[2])}};rc.exports=tc});var nc=v(()=>{l()});var ac=v((R3,sc)=>{l();var Q0="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict",J0=(r,e=21)=>(t=e)=>{let i="",n=t;for(;n--;)i+=r[Math.random()*r.length|0];return i},X0=(r=21)=>{let e="",t=r;for(;t--;)e+=Q0[Math.random()*64|0];return e};sc.exports={nanoid:X0,customAlphabet:J0}});var Fs=v((M3,oc)=>{l();oc.exports={}});var Hi=v((B3,cc)=>{l();"use strict";var{SourceMapConsumer:K0,SourceMapGenerator:Z0}=nc(),{fileURLToPath:lc,pathToFileURL:Gi}=(Cs(),pf),{resolve:Ls,isAbsolute:Ns}=(bt(),uf),{nanoid:ev}=ac(),$s=As(),uc=_i(),tv=Fs(),zs=Symbol("fromOffsetCache"),rv=Boolean(K0&&Z0),fc=Boolean(Ls&&Ns),Ar=class{constructor(e,t={}){if(e===null||typeof e=="undefined"||typeof e=="object"&&!e.toString)throw new Error(`PostCSS received ${e} instead of CSS string`);if(this.css=e.toString(),this.css[0]==="\uFEFF"||this.css[0]==="\uFFFE"?(this.hasBOM=!0,this.css=this.css.slice(1)):this.hasBOM=!1,t.from&&(!fc||/^\w+:\/\//.test(t.from)||Ns(t.from)?this.file=t.from:this.file=Ls(t.from)),fc&&rv){let i=new tv(this.css,t);if(i.text){this.map=i;let n=i.consumer().file;!this.file&&n&&(this.file=this.mapResolve(n))}}this.file||(this.id=""),this.map&&(this.map.file=this.from)}fromOffset(e){let t,i;if(this[zs])i=this[zs];else{let a=this.css.split(` +`);i=new Array(a.length);let s=0;for(let o=0,u=a.length;o=t)n=i.length-1;else{let a=i.length-2,s;for(;n>1),e=i[s+1])n=s+1;else{n=s;break}}return{line:n+1,col:e-i[n]+1}}error(e,t,i,n={}){let a,s,o;if(t&&typeof t=="object"){let c=t,f=i;if(typeof c.offset=="number"){let d=this.fromOffset(c.offset);t=d.line,i=d.col}else t=c.line,i=c.column;if(typeof f.offset=="number"){let d=this.fromOffset(f.offset);s=d.line,o=d.col}else s=f.line,o=f.column}else if(!i){let c=this.fromOffset(t);t=c.line,i=c.col}let u=this.origin(t,i,s,o);return u?a=new uc(e,u.endLine===void 0?u.line:{line:u.line,column:u.column},u.endLine===void 0?u.column:{line:u.endLine,column:u.endColumn},u.source,u.file,n.plugin):a=new uc(e,s===void 0?t:{line:t,column:i},s===void 0?i:{line:s,column:o},this.css,this.file,n.plugin),a.input={line:t,column:i,endLine:s,endColumn:o,source:this.css},this.file&&(Gi&&(a.input.url=Gi(this.file).toString()),a.input.file=this.file),a}origin(e,t,i,n){if(!this.map)return!1;let a=this.map.consumer(),s=a.originalPositionFor({line:e,column:t});if(!s.source)return!1;let o;typeof i=="number"&&(o=a.originalPositionFor({line:i,column:n}));let u;Ns(s.source)?u=Gi(s.source):u=new URL(s.source,this.map.consumer().sourceRoot||Gi(this.map.mapFile));let c={url:u.toString(),line:s.line,column:s.column,endLine:o&&o.line,endColumn:o&&o.column};if(u.protocol==="file:")if(lc)c.file=lc(u);else throw new Error("file: protocol is not available in this PostCSS build");let f=a.sourceContentFor(s.source);return f&&(c.source=f),c}mapResolve(e){return/^\w+:\/\//.test(e)?e:Ls(this.map.consumer().sourceRoot||this.map.root||".",e)}get from(){return this.file||this.id}toJSON(){let e={};for(let t of["hasBOM","css","file","id"])this[t]!=null&&(e[t]=this[t]);return this.map&&(e.map={...this.map},e.map.consumerCache&&(e.map.consumerCache=void 0)),e}};cc.exports=Ar;Ar.default=Ar;$s&&$s.registerInput&&$s.registerInput(Ar)});var Qi=v((F3,pc)=>{l();"use strict";var iv=nt(),nv=ic(),sv=Hi();function Yi(r,e){let t=new sv(r,e),i=new nv(t);try{i.parse()}catch(n){throw n}return i.root}pc.exports=Yi;Yi.default=Yi;iv.registerParse(Yi)});var Us=v((N3,gc)=>{l();"use strict";var{isClean:qe,my:av}=Oi(),ov=Ps(),lv=yr(),uv=nt(),fv=qi(),L3=qs(),dc=Bi(),cv=Qi(),pv=Dt(),dv={document:"Document",root:"Root",atrule:"AtRule",rule:"Rule",decl:"Declaration",comment:"Comment"},hv={postcssPlugin:!0,prepare:!0,Once:!0,Document:!0,Root:!0,Declaration:!0,Rule:!0,AtRule:!0,Comment:!0,DeclarationExit:!0,RuleExit:!0,AtRuleExit:!0,CommentExit:!0,RootExit:!0,DocumentExit:!0,OnceExit:!0},mv={postcssPlugin:!0,prepare:!0,Once:!0},It=0;function _r(r){return typeof r=="object"&&typeof r.then=="function"}function hc(r){let e=!1,t=dv[r.type];return r.type==="decl"?e=r.prop.toLowerCase():r.type==="atrule"&&(e=r.name.toLowerCase()),e&&r.append?[t,t+"-"+e,It,t+"Exit",t+"Exit-"+e]:e?[t,t+"-"+e,t+"Exit",t+"Exit-"+e]:r.append?[t,It,t+"Exit"]:[t,t+"Exit"]}function mc(r){let e;return r.type==="document"?e=["Document",It,"DocumentExit"]:r.type==="root"?e=["Root",It,"RootExit"]:e=hc(r),{node:r,events:e,eventIndex:0,visitors:[],visitorIndex:0,iterator:0}}function js(r){return r[qe]=!1,r.nodes&&r.nodes.forEach(e=>js(e)),r}var Vs={},Ve=class{constructor(e,t,i){this.stringified=!1,this.processed=!1;let n;if(typeof t=="object"&&t!==null&&(t.type==="root"||t.type==="document"))n=js(t);else if(t instanceof Ve||t instanceof dc)n=js(t.root),t.map&&(typeof i.map=="undefined"&&(i.map={}),i.map.inline||(i.map.inline=!1),i.map.prev=t.map);else{let a=cv;i.syntax&&(a=i.syntax.parse),i.parser&&(a=i.parser),a.parse&&(a=a.parse);try{n=a(t,i)}catch(s){this.processed=!0,this.error=s}n&&!n[av]&&uv.rebuild(n)}this.result=new dc(e,n,i),this.helpers={...Vs,result:this.result,postcss:Vs},this.plugins=this.processor.plugins.map(a=>typeof a=="object"&&a.prepare?{...a,...a.prepare(this.result)}:a)}get[Symbol.toStringTag](){return"LazyResult"}get processor(){return this.result.processor}get opts(){return this.result.opts}get css(){return this.stringify().css}get content(){return this.stringify().content}get map(){return this.stringify().map}get root(){return this.sync().root}get messages(){return this.sync().messages}warnings(){return this.sync().warnings()}toString(){return this.css}then(e,t){return this.async().then(e,t)}catch(e){return this.async().catch(e)}finally(e){return this.async().then(e,e)}async(){return this.error?Promise.reject(this.error):this.processed?Promise.resolve(this.result):(this.processing||(this.processing=this.runAsync()),this.processing)}sync(){if(this.error)throw this.error;if(this.processed)return this.result;if(this.processed=!0,this.processing)throw this.getAsyncError();for(let e of this.plugins){let t=this.runOnRoot(e);if(_r(t))throw this.getAsyncError()}if(this.prepareVisitors(),this.hasListener){let e=this.result.root;for(;!e[qe];)e[qe]=!0,this.walkSync(e);if(this.listeners.OnceExit)if(e.type==="document")for(let t of e.nodes)this.visitSync(this.listeners.OnceExit,t);else this.visitSync(this.listeners.OnceExit,e)}return this.result}stringify(){if(this.error)throw this.error;if(this.stringified)return this.result;this.stringified=!0,this.sync();let e=this.result.opts,t=lv;e.syntax&&(t=e.syntax.stringify),e.stringifier&&(t=e.stringifier),t.stringify&&(t=t.stringify);let n=new ov(t,this.result.root,this.result.opts).generate();return this.result.css=n[0],this.result.map=n[1],this.result}walkSync(e){e[qe]=!0;let t=hc(e);for(let i of t)if(i===It)e.nodes&&e.each(n=>{n[qe]||this.walkSync(n)});else{let n=this.listeners[i];if(n&&this.visitSync(n,e.toProxy()))return}}visitSync(e,t){for(let[i,n]of e){this.result.lastPlugin=i;let a;try{a=n(t,this.helpers)}catch(s){throw this.handleError(s,t.proxyOf)}if(t.type!=="root"&&t.type!=="document"&&!t.parent)return!0;if(_r(a))throw this.getAsyncError()}}runOnRoot(e){this.result.lastPlugin=e;try{if(typeof e=="object"&&e.Once){if(this.result.root.type==="document"){let t=this.result.root.nodes.map(i=>e.Once(i,this.helpers));return _r(t[0])?Promise.all(t):t}return e.Once(this.result.root,this.helpers)}else if(typeof e=="function")return e(this.result.root,this.result)}catch(t){throw this.handleError(t)}}getAsyncError(){throw new Error("Use process(css).then(cb) to work with async plugins")}handleError(e,t){let i=this.result.lastPlugin;try{t&&t.addToError(e),this.error=e,e.name==="CssSyntaxError"&&!e.plugin?(e.plugin=i.postcssPlugin,e.setMessage()):i.postcssVersion}catch(n){console&&console.error&&console.error(n)}return e}async runAsync(){this.plugin=0;for(let e=0;e0;){let i=this.visitTick(t);if(_r(i))try{await i}catch(n){let a=t[t.length-1].node;throw this.handleError(n,a)}}}if(this.listeners.OnceExit)for(let[t,i]of this.listeners.OnceExit){this.result.lastPlugin=t;try{if(e.type==="document"){let n=e.nodes.map(a=>i(a,this.helpers));await Promise.all(n)}else await i(e,this.helpers)}catch(n){throw this.handleError(n)}}}return this.processed=!0,this.stringify()}prepareVisitors(){this.listeners={};let e=(t,i,n)=>{this.listeners[i]||(this.listeners[i]=[]),this.listeners[i].push([t,n])};for(let t of this.plugins)if(typeof t=="object")for(let i in t){if(!hv[i]&&/^[A-Z]/.test(i))throw new Error(`Unknown event ${i} in ${t.postcssPlugin}. Try to update PostCSS (${this.processor.version} now).`);if(!mv[i])if(typeof t[i]=="object")for(let n in t[i])n==="*"?e(t,i,t[i][n]):e(t,i+"-"+n.toLowerCase(),t[i][n]);else typeof t[i]=="function"&&e(t,i,t[i])}this.hasListener=Object.keys(this.listeners).length>0}visitTick(e){let t=e[e.length-1],{node:i,visitors:n}=t;if(i.type!=="root"&&i.type!=="document"&&!i.parent){e.pop();return}if(n.length>0&&t.visitorIndex{Vs=r};gc.exports=Ve;Ve.default=Ve;pv.registerLazyResult(Ve);fv.registerLazyResult(Ve)});var wc=v((z3,yc)=>{l();"use strict";var gv=Ps(),yv=yr(),$3=qs(),wv=Qi(),bv=Bi(),Ji=class{constructor(e,t,i){t=t.toString(),this.stringified=!1,this._processor=e,this._css=t,this._opts=i,this._map=void 0;let n,a=yv;this.result=new bv(this._processor,n,this._opts),this.result.css=t;let s=this;Object.defineProperty(this.result,"root",{get(){return s.root}});let o=new gv(a,n,this._opts,t);if(o.isMap()){let[u,c]=o.generate();u&&(this.result.css=u),c&&(this.result.map=c)}}get[Symbol.toStringTag](){return"NoWorkResult"}get processor(){return this.result.processor}get opts(){return this.result.opts}get css(){return this.result.css}get content(){return this.result.css}get map(){return this.result.map}get root(){if(this._root)return this._root;let e,t=wv;try{e=t(this._css,this._opts)}catch(i){this.error=i}if(this.error)throw this.error;return this._root=e,e}get messages(){return[]}warnings(){return[]}toString(){return this._css}then(e,t){return this.async().then(e,t)}catch(e){return this.async().catch(e)}finally(e){return this.async().then(e,e)}async(){return this.error?Promise.reject(this.error):Promise.resolve(this.result)}sync(){if(this.error)throw this.error;return this.result}};yc.exports=Ji;Ji.default=Ji});var vc=v((j3,bc)=>{l();"use strict";var vv=wc(),xv=Us(),kv=qi(),Sv=Dt(),qt=class{constructor(e=[]){this.version="8.4.24",this.plugins=this.normalize(e)}use(e){return this.plugins=this.plugins.concat(this.normalize([e])),this}process(e,t={}){return this.plugins.length===0&&typeof t.parser=="undefined"&&typeof t.stringifier=="undefined"&&typeof t.syntax=="undefined"?new vv(this,e,t):new xv(this,e,t)}normalize(e){let t=[];for(let i of e)if(i.postcss===!0?i=i():i.postcss&&(i=i.postcss),typeof i=="object"&&Array.isArray(i.plugins))t=t.concat(i.plugins);else if(typeof i=="object"&&i.postcssPlugin)t.push(i);else if(typeof i=="function")t.push(i);else if(!(typeof i=="object"&&(i.parse||i.stringify)))throw new Error(i+" is not a PostCSS plugin");return t}};bc.exports=qt;qt.default=qt;Sv.registerProcessor(qt);kv.registerProcessor(qt)});var kc=v((V3,xc)=>{l();"use strict";var Cv=br(),Av=Fs(),_v=vr(),Ov=Ui(),Ev=Hi(),Tv=Dt(),Pv=Wi();function Or(r,e){if(Array.isArray(r))return r.map(n=>Or(n));let{inputs:t,...i}=r;if(t){e=[];for(let n of t){let a={...n,__proto__:Ev.prototype};a.map&&(a.map={...a.map,__proto__:Av.prototype}),e.push(a)}}if(i.nodes&&(i.nodes=r.nodes.map(n=>Or(n,e))),i.source){let{inputId:n,...a}=i.source;i.source=a,n!=null&&(i.source.input=e[n])}if(i.type==="root")return new Tv(i);if(i.type==="decl")return new Cv(i);if(i.type==="rule")return new Pv(i);if(i.type==="comment")return new _v(i);if(i.type==="atrule")return new Ov(i);throw new Error("Unknown node type: "+r.type)}xc.exports=Or;Or.default=Or});var ge=v((U3,Tc)=>{l();"use strict";var Dv=_i(),Sc=br(),Iv=Us(),qv=nt(),Ws=vc(),Rv=yr(),Mv=kc(),Cc=qi(),Bv=Rs(),Ac=vr(),_c=Ui(),Fv=Bi(),Lv=Hi(),Nv=Qi(),$v=Bs(),Oc=Wi(),Ec=Dt(),zv=wr();function z(...r){return r.length===1&&Array.isArray(r[0])&&(r=r[0]),new Ws(r)}z.plugin=function(e,t){let i=!1;function n(...s){console&&console.warn&&!i&&(i=!0,console.warn(e+`: postcss.plugin was deprecated. Migration guide: +https://evilmartians.com/chronicles/postcss-8-plugin-migration`),h.env.LANG&&h.env.LANG.startsWith("cn")&&console.warn(e+`: \u91CC\u9762 postcss.plugin \u88AB\u5F03\u7528. \u8FC1\u79FB\u6307\u5357: +https://www.w3ctech.com/topic/2226`));let o=t(...s);return o.postcssPlugin=e,o.postcssVersion=new Ws().version,o}let a;return Object.defineProperty(n,"postcss",{get(){return a||(a=n()),a}}),n.process=function(s,o,u){return z([n(u)]).process(s,o)},n};z.stringify=Rv;z.parse=Nv;z.fromJSON=Mv;z.list=$v;z.comment=r=>new Ac(r);z.atRule=r=>new _c(r);z.decl=r=>new Sc(r);z.rule=r=>new Oc(r);z.root=r=>new Ec(r);z.document=r=>new Cc(r);z.CssSyntaxError=Dv;z.Declaration=Sc;z.Container=qv;z.Processor=Ws;z.Document=Cc;z.Comment=Ac;z.Warning=Bv;z.AtRule=_c;z.Result=Fv;z.Input=Lv;z.Rule=Oc;z.Root=Ec;z.Node=zv;Iv.registerPostcss(z);Tc.exports=z;z.default=z});var U,j,W3,G3,H3,Y3,Q3,J3,X3,K3,Z3,eT,tT,rT,iT,nT,sT,aT,oT,lT,uT,fT,cT,pT,dT,hT,st=C(()=>{l();U=X(ge()),j=U.default,W3=U.default.stringify,G3=U.default.fromJSON,H3=U.default.plugin,Y3=U.default.parse,Q3=U.default.list,J3=U.default.document,X3=U.default.comment,K3=U.default.atRule,Z3=U.default.rule,eT=U.default.decl,tT=U.default.root,rT=U.default.CssSyntaxError,iT=U.default.Declaration,nT=U.default.Container,sT=U.default.Processor,aT=U.default.Document,oT=U.default.Comment,lT=U.default.Warning,uT=U.default.AtRule,fT=U.default.Result,cT=U.default.Input,pT=U.default.Rule,dT=U.default.Root,hT=U.default.Node});var Gs=v((gT,Pc)=>{l();Pc.exports=function(r,e,t,i,n){for(e=e.split?e.split("."):e,i=0;i{l();"use strict";Xi.__esModule=!0;Xi.default=Uv;function jv(r){for(var e=r.toLowerCase(),t="",i=!1,n=0;n<6&&e[n]!==void 0;n++){var a=e.charCodeAt(n),s=a>=97&&a<=102||a>=48&&a<=57;if(i=a===32,!s)break;t+=e[n]}if(t.length!==0){var o=parseInt(t,16),u=o>=55296&&o<=57343;return u||o===0||o>1114111?["\uFFFD",t.length+(i?1:0)]:[String.fromCodePoint(o),t.length+(i?1:0)]}}var Vv=/\\/;function Uv(r){var e=Vv.test(r);if(!e)return r;for(var t="",i=0;i{l();"use strict";Zi.__esModule=!0;Zi.default=Wv;function Wv(r){for(var e=arguments.length,t=new Array(e>1?e-1:0),i=1;i0;){var n=t.shift();if(!r[n])return;r=r[n]}return r}Ic.exports=Zi.default});var Mc=v((en,Rc)=>{l();"use strict";en.__esModule=!0;en.default=Gv;function Gv(r){for(var e=arguments.length,t=new Array(e>1?e-1:0),i=1;i0;){var n=t.shift();r[n]||(r[n]={}),r=r[n]}}Rc.exports=en.default});var Fc=v((tn,Bc)=>{l();"use strict";tn.__esModule=!0;tn.default=Hv;function Hv(r){for(var e="",t=r.indexOf("/*"),i=0;t>=0;){e=e+r.slice(i,t);var n=r.indexOf("*/",t+2);if(n<0)return e;i=n+2,t=r.indexOf("/*",i)}return e=e+r.slice(i),e}Bc.exports=tn.default});var Er=v(Re=>{l();"use strict";Re.__esModule=!0;Re.unesc=Re.stripComments=Re.getProp=Re.ensureObject=void 0;var Yv=rn(Ki());Re.unesc=Yv.default;var Qv=rn(qc());Re.getProp=Qv.default;var Jv=rn(Mc());Re.ensureObject=Jv.default;var Xv=rn(Fc());Re.stripComments=Xv.default;function rn(r){return r&&r.__esModule?r:{default:r}}});var Ue=v((Tr,$c)=>{l();"use strict";Tr.__esModule=!0;Tr.default=void 0;var Lc=Er();function Nc(r,e){for(var t=0;ti||this.source.end.linen||this.source.end.line===i&&this.source.end.column{l();"use strict";W.__esModule=!0;W.UNIVERSAL=W.TAG=W.STRING=W.SELECTOR=W.ROOT=W.PSEUDO=W.NESTING=W.ID=W.COMMENT=W.COMBINATOR=W.CLASS=W.ATTRIBUTE=void 0;var tx="tag";W.TAG=tx;var rx="string";W.STRING=rx;var ix="selector";W.SELECTOR=ix;var nx="root";W.ROOT=nx;var sx="pseudo";W.PSEUDO=sx;var ax="nesting";W.NESTING=ax;var ox="id";W.ID=ox;var lx="comment";W.COMMENT=lx;var ux="combinator";W.COMBINATOR=ux;var fx="class";W.CLASS=fx;var cx="attribute";W.ATTRIBUTE=cx;var px="universal";W.UNIVERSAL=px});var nn=v((Pr,Uc)=>{l();"use strict";Pr.__esModule=!0;Pr.default=void 0;var dx=mx(Ue()),We=hx(ne());function zc(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(zc=function(n){return n?t:e})(r)}function hx(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=zc(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in r)if(a!=="default"&&Object.prototype.hasOwnProperty.call(r,a)){var s=n?Object.getOwnPropertyDescriptor(r,a):null;s&&(s.get||s.set)?Object.defineProperty(i,a,s):i[a]=r[a]}return i.default=r,t&&t.set(r,i),i}function mx(r){return r&&r.__esModule?r:{default:r}}function gx(r,e){var t=typeof Symbol!="undefined"&&r[Symbol.iterator]||r["@@iterator"];if(t)return(t=t.call(r)).next.bind(t);if(Array.isArray(r)||(t=yx(r))||e&&r&&typeof r.length=="number"){t&&(r=t);var i=0;return function(){return i>=r.length?{done:!0}:{done:!1,value:r[i++]}}}throw new TypeError(`Invalid attempt to iterate non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function yx(r,e){if(!!r){if(typeof r=="string")return jc(r,e);var t=Object.prototype.toString.call(r).slice(8,-1);if(t==="Object"&&r.constructor&&(t=r.constructor.name),t==="Map"||t==="Set")return Array.from(r);if(t==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t))return jc(r,e)}}function jc(r,e){(e==null||e>r.length)&&(e=r.length);for(var t=0,i=new Array(e);t=n&&(this.indexes[s]=a-1);return this},t.removeAll=function(){for(var n=gx(this.nodes),a;!(a=n()).done;){var s=a.value;s.parent=void 0}return this.nodes=[],this},t.empty=function(){return this.removeAll()},t.insertAfter=function(n,a){a.parent=this;var s=this.index(n);this.nodes.splice(s+1,0,a),a.parent=this;var o;for(var u in this.indexes)o=this.indexes[u],s<=o&&(this.indexes[u]=o+1);return this},t.insertBefore=function(n,a){a.parent=this;var s=this.index(n);this.nodes.splice(s,0,a),a.parent=this;var o;for(var u in this.indexes)o=this.indexes[u],o<=s&&(this.indexes[u]=o+1);return this},t._findChildAtPosition=function(n,a){var s=void 0;return this.each(function(o){if(o.atPosition){var u=o.atPosition(n,a);if(u)return s=u,!1}else if(o.isAtPosition(n,a))return s=o,!1}),s},t.atPosition=function(n,a){if(this.isAtPosition(n,a))return this._findChildAtPosition(n,a)||this},t._inferEndPosition=function(){this.last&&this.last.source&&this.last.source.end&&(this.source=this.source||{},this.source.end=this.source.end||{},Object.assign(this.source.end,this.last.source.end))},t.each=function(n){this.lastEach||(this.lastEach=0),this.indexes||(this.indexes={}),this.lastEach++;var a=this.lastEach;if(this.indexes[a]=0,!!this.length){for(var s,o;this.indexes[a]{l();"use strict";Dr.__esModule=!0;Dr.default=void 0;var xx=Sx(nn()),kx=ne();function Sx(r){return r&&r.__esModule?r:{default:r}}function Wc(r,e){for(var t=0;t{l();"use strict";Ir.__esModule=!0;Ir.default=void 0;var Ox=Tx(nn()),Ex=ne();function Tx(r){return r&&r.__esModule?r:{default:r}}function Px(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,Js(r,e)}function Js(r,e){return Js=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},Js(r,e)}var Dx=function(r){Px(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=Ex.SELECTOR,i}return e}(Ox.default);Ir.default=Dx;Hc.exports=Ir.default});var sn=v((bT,Yc)=>{l();"use strict";var Ix={},qx=Ix.hasOwnProperty,Rx=function(e,t){if(!e)return t;var i={};for(var n in t)i[n]=qx.call(e,n)?e[n]:t[n];return i},Mx=/[ -,\.\/:-@\[-\^`\{-~]/,Bx=/[ -,\.\/:-@\[\]\^`\{-~]/,Fx=/(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g,Ks=function r(e,t){t=Rx(t,r.options),t.quotes!="single"&&t.quotes!="double"&&(t.quotes="single");for(var i=t.quotes=="double"?'"':"'",n=t.isIdentifier,a=e.charAt(0),s="",o=0,u=e.length;o126){if(f>=55296&&f<=56319&&o{l();"use strict";qr.__esModule=!0;qr.default=void 0;var Lx=Qc(sn()),Nx=Er(),$x=Qc(Ue()),zx=ne();function Qc(r){return r&&r.__esModule?r:{default:r}}function Jc(r,e){for(var t=0;t{l();"use strict";Rr.__esModule=!0;Rr.default=void 0;var Wx=Hx(Ue()),Gx=ne();function Hx(r){return r&&r.__esModule?r:{default:r}}function Yx(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,ta(r,e)}function ta(r,e){return ta=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},ta(r,e)}var Qx=function(r){Yx(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=Gx.COMMENT,i}return e}(Wx.default);Rr.default=Qx;Kc.exports=Rr.default});var na=v((Mr,Zc)=>{l();"use strict";Mr.__esModule=!0;Mr.default=void 0;var Jx=Kx(Ue()),Xx=ne();function Kx(r){return r&&r.__esModule?r:{default:r}}function Zx(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,ia(r,e)}function ia(r,e){return ia=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},ia(r,e)}var e1=function(r){Zx(e,r);function e(i){var n;return n=r.call(this,i)||this,n.type=Xx.ID,n}var t=e.prototype;return t.valueToString=function(){return"#"+r.prototype.valueToString.call(this)},e}(Jx.default);Mr.default=e1;Zc.exports=Mr.default});var an=v((Br,rp)=>{l();"use strict";Br.__esModule=!0;Br.default=void 0;var t1=ep(sn()),r1=Er(),i1=ep(Ue());function ep(r){return r&&r.__esModule?r:{default:r}}function tp(r,e){for(var t=0;t{l();"use strict";Fr.__esModule=!0;Fr.default=void 0;var o1=u1(an()),l1=ne();function u1(r){return r&&r.__esModule?r:{default:r}}function f1(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,aa(r,e)}function aa(r,e){return aa=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},aa(r,e)}var c1=function(r){f1(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=l1.TAG,i}return e}(o1.default);Fr.default=c1;ip.exports=Fr.default});var ua=v((Lr,np)=>{l();"use strict";Lr.__esModule=!0;Lr.default=void 0;var p1=h1(Ue()),d1=ne();function h1(r){return r&&r.__esModule?r:{default:r}}function m1(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,la(r,e)}function la(r,e){return la=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},la(r,e)}var g1=function(r){m1(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=d1.STRING,i}return e}(p1.default);Lr.default=g1;np.exports=Lr.default});var ca=v((Nr,sp)=>{l();"use strict";Nr.__esModule=!0;Nr.default=void 0;var y1=b1(nn()),w1=ne();function b1(r){return r&&r.__esModule?r:{default:r}}function v1(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,fa(r,e)}function fa(r,e){return fa=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},fa(r,e)}var x1=function(r){v1(e,r);function e(i){var n;return n=r.call(this,i)||this,n.type=w1.PSEUDO,n}var t=e.prototype;return t.toString=function(){var n=this.length?"("+this.map(String).join(",")+")":"";return[this.rawSpaceBefore,this.stringifyProperty("value"),n,this.rawSpaceAfter].join("")},e}(y1.default);Nr.default=x1;sp.exports=Nr.default});var ap={};Ae(ap,{deprecate:()=>k1});function k1(r){return r}var op=C(()=>{l()});var up=v((vT,lp)=>{l();lp.exports=(op(),ap).deprecate});var ya=v(jr=>{l();"use strict";jr.__esModule=!0;jr.default=void 0;jr.unescapeValue=ma;var $r=da(sn()),S1=da(Ki()),C1=da(an()),A1=ne(),pa;function da(r){return r&&r.__esModule?r:{default:r}}function fp(r,e){for(var t=0;t0&&!n.quoted&&o.before.length===0&&!(n.spaces.value&&n.spaces.value.after)&&(o.before=" "),cp(s,o)}))),a.push("]"),a.push(this.rawSpaceAfter),a.join("")},_1(e,[{key:"quoted",get:function(){var n=this.quoteMark;return n==="'"||n==='"'},set:function(n){P1()}},{key:"quoteMark",get:function(){return this._quoteMark},set:function(n){if(!this._constructed){this._quoteMark=n;return}this._quoteMark!==n&&(this._quoteMark=n,this._syncRawValue())}},{key:"qualifiedAttribute",get:function(){return this.qualifiedName(this.raws.attribute||this.attribute)}},{key:"insensitiveFlag",get:function(){return this.insensitive?"i":""}},{key:"value",get:function(){return this._value},set:function(n){if(this._constructed){var a=ma(n),s=a.deprecatedUsage,o=a.unescaped,u=a.quoteMark;if(s&&T1(),o===this._value&&u===this._quoteMark)return;this._value=o,this._quoteMark=u,this._syncRawValue()}else this._value=n}},{key:"insensitive",get:function(){return this._insensitive},set:function(n){n||(this._insensitive=!1,this.raws&&(this.raws.insensitiveFlag==="I"||this.raws.insensitiveFlag==="i")&&(this.raws.insensitiveFlag=void 0)),this._insensitive=n}},{key:"attribute",get:function(){return this._attribute},set:function(n){this._handleEscapes("attribute",n),this._attribute=n}}]),e}(C1.default);jr.default=on;on.NO_QUOTE=null;on.SINGLE_QUOTE="'";on.DOUBLE_QUOTE='"';var ga=(pa={"'":{quotes:"single",wrap:!0},'"':{quotes:"double",wrap:!0}},pa[null]={isIdentifier:!0},pa);function cp(r,e){return""+e.before+r+e.after}});var ba=v((Vr,pp)=>{l();"use strict";Vr.__esModule=!0;Vr.default=void 0;var q1=M1(an()),R1=ne();function M1(r){return r&&r.__esModule?r:{default:r}}function B1(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,wa(r,e)}function wa(r,e){return wa=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},wa(r,e)}var F1=function(r){B1(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=R1.UNIVERSAL,i.value="*",i}return e}(q1.default);Vr.default=F1;pp.exports=Vr.default});var xa=v((Ur,dp)=>{l();"use strict";Ur.__esModule=!0;Ur.default=void 0;var L1=$1(Ue()),N1=ne();function $1(r){return r&&r.__esModule?r:{default:r}}function z1(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,va(r,e)}function va(r,e){return va=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},va(r,e)}var j1=function(r){z1(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=N1.COMBINATOR,i}return e}(L1.default);Ur.default=j1;dp.exports=Ur.default});var Sa=v((Wr,hp)=>{l();"use strict";Wr.__esModule=!0;Wr.default=void 0;var V1=W1(Ue()),U1=ne();function W1(r){return r&&r.__esModule?r:{default:r}}function G1(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,ka(r,e)}function ka(r,e){return ka=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},ka(r,e)}var H1=function(r){G1(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=U1.NESTING,i.value="&",i}return e}(V1.default);Wr.default=H1;hp.exports=Wr.default});var gp=v((ln,mp)=>{l();"use strict";ln.__esModule=!0;ln.default=Y1;function Y1(r){return r.sort(function(e,t){return e-t})}mp.exports=ln.default});var Ca=v(D=>{l();"use strict";D.__esModule=!0;D.word=D.tilde=D.tab=D.str=D.space=D.slash=D.singleQuote=D.semicolon=D.plus=D.pipe=D.openSquare=D.openParenthesis=D.newline=D.greaterThan=D.feed=D.equals=D.doubleQuote=D.dollar=D.cr=D.comment=D.comma=D.combinator=D.colon=D.closeSquare=D.closeParenthesis=D.caret=D.bang=D.backslash=D.at=D.asterisk=D.ampersand=void 0;var Q1=38;D.ampersand=Q1;var J1=42;D.asterisk=J1;var X1=64;D.at=X1;var K1=44;D.comma=K1;var Z1=58;D.colon=Z1;var ek=59;D.semicolon=ek;var tk=40;D.openParenthesis=tk;var rk=41;D.closeParenthesis=rk;var ik=91;D.openSquare=ik;var nk=93;D.closeSquare=nk;var sk=36;D.dollar=sk;var ak=126;D.tilde=ak;var ok=94;D.caret=ok;var lk=43;D.plus=lk;var uk=61;D.equals=uk;var fk=124;D.pipe=fk;var ck=62;D.greaterThan=ck;var pk=32;D.space=pk;var yp=39;D.singleQuote=yp;var dk=34;D.doubleQuote=dk;var hk=47;D.slash=hk;var mk=33;D.bang=mk;var gk=92;D.backslash=gk;var yk=13;D.cr=yk;var wk=12;D.feed=wk;var bk=10;D.newline=bk;var vk=9;D.tab=vk;var xk=yp;D.str=xk;var kk=-1;D.comment=kk;var Sk=-2;D.word=Sk;var Ck=-3;D.combinator=Ck});var vp=v(Gr=>{l();"use strict";Gr.__esModule=!0;Gr.FIELDS=void 0;Gr.default=Dk;var O=Ak(Ca()),Rt,V;function wp(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(wp=function(n){return n?t:e})(r)}function Ak(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=wp(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in r)if(a!=="default"&&Object.prototype.hasOwnProperty.call(r,a)){var s=n?Object.getOwnPropertyDescriptor(r,a):null;s&&(s.get||s.set)?Object.defineProperty(i,a,s):i[a]=r[a]}return i.default=r,t&&t.set(r,i),i}var _k=(Rt={},Rt[O.tab]=!0,Rt[O.newline]=!0,Rt[O.cr]=!0,Rt[O.feed]=!0,Rt),Ok=(V={},V[O.space]=!0,V[O.tab]=!0,V[O.newline]=!0,V[O.cr]=!0,V[O.feed]=!0,V[O.ampersand]=!0,V[O.asterisk]=!0,V[O.bang]=!0,V[O.comma]=!0,V[O.colon]=!0,V[O.semicolon]=!0,V[O.openParenthesis]=!0,V[O.closeParenthesis]=!0,V[O.openSquare]=!0,V[O.closeSquare]=!0,V[O.singleQuote]=!0,V[O.doubleQuote]=!0,V[O.plus]=!0,V[O.pipe]=!0,V[O.tilde]=!0,V[O.greaterThan]=!0,V[O.equals]=!0,V[O.dollar]=!0,V[O.caret]=!0,V[O.slash]=!0,V),Aa={},bp="0123456789abcdefABCDEF";for(un=0;un0?(k=s+x,S=w-y[x].length):(k=s,S=a),E=O.comment,s=k,p=k,d=w-S):c===O.slash?(w=o,E=c,p=s,d=o-a,u=w+1):(w=Ek(t,o),E=O.word,p=s,d=w-a),u=w+1;break}e.push([E,s,o-a,p,d,o,u]),S&&(a=S,S=null),o=u}return e}});var Ep=v((Hr,Op)=>{l();"use strict";Hr.__esModule=!0;Hr.default=void 0;var Ik=be(Qs()),_a=be(Xs()),qk=be(ea()),xp=be(ra()),Rk=be(na()),Mk=be(oa()),Oa=be(ua()),Bk=be(ca()),kp=fn(ya()),Fk=be(ba()),Ea=be(xa()),Lk=be(Sa()),Nk=be(gp()),A=fn(vp()),T=fn(Ca()),$k=fn(ne()),Y=Er(),xt,Ta;function Sp(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(Sp=function(n){return n?t:e})(r)}function fn(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=Sp(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in r)if(a!=="default"&&Object.prototype.hasOwnProperty.call(r,a)){var s=n?Object.getOwnPropertyDescriptor(r,a):null;s&&(s.get||s.set)?Object.defineProperty(i,a,s):i[a]=r[a]}return i.default=r,t&&t.set(r,i),i}function be(r){return r&&r.__esModule?r:{default:r}}function Cp(r,e){for(var t=0;t0){var s=this.current.last;if(s){var o=this.convertWhitespaceNodesToSpace(a),u=o.space,c=o.rawSpace;c!==void 0&&(s.rawSpaceAfter+=c),s.spaces.after+=u}else a.forEach(function(E){return i.newNode(E)})}return}var f=this.currToken,d=void 0;n>this.position&&(d=this.parseWhitespaceEquivalentTokens(n));var p;if(this.isNamedCombinator()?p=this.namedCombinator():this.currToken[A.FIELDS.TYPE]===T.combinator?(p=new Ea.default({value:this.content(),source:Mt(this.currToken),sourceIndex:this.currToken[A.FIELDS.START_POS]}),this.position++):Pa[this.currToken[A.FIELDS.TYPE]]||d||this.unexpected(),p){if(d){var m=this.convertWhitespaceNodesToSpace(d),b=m.space,x=m.rawSpace;p.spaces.before=b,p.rawSpaceBefore=x}}else{var y=this.convertWhitespaceNodesToSpace(d,!0),w=y.space,k=y.rawSpace;k||(k=w);var S={},_={spaces:{}};w.endsWith(" ")&&k.endsWith(" ")?(S.before=w.slice(0,w.length-1),_.spaces.before=k.slice(0,k.length-1)):w.startsWith(" ")&&k.startsWith(" ")?(S.after=w.slice(1),_.spaces.after=k.slice(1)):_.value=k,p=new Ea.default({value:" ",source:Da(f,this.tokens[this.position-1]),sourceIndex:f[A.FIELDS.START_POS],spaces:S,raws:_})}return this.currToken&&this.currToken[A.FIELDS.TYPE]===T.space&&(p.spaces.after=this.optionalSpace(this.content()),this.position++),this.newNode(p)},e.comma=function(){if(this.position===this.tokens.length-1){this.root.trailingComma=!0,this.position++;return}this.current._inferEndPosition();var i=new _a.default({source:{start:Ap(this.tokens[this.position+1])}});this.current.parent.append(i),this.current=i,this.position++},e.comment=function(){var i=this.currToken;this.newNode(new xp.default({value:this.content(),source:Mt(i),sourceIndex:i[A.FIELDS.START_POS]})),this.position++},e.error=function(i,n){throw this.root.error(i,n)},e.missingBackslash=function(){return this.error("Expected a backslash preceding the semicolon.",{index:this.currToken[A.FIELDS.START_POS]})},e.missingParenthesis=function(){return this.expected("opening parenthesis",this.currToken[A.FIELDS.START_POS])},e.missingSquareBracket=function(){return this.expected("opening square bracket",this.currToken[A.FIELDS.START_POS])},e.unexpected=function(){return this.error("Unexpected '"+this.content()+"'. Escaping special characters with \\ may help.",this.currToken[A.FIELDS.START_POS])},e.unexpectedPipe=function(){return this.error("Unexpected '|'.",this.currToken[A.FIELDS.START_POS])},e.namespace=function(){var i=this.prevToken&&this.content(this.prevToken)||!0;if(this.nextToken[A.FIELDS.TYPE]===T.word)return this.position++,this.word(i);if(this.nextToken[A.FIELDS.TYPE]===T.asterisk)return this.position++,this.universal(i);this.unexpectedPipe()},e.nesting=function(){if(this.nextToken){var i=this.content(this.nextToken);if(i==="|"){this.position++;return}}var n=this.currToken;this.newNode(new Lk.default({value:this.content(),source:Mt(n),sourceIndex:n[A.FIELDS.START_POS]})),this.position++},e.parentheses=function(){var i=this.current.last,n=1;if(this.position++,i&&i.type===$k.PSEUDO){var a=new _a.default({source:{start:Ap(this.tokens[this.position-1])}}),s=this.current;for(i.append(a),this.current=a;this.position1&&i.nextToken&&i.nextToken[A.FIELDS.TYPE]===T.openParenthesis&&i.error("Misplaced parenthesis.",{index:i.nextToken[A.FIELDS.START_POS]})});else return this.expected(["pseudo-class","pseudo-element"],this.currToken[A.FIELDS.START_POS])},e.space=function(){var i=this.content();this.position===0||this.prevToken[A.FIELDS.TYPE]===T.comma||this.prevToken[A.FIELDS.TYPE]===T.openParenthesis||this.current.nodes.every(function(n){return n.type==="comment"})?(this.spaces=this.optionalSpace(i),this.position++):this.position===this.tokens.length-1||this.nextToken[A.FIELDS.TYPE]===T.comma||this.nextToken[A.FIELDS.TYPE]===T.closeParenthesis?(this.current.last.spaces.after=this.optionalSpace(i),this.position++):this.combinator()},e.string=function(){var i=this.currToken;this.newNode(new Oa.default({value:this.content(),source:Mt(i),sourceIndex:i[A.FIELDS.START_POS]})),this.position++},e.universal=function(i){var n=this.nextToken;if(n&&this.content(n)==="|")return this.position++,this.namespace();var a=this.currToken;this.newNode(new Fk.default({value:this.content(),source:Mt(a),sourceIndex:a[A.FIELDS.START_POS]}),i),this.position++},e.splitWord=function(i,n){for(var a=this,s=this.nextToken,o=this.content();s&&~[T.dollar,T.caret,T.equals,T.word].indexOf(s[A.FIELDS.TYPE]);){this.position++;var u=this.content();if(o+=u,u.lastIndexOf("\\")===u.length-1){var c=this.nextToken;c&&c[A.FIELDS.TYPE]===T.space&&(o+=this.requiredSpace(this.content(c)),this.position++)}s=this.nextToken}var f=Ia(o,".").filter(function(b){var x=o[b-1]==="\\",y=/^\d+\.\d+%$/.test(o);return!x&&!y}),d=Ia(o,"#").filter(function(b){return o[b-1]!=="\\"}),p=Ia(o,"#{");p.length&&(d=d.filter(function(b){return!~p.indexOf(b)}));var m=(0,Nk.default)(Vk([0].concat(f,d)));m.forEach(function(b,x){var y=m[x+1]||o.length,w=o.slice(b,y);if(x===0&&n)return n.call(a,w,m.length);var k,S=a.currToken,_=S[A.FIELDS.START_POS]+m[x],E=kt(S[1],S[2]+b,S[3],S[2]+(y-1));if(~f.indexOf(b)){var I={value:w.slice(1),source:E,sourceIndex:_};k=new qk.default(Bt(I,"value"))}else if(~d.indexOf(b)){var q={value:w.slice(1),source:E,sourceIndex:_};k=new Rk.default(Bt(q,"value"))}else{var R={value:w,source:E,sourceIndex:_};Bt(R,"value"),k=new Mk.default(R)}a.newNode(k,i),i=null}),this.position++},e.word=function(i){var n=this.nextToken;return n&&this.content(n)==="|"?(this.position++,this.namespace()):this.splitWord(i)},e.loop=function(){for(;this.position{l();"use strict";Yr.__esModule=!0;Yr.default=void 0;var Wk=Gk(Ep());function Gk(r){return r&&r.__esModule?r:{default:r}}var Hk=function(){function r(t,i){this.func=t||function(){},this.funcRes=null,this.options=i}var e=r.prototype;return e._shouldUpdateSelector=function(i,n){n===void 0&&(n={});var a=Object.assign({},this.options,n);return a.updateSelector===!1?!1:typeof i!="string"},e._isLossy=function(i){i===void 0&&(i={});var n=Object.assign({},this.options,i);return n.lossless===!1},e._root=function(i,n){n===void 0&&(n={});var a=new Wk.default(i,this._parseOptions(n));return a.root},e._parseOptions=function(i){return{lossy:this._isLossy(i)}},e._run=function(i,n){var a=this;return n===void 0&&(n={}),new Promise(function(s,o){try{var u=a._root(i,n);Promise.resolve(a.func(u)).then(function(c){var f=void 0;return a._shouldUpdateSelector(i,n)&&(f=u.toString(),i.selector=f),{transform:c,root:u,string:f}}).then(s,o)}catch(c){o(c);return}})},e._runSync=function(i,n){n===void 0&&(n={});var a=this._root(i,n),s=this.func(a);if(s&&typeof s.then=="function")throw new Error("Selector processor returned a promise to a synchronous call.");var o=void 0;return n.updateSelector&&typeof i!="string"&&(o=a.toString(),i.selector=o),{transform:s,root:a,string:o}},e.ast=function(i,n){return this._run(i,n).then(function(a){return a.root})},e.astSync=function(i,n){return this._runSync(i,n).root},e.transform=function(i,n){return this._run(i,n).then(function(a){return a.transform})},e.transformSync=function(i,n){return this._runSync(i,n).transform},e.process=function(i,n){return this._run(i,n).then(function(a){return a.string||a.root.toString()})},e.processSync=function(i,n){var a=this._runSync(i,n);return a.string||a.root.toString()},r}();Yr.default=Hk;Tp.exports=Yr.default});var Dp=v(G=>{l();"use strict";G.__esModule=!0;G.universal=G.tag=G.string=G.selector=G.root=G.pseudo=G.nesting=G.id=G.comment=G.combinator=G.className=G.attribute=void 0;var Yk=ve(ya()),Qk=ve(ea()),Jk=ve(xa()),Xk=ve(ra()),Kk=ve(na()),Zk=ve(Sa()),eS=ve(ca()),tS=ve(Qs()),rS=ve(Xs()),iS=ve(ua()),nS=ve(oa()),sS=ve(ba());function ve(r){return r&&r.__esModule?r:{default:r}}var aS=function(e){return new Yk.default(e)};G.attribute=aS;var oS=function(e){return new Qk.default(e)};G.className=oS;var lS=function(e){return new Jk.default(e)};G.combinator=lS;var uS=function(e){return new Xk.default(e)};G.comment=uS;var fS=function(e){return new Kk.default(e)};G.id=fS;var cS=function(e){return new Zk.default(e)};G.nesting=cS;var pS=function(e){return new eS.default(e)};G.pseudo=pS;var dS=function(e){return new tS.default(e)};G.root=dS;var hS=function(e){return new rS.default(e)};G.selector=hS;var mS=function(e){return new iS.default(e)};G.string=mS;var gS=function(e){return new nS.default(e)};G.tag=gS;var yS=function(e){return new sS.default(e)};G.universal=yS});var Mp=v($=>{l();"use strict";$.__esModule=!0;$.isComment=$.isCombinator=$.isClassName=$.isAttribute=void 0;$.isContainer=TS;$.isIdentifier=void 0;$.isNamespace=PS;$.isNesting=void 0;$.isNode=qa;$.isPseudo=void 0;$.isPseudoClass=ES;$.isPseudoElement=Rp;$.isUniversal=$.isTag=$.isString=$.isSelector=$.isRoot=void 0;var Q=ne(),fe,wS=(fe={},fe[Q.ATTRIBUTE]=!0,fe[Q.CLASS]=!0,fe[Q.COMBINATOR]=!0,fe[Q.COMMENT]=!0,fe[Q.ID]=!0,fe[Q.NESTING]=!0,fe[Q.PSEUDO]=!0,fe[Q.ROOT]=!0,fe[Q.SELECTOR]=!0,fe[Q.STRING]=!0,fe[Q.TAG]=!0,fe[Q.UNIVERSAL]=!0,fe);function qa(r){return typeof r=="object"&&wS[r.type]}function xe(r,e){return qa(e)&&e.type===r}var Ip=xe.bind(null,Q.ATTRIBUTE);$.isAttribute=Ip;var bS=xe.bind(null,Q.CLASS);$.isClassName=bS;var vS=xe.bind(null,Q.COMBINATOR);$.isCombinator=vS;var xS=xe.bind(null,Q.COMMENT);$.isComment=xS;var kS=xe.bind(null,Q.ID);$.isIdentifier=kS;var SS=xe.bind(null,Q.NESTING);$.isNesting=SS;var Ra=xe.bind(null,Q.PSEUDO);$.isPseudo=Ra;var CS=xe.bind(null,Q.ROOT);$.isRoot=CS;var AS=xe.bind(null,Q.SELECTOR);$.isSelector=AS;var _S=xe.bind(null,Q.STRING);$.isString=_S;var qp=xe.bind(null,Q.TAG);$.isTag=qp;var OS=xe.bind(null,Q.UNIVERSAL);$.isUniversal=OS;function Rp(r){return Ra(r)&&r.value&&(r.value.startsWith("::")||r.value.toLowerCase()===":before"||r.value.toLowerCase()===":after"||r.value.toLowerCase()===":first-letter"||r.value.toLowerCase()===":first-line")}function ES(r){return Ra(r)&&!Rp(r)}function TS(r){return!!(qa(r)&&r.walk)}function PS(r){return Ip(r)||qp(r)}});var Bp=v(Ee=>{l();"use strict";Ee.__esModule=!0;var Ma=ne();Object.keys(Ma).forEach(function(r){r==="default"||r==="__esModule"||r in Ee&&Ee[r]===Ma[r]||(Ee[r]=Ma[r])});var Ba=Dp();Object.keys(Ba).forEach(function(r){r==="default"||r==="__esModule"||r in Ee&&Ee[r]===Ba[r]||(Ee[r]=Ba[r])});var Fa=Mp();Object.keys(Fa).forEach(function(r){r==="default"||r==="__esModule"||r in Ee&&Ee[r]===Fa[r]||(Ee[r]=Fa[r])})});var Me=v((Qr,Lp)=>{l();"use strict";Qr.__esModule=!0;Qr.default=void 0;var DS=RS(Pp()),IS=qS(Bp());function Fp(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(Fp=function(n){return n?t:e})(r)}function qS(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=Fp(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in r)if(a!=="default"&&Object.prototype.hasOwnProperty.call(r,a)){var s=n?Object.getOwnPropertyDescriptor(r,a):null;s&&(s.get||s.set)?Object.defineProperty(i,a,s):i[a]=r[a]}return i.default=r,t&&t.set(r,i),i}function RS(r){return r&&r.__esModule?r:{default:r}}var La=function(e){return new DS.default(e)};Object.assign(La,IS);delete La.__esModule;var MS=La;Qr.default=MS;Lp.exports=Qr.default});function Ge(r){return["fontSize","outline"].includes(r)?e=>(typeof e=="function"&&(e=e({})),Array.isArray(e)&&(e=e[0]),e):r==="fontFamily"?e=>{typeof e=="function"&&(e=e({}));let t=Array.isArray(e)&&ie(e[1])?e[0]:e;return Array.isArray(t)?t.join(", "):t}:["boxShadow","transitionProperty","transitionDuration","transitionDelay","transitionTimingFunction","backgroundImage","backgroundSize","backgroundColor","cursor","animation"].includes(r)?e=>(typeof e=="function"&&(e=e({})),Array.isArray(e)&&(e=e.join(", ")),e):["gridTemplateColumns","gridTemplateRows","objectPosition"].includes(r)?e=>(typeof e=="function"&&(e=e({})),typeof e=="string"&&(e=j.list.comma(e).join(" ")),e):(e,t={})=>(typeof e=="function"&&(e=e(t)),e)}var Jr=C(()=>{l();st();At()});var Wp=v((PT,Va)=>{l();var{Rule:Np,AtRule:BS}=ge(),$p=Me();function Na(r,e){let t;try{$p(i=>{t=i}).processSync(r)}catch(i){throw r.includes(":")?e?e.error("Missed semicolon"):i:e?e.error(i.message):i}return t.at(0)}function zp(r,e){let t=!1;return r.each(i=>{if(i.type==="nesting"){let n=e.clone({});i.value!=="&"?i.replaceWith(Na(i.value.replace("&",n.toString()))):i.replaceWith(n),t=!0}else"nodes"in i&&i.nodes&&zp(i,e)&&(t=!0)}),t}function jp(r,e){let t=[];return r.selectors.forEach(i=>{let n=Na(i,r);e.selectors.forEach(a=>{if(!a)return;let s=Na(a,e);zp(s,n)||(s.prepend($p.combinator({value:" "})),s.prepend(n.clone({}))),t.push(s.toString())})}),t}function cn(r,e){let t=r.prev();for(e.after(r);t&&t.type==="comment";){let i=t.prev();e.after(t),t=i}return r}function FS(r){return function e(t,i,n,a=n){let s=[];if(i.each(o=>{o.type==="rule"&&n?a&&(o.selectors=jp(t,o)):o.type==="atrule"&&o.nodes?r[o.name]?e(t,o,a):i[za]!==!1&&s.push(o):s.push(o)}),n&&s.length){let o=t.clone({nodes:[]});for(let u of s)o.append(u);i.prepend(o)}}}function $a(r,e,t){let i=new Np({selector:r,nodes:[]});return i.append(e),t.after(i),i}function Vp(r,e){let t={};for(let i of r)t[i]=!0;if(e)for(let i of e)t[i.replace(/^@/,"")]=!0;return t}function LS(r){r=r.trim();let e=r.match(/^\((.*)\)$/);if(!e)return{type:"basic",selector:r};let t=e[1].match(/^(with(?:out)?):(.+)$/);if(t){let i=t[1]==="with",n=Object.fromEntries(t[2].trim().split(/\s+/).map(s=>[s,!0]));if(i&&n.all)return{type:"noop"};let a=s=>!!n[s];return n.all?a=()=>!0:i&&(a=s=>s==="all"?!1:!n[s]),{type:"withrules",escapes:a}}return{type:"unknown"}}function NS(r){let e=[],t=r.parent;for(;t&&t instanceof BS;)e.push(t),t=t.parent;return e}function $S(r){let e=r[Up];if(!e)r.after(r.nodes);else{let t=r.nodes,i,n=-1,a,s,o,u=NS(r);if(u.forEach((c,f)=>{if(e(c.name))i=c,n=f,s=o;else{let d=o;o=c.clone({nodes:[]}),d&&o.append(d),a=a||o}}),i?s?(a.append(t),i.after(s)):i.after(t):r.after(t),r.next()&&i){let c;u.slice(0,n+1).forEach((f,d,p)=>{let m=c;c=f.clone({nodes:[]}),m&&c.append(m);let b=[],y=(p[d-1]||r).next();for(;y;)b.push(y),y=y.next();c.append(b)}),c&&(s||t[t.length-1]).after(c)}}r.remove()}var za=Symbol("rootRuleMergeSel"),Up=Symbol("rootRuleEscapes");function zS(r){let{params:e}=r,{type:t,selector:i,escapes:n}=LS(e);if(t==="unknown")throw r.error(`Unknown @${r.name} parameter ${JSON.stringify(e)}`);if(t==="basic"&&i){let a=new Np({selector:i,nodes:r.nodes});r.removeAll(),r.append(a)}r[Up]=n,r[za]=n?!n("all"):t==="noop"}var ja=Symbol("hasRootRule");Va.exports=(r={})=>{let e=Vp(["media","supports","layer","container"],r.bubble),t=FS(e),i=Vp(["document","font-face","keyframes","-webkit-keyframes","-moz-keyframes"],r.unwrap),n=(r.rootRuleName||"at-root").replace(/^@/,""),a=r.preserveEmpty;return{postcssPlugin:"postcss-nested",Once(s){s.walkAtRules(n,o=>{zS(o),s[ja]=!0})},Rule(s){let o=!1,u=s,c=!1,f=[];s.each(d=>{d.type==="rule"?(f.length&&(u=$a(s.selector,f,u),f=[]),c=!0,o=!0,d.selectors=jp(s,d),u=cn(d,u)):d.type==="atrule"?(f.length&&(u=$a(s.selector,f,u),f=[]),d.name===n?(o=!0,t(s,d,!0,d[za]),u=cn(d,u)):e[d.name]?(c=!0,o=!0,t(s,d,!0),u=cn(d,u)):i[d.name]?(c=!0,o=!0,t(s,d,!1),u=cn(d,u)):c&&f.push(d)):d.type==="decl"&&c&&f.push(d)}),f.length&&(u=$a(s.selector,f,u)),o&&a!==!0&&(s.raws.semicolon=!0,s.nodes.length===0&&s.remove())},RootExit(s){s[ja]&&(s.walkAtRules(n,$S),s[ja]=!1)}}};Va.exports.postcss=!0});var Qp=v((DT,Yp)=>{l();"use strict";var Gp=/-(\w|$)/g,Hp=(r,e)=>e.toUpperCase(),jS=r=>(r=r.toLowerCase(),r==="float"?"cssFloat":r.startsWith("-ms-")?r.substr(1).replace(Gp,Hp):r.replace(Gp,Hp));Yp.exports=jS});var Ga=v((IT,Jp)=>{l();var VS=Qp(),US={boxFlex:!0,boxFlexGroup:!0,columnCount:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,strokeDashoffset:!0,strokeOpacity:!0,strokeWidth:!0};function Ua(r){return typeof r.nodes=="undefined"?!0:Wa(r)}function Wa(r){let e,t={};return r.each(i=>{if(i.type==="atrule")e="@"+i.name,i.params&&(e+=" "+i.params),typeof t[e]=="undefined"?t[e]=Ua(i):Array.isArray(t[e])?t[e].push(Ua(i)):t[e]=[t[e],Ua(i)];else if(i.type==="rule"){let n=Wa(i);if(t[i.selector])for(let a in n)t[i.selector][a]=n[a];else t[i.selector]=n}else if(i.type==="decl"){i.prop[0]==="-"&&i.prop[1]==="-"||i.parent&&i.parent.selector===":export"?e=i.prop:e=VS(i.prop);let n=i.value;!isNaN(i.value)&&US[e]&&(n=parseFloat(i.value)),i.important&&(n+=" !important"),typeof t[e]=="undefined"?t[e]=n:Array.isArray(t[e])?t[e].push(n):t[e]=[t[e],n]}}),t}Jp.exports=Wa});var pn=v((qT,ed)=>{l();var Xr=ge(),Xp=/\s*!important\s*$/i,WS={"box-flex":!0,"box-flex-group":!0,"column-count":!0,flex:!0,"flex-grow":!0,"flex-positive":!0,"flex-shrink":!0,"flex-negative":!0,"font-weight":!0,"line-clamp":!0,"line-height":!0,opacity:!0,order:!0,orphans:!0,"tab-size":!0,widows:!0,"z-index":!0,zoom:!0,"fill-opacity":!0,"stroke-dashoffset":!0,"stroke-opacity":!0,"stroke-width":!0};function GS(r){return r.replace(/([A-Z])/g,"-$1").replace(/^ms-/,"-ms-").toLowerCase()}function Kp(r,e,t){t===!1||t===null||(e.startsWith("--")||(e=GS(e)),typeof t=="number"&&(t===0||WS[e]?t=t.toString():t+="px"),e==="css-float"&&(e="float"),Xp.test(t)?(t=t.replace(Xp,""),r.push(Xr.decl({prop:e,value:t,important:!0}))):r.push(Xr.decl({prop:e,value:t})))}function Zp(r,e,t){let i=Xr.atRule({name:e[1],params:e[3]||""});typeof t=="object"&&(i.nodes=[],Ha(t,i)),r.push(i)}function Ha(r,e){let t,i,n;for(t in r)if(i=r[t],!(i===null||typeof i=="undefined"))if(t[0]==="@"){let a=t.match(/@(\S+)(\s+([\W\w]*)\s*)?/);if(Array.isArray(i))for(let s of i)Zp(e,a,s);else Zp(e,a,i)}else if(Array.isArray(i))for(let a of i)Kp(e,t,a);else typeof i=="object"?(n=Xr.rule({selector:t}),Ha(i,n),e.push(n)):Kp(e,t,i)}ed.exports=function(r){let e=Xr.root();return Ha(r,e),e}});var Ya=v((RT,td)=>{l();var HS=Ga();td.exports=function(e){return console&&console.warn&&e.warnings().forEach(t=>{let i=t.plugin||"PostCSS";console.warn(i+": "+t.text)}),HS(e.root)}});var id=v((MT,rd)=>{l();var YS=ge(),QS=Ya(),JS=pn();rd.exports=function(e){let t=YS(e);return async i=>{let n=await t.process(i,{parser:JS,from:void 0});return QS(n)}}});var sd=v((BT,nd)=>{l();var XS=ge(),KS=Ya(),ZS=pn();nd.exports=function(r){let e=XS(r);return t=>{let i=e.process(t,{parser:ZS,from:void 0});return KS(i)}}});var od=v((FT,ad)=>{l();var e2=Ga(),t2=pn(),r2=id(),i2=sd();ad.exports={objectify:e2,parse:t2,async:r2,sync:i2}});var Ft,ld,LT,NT,$T,zT,ud=C(()=>{l();Ft=X(od()),ld=Ft.default,LT=Ft.default.objectify,NT=Ft.default.parse,$T=Ft.default.async,zT=Ft.default.sync});function Lt(r){return Array.isArray(r)?r.flatMap(e=>j([(0,fd.default)({bubble:["screen"]})]).process(e,{parser:ld}).root.nodes):Lt([r])}var fd,Qa=C(()=>{l();st();fd=X(Wp());ud()});function Nt(r,e,t=!1){if(r==="")return e;let i=typeof e=="string"?(0,cd.default)().astSync(e):e;return i.walkClasses(n=>{let a=n.value,s=t&&a.startsWith("-");n.value=s?`-${r}${a.slice(1)}`:`${r}${a}`}),typeof e=="string"?i.toString():i}var cd,dn=C(()=>{l();cd=X(Me())});function ce(r){let e=pd.default.className();return e.value=r,wt(e?.raws?.value??e.value)}var pd,$t=C(()=>{l();pd=X(Me());bi()});function Ja(r){return wt(`.${ce(r)}`)}function hn(r,e){return Ja(Kr(r,e))}function Kr(r,e){return e==="DEFAULT"?r:e==="-"||e==="-DEFAULT"?`-${r}`:e.startsWith("-")?`-${r}${e}`:e.startsWith("/")?`${r}${e}`:`${r}-${e}`}var Xa=C(()=>{l();$t();bi()});function P(r,e=[[r,[r]]],{filterDefault:t=!1,...i}={}){let n=Ge(r);return function({matchUtilities:a,theme:s}){for(let o of e){let u=Array.isArray(o[0])?o:[o];a(u.reduce((c,[f,d])=>Object.assign(c,{[f]:p=>d.reduce((m,b)=>Array.isArray(b)?Object.assign(m,{[b[0]]:b[1]}):Object.assign(m,{[b]:n(p)}),{})}),{}),{...i,values:t?Object.fromEntries(Object.entries(s(r)??{}).filter(([c])=>c!=="DEFAULT")):s(r)})}}}var dd=C(()=>{l();Jr()});function at(r){return r=Array.isArray(r)?r:[r],r.map(e=>{let t=e.values.map(i=>i.raw!==void 0?i.raw:[i.min&&`(min-width: ${i.min})`,i.max&&`(max-width: ${i.max})`].filter(Boolean).join(" and "));return e.not?`not all and ${t}`:t}).join(", ")}var mn=C(()=>{l()});function Ka(r){return r.split(f2).map(t=>{let i=t.trim(),n={value:i},a=i.split(c2),s=new Set;for(let o of a)!s.has("DIRECTIONS")&&n2.has(o)?(n.direction=o,s.add("DIRECTIONS")):!s.has("PLAY_STATES")&&s2.has(o)?(n.playState=o,s.add("PLAY_STATES")):!s.has("FILL_MODES")&&a2.has(o)?(n.fillMode=o,s.add("FILL_MODES")):!s.has("ITERATION_COUNTS")&&(o2.has(o)||p2.test(o))?(n.iterationCount=o,s.add("ITERATION_COUNTS")):!s.has("TIMING_FUNCTION")&&l2.has(o)||!s.has("TIMING_FUNCTION")&&u2.some(u=>o.startsWith(`${u}(`))?(n.timingFunction=o,s.add("TIMING_FUNCTION")):!s.has("DURATION")&&hd.test(o)?(n.duration=o,s.add("DURATION")):!s.has("DELAY")&&hd.test(o)?(n.delay=o,s.add("DELAY")):s.has("NAME")?(n.unknown||(n.unknown=[]),n.unknown.push(o)):(n.name=o,s.add("NAME"));return n})}var n2,s2,a2,o2,l2,u2,f2,c2,hd,p2,md=C(()=>{l();n2=new Set(["normal","reverse","alternate","alternate-reverse"]),s2=new Set(["running","paused"]),a2=new Set(["none","forwards","backwards","both"]),o2=new Set(["infinite"]),l2=new Set(["linear","ease","ease-in","ease-out","ease-in-out","step-start","step-end"]),u2=["cubic-bezier","steps"],f2=/\,(?![^(]*\))/g,c2=/\ +(?![^(]*\))/g,hd=/^(-?[\d.]+m?s)$/,p2=/^(\d+)$/});var gd,re,yd=C(()=>{l();gd=r=>Object.assign({},...Object.entries(r??{}).flatMap(([e,t])=>typeof t=="object"?Object.entries(gd(t)).map(([i,n])=>({[e+(i==="DEFAULT"?"":`-${i}`)]:n})):[{[`${e}`]:t}])),re=gd});var bd,wd=C(()=>{bd="3.4.3"});function ot(r,e=!0){return Array.isArray(r)?r.map(t=>{if(e&&Array.isArray(t))throw new Error("The tuple syntax is not supported for `screens`.");if(typeof t=="string")return{name:t.toString(),not:!1,values:[{min:t,max:void 0}]};let[i,n]=t;return i=i.toString(),typeof n=="string"?{name:i,not:!1,values:[{min:n,max:void 0}]}:Array.isArray(n)?{name:i,not:!1,values:n.map(a=>xd(a))}:{name:i,not:!1,values:[xd(n)]}}):ot(Object.entries(r??{}),!1)}function gn(r){return r.values.length!==1?{result:!1,reason:"multiple-values"}:r.values[0].raw!==void 0?{result:!1,reason:"raw-values"}:r.values[0].min!==void 0&&r.values[0].max!==void 0?{result:!1,reason:"min-and-max"}:{result:!0,reason:null}}function vd(r,e,t){let i=yn(e,r),n=yn(t,r),a=gn(i),s=gn(n);if(a.reason==="multiple-values"||s.reason==="multiple-values")throw new Error("Attempted to sort a screen with multiple values. This should never happen. Please open a bug report.");if(a.reason==="raw-values"||s.reason==="raw-values")throw new Error("Attempted to sort a screen with raw values. This should never happen. Please open a bug report.");if(a.reason==="min-and-max"||s.reason==="min-and-max")throw new Error("Attempted to sort a screen with both min and max values. This should never happen. Please open a bug report.");let{min:o,max:u}=i.values[0],{min:c,max:f}=n.values[0];e.not&&([o,u]=[u,o]),t.not&&([c,f]=[f,c]),o=o===void 0?o:parseFloat(o),u=u===void 0?u:parseFloat(u),c=c===void 0?c:parseFloat(c),f=f===void 0?f:parseFloat(f);let[d,p]=r==="min"?[o,c]:[f,u];return d-p}function yn(r,e){return typeof r=="object"?r:{name:"arbitrary-screen",values:[{[e]:r}]}}function xd({"min-width":r,min:e=r,max:t,raw:i}={}){return{min:e,max:t,raw:i}}var wn=C(()=>{l()});function bn(r,e){r.walkDecls(t=>{if(e.includes(t.prop)){t.remove();return}for(let i of e)t.value.includes(`/ var(${i})`)&&(t.value=t.value.replace(`/ var(${i})`,""))})}var kd=C(()=>{l()});var H,Te,Be,Fe,Sd,Cd=C(()=>{l();ze();bt();st();dd();mn();$t();md();yd();fr();ws();At();Jr();wd();Oe();wn();cs();kd();je();dr();ei();H={childVariant:({addVariant:r})=>{r("*","& > *")},pseudoElementVariants:({addVariant:r})=>{r("first-letter","&::first-letter"),r("first-line","&::first-line"),r("marker",[({container:e})=>(bn(e,["--tw-text-opacity"]),"& *::marker"),({container:e})=>(bn(e,["--tw-text-opacity"]),"&::marker")]),r("selection",["& *::selection","&::selection"]),r("file","&::file-selector-button"),r("placeholder","&::placeholder"),r("backdrop","&::backdrop"),r("before",({container:e})=>(e.walkRules(t=>{let i=!1;t.walkDecls("content",()=>{i=!0}),i||t.prepend(j.decl({prop:"content",value:"var(--tw-content)"}))}),"&::before")),r("after",({container:e})=>(e.walkRules(t=>{let i=!1;t.walkDecls("content",()=>{i=!0}),i||t.prepend(j.decl({prop:"content",value:"var(--tw-content)"}))}),"&::after"))},pseudoClassVariants:({addVariant:r,matchVariant:e,config:t,prefix:i})=>{let n=[["first","&:first-child"],["last","&:last-child"],["only","&:only-child"],["odd","&:nth-child(odd)"],["even","&:nth-child(even)"],"first-of-type","last-of-type","only-of-type",["visited",({container:s})=>(bn(s,["--tw-text-opacity","--tw-border-opacity","--tw-bg-opacity"]),"&:visited")],"target",["open","&[open]"],"default","checked","indeterminate","placeholder-shown","autofill","optional","required","valid","invalid","in-range","out-of-range","read-only","empty","focus-within",["hover",K(t(),"hoverOnlyWhenSupported")?"@media (hover: hover) and (pointer: fine) { &:hover }":"&:hover"],"focus","focus-visible","active","enabled","disabled"].map(s=>Array.isArray(s)?s:[s,`&:${s}`]);for(let[s,o]of n)r(s,u=>typeof o=="function"?o(u):o);let a={group:(s,{modifier:o})=>o?[`:merge(${i(".group")}\\/${ce(o)})`," &"]:[`:merge(${i(".group")})`," &"],peer:(s,{modifier:o})=>o?[`:merge(${i(".peer")}\\/${ce(o)})`," ~ &"]:[`:merge(${i(".peer")})`," ~ &"]};for(let[s,o]of Object.entries(a))e(s,(u="",c)=>{let f=L(typeof u=="function"?u(c):u);f.includes("&")||(f="&"+f);let[d,p]=o("",c),m=null,b=null,x=0;for(let y=0;y{r("ltr",'&:where([dir="ltr"], [dir="ltr"] *)'),r("rtl",'&:where([dir="rtl"], [dir="rtl"] *)')},reducedMotionVariants:({addVariant:r})=>{r("motion-safe","@media (prefers-reduced-motion: no-preference)"),r("motion-reduce","@media (prefers-reduced-motion: reduce)")},darkVariants:({config:r,addVariant:e})=>{let[t,i=".dark"]=[].concat(r("darkMode","media"));if(t===!1&&(t="media",F.warn("darkmode-false",["The `darkMode` option in your Tailwind CSS configuration is set to `false`, which now behaves the same as `media`.","Change `darkMode` to `media` or remove it entirely.","https://tailwindcss.com/docs/upgrade-guide#remove-dark-mode-configuration"])),t==="variant"){let n;if(Array.isArray(i)||typeof i=="function"?n=i:typeof i=="string"&&(n=[i]),Array.isArray(n))for(let a of n)a===".dark"?(t=!1,F.warn("darkmode-variant-without-selector",["When using `variant` for `darkMode`, you must provide a selector.",'Example: `darkMode: ["variant", ".your-selector &"]`'])):a.includes("&")||(t=!1,F.warn("darkmode-variant-without-ampersand",["When using `variant` for `darkMode`, your selector must contain `&`.",'Example `darkMode: ["variant", ".your-selector &"]`']));i=n}t==="selector"?e("dark",`&:where(${i}, ${i} *)`):t==="media"?e("dark","@media (prefers-color-scheme: dark)"):t==="variant"?e("dark",i):t==="class"&&e("dark",`&:is(${i} *)`)},printVariant:({addVariant:r})=>{r("print","@media print")},screenVariants:({theme:r,addVariant:e,matchVariant:t})=>{let i=r("screens")??{},n=Object.values(i).every(w=>typeof w=="string"),a=ot(r("screens")),s=new Set([]);function o(w){return w.match(/(\D+)$/)?.[1]??"(none)"}function u(w){w!==void 0&&s.add(o(w))}function c(w){return u(w),s.size===1}for(let w of a)for(let k of w.values)u(k.min),u(k.max);let f=s.size<=1;function d(w){return Object.fromEntries(a.filter(k=>gn(k).result).map(k=>{let{min:S,max:_}=k.values[0];if(w==="min"&&S!==void 0)return k;if(w==="min"&&_!==void 0)return{...k,not:!k.not};if(w==="max"&&_!==void 0)return k;if(w==="max"&&S!==void 0)return{...k,not:!k.not}}).map(k=>[k.name,k]))}function p(w){return(k,S)=>vd(w,k.value,S.value)}let m=p("max"),b=p("min");function x(w){return k=>{if(n)if(f){if(typeof k=="string"&&!c(k))return F.warn("minmax-have-mixed-units",["The `min-*` and `max-*` variants are not supported with a `screens` configuration containing mixed units."]),[]}else return F.warn("mixed-screen-units",["The `min-*` and `max-*` variants are not supported with a `screens` configuration containing mixed units."]),[];else return F.warn("complex-screen-config",["The `min-*` and `max-*` variants are not supported with a `screens` configuration containing objects."]),[];return[`@media ${at(yn(k,w))}`]}}t("max",x("max"),{sort:m,values:n?d("max"):{}});let y="min-screens";for(let w of a)e(w.name,`@media ${at(w)}`,{id:y,sort:n&&f?b:void 0,value:w});t("min",x("min"),{id:y,sort:b})},supportsVariants:({matchVariant:r,theme:e})=>{r("supports",(t="")=>{let i=L(t),n=/^\w*\s*\(/.test(i);return i=n?i.replace(/\b(and|or|not)\b/g," $1 "):i,n?`@supports ${i}`:(i.includes(":")||(i=`${i}: var(--tw)`),i.startsWith("(")&&i.endsWith(")")||(i=`(${i})`),`@supports ${i}`)},{values:e("supports")??{}})},hasVariants:({matchVariant:r})=>{r("has",e=>`&:has(${L(e)})`,{values:{}}),r("group-has",(e,{modifier:t})=>t?`:merge(.group\\/${t}):has(${L(e)}) &`:`:merge(.group):has(${L(e)}) &`,{values:{}}),r("peer-has",(e,{modifier:t})=>t?`:merge(.peer\\/${t}):has(${L(e)}) ~ &`:`:merge(.peer):has(${L(e)}) ~ &`,{values:{}})},ariaVariants:({matchVariant:r,theme:e})=>{r("aria",t=>`&[aria-${L(t)}]`,{values:e("aria")??{}}),r("group-aria",(t,{modifier:i})=>i?`:merge(.group\\/${i})[aria-${L(t)}] &`:`:merge(.group)[aria-${L(t)}] &`,{values:e("aria")??{}}),r("peer-aria",(t,{modifier:i})=>i?`:merge(.peer\\/${i})[aria-${L(t)}] ~ &`:`:merge(.peer)[aria-${L(t)}] ~ &`,{values:e("aria")??{}})},dataVariants:({matchVariant:r,theme:e})=>{r("data",t=>`&[data-${L(t)}]`,{values:e("data")??{}}),r("group-data",(t,{modifier:i})=>i?`:merge(.group\\/${i})[data-${L(t)}] &`:`:merge(.group)[data-${L(t)}] &`,{values:e("data")??{}}),r("peer-data",(t,{modifier:i})=>i?`:merge(.peer\\/${i})[data-${L(t)}] ~ &`:`:merge(.peer)[data-${L(t)}] ~ &`,{values:e("data")??{}})},orientationVariants:({addVariant:r})=>{r("portrait","@media (orientation: portrait)"),r("landscape","@media (orientation: landscape)")},prefersContrastVariants:({addVariant:r})=>{r("contrast-more","@media (prefers-contrast: more)"),r("contrast-less","@media (prefers-contrast: less)")},forcedColorsVariants:({addVariant:r})=>{r("forced-colors","@media (forced-colors: active)")}},Te=["translate(var(--tw-translate-x), var(--tw-translate-y))","rotate(var(--tw-rotate))","skewX(var(--tw-skew-x))","skewY(var(--tw-skew-y))","scaleX(var(--tw-scale-x))","scaleY(var(--tw-scale-y))"].join(" "),Be=["var(--tw-blur)","var(--tw-brightness)","var(--tw-contrast)","var(--tw-grayscale)","var(--tw-hue-rotate)","var(--tw-invert)","var(--tw-saturate)","var(--tw-sepia)","var(--tw-drop-shadow)"].join(" "),Fe=["var(--tw-backdrop-blur)","var(--tw-backdrop-brightness)","var(--tw-backdrop-contrast)","var(--tw-backdrop-grayscale)","var(--tw-backdrop-hue-rotate)","var(--tw-backdrop-invert)","var(--tw-backdrop-opacity)","var(--tw-backdrop-saturate)","var(--tw-backdrop-sepia)"].join(" "),Sd={preflight:({addBase:r})=>{let e=j.parse(`*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:theme('borderColor.DEFAULT', currentColor)}::after,::before{--tw-content:''}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:theme('fontFamily.sans', ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:theme('fontFamily.sans[1].fontFeatureSettings', normal);font-variation-settings:theme('fontFamily.sans[1].fontVariationSettings', normal);-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:theme('fontFamily.mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:theme('fontFamily.mono[1].fontFeatureSettings', normal);font-variation-settings:theme('fontFamily.mono[1].fontVariationSettings', normal);font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:theme('colors.gray.4', #9ca3af)}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}`);r([j.comment({text:`! tailwindcss v${bd} | MIT License | https://tailwindcss.com`}),...e.nodes])},container:(()=>{function r(t=[]){return t.flatMap(i=>i.values.map(n=>n.min)).filter(i=>i!==void 0)}function e(t,i,n){if(typeof n=="undefined")return[];if(!(typeof n=="object"&&n!==null))return[{screen:"DEFAULT",minWidth:0,padding:n}];let a=[];n.DEFAULT&&a.push({screen:"DEFAULT",minWidth:0,padding:n.DEFAULT});for(let s of t)for(let o of i)for(let{min:u}of o.values)u===s&&a.push({minWidth:s,padding:n[o.name]});return a}return function({addComponents:t,theme:i}){let n=ot(i("container.screens",i("screens"))),a=r(n),s=e(a,n,i("container.padding")),o=c=>{let f=s.find(d=>d.minWidth===c);return f?{paddingRight:f.padding,paddingLeft:f.padding}:{}},u=Array.from(new Set(a.slice().sort((c,f)=>parseInt(c)-parseInt(f)))).map(c=>({[`@media (min-width: ${c})`]:{".container":{"max-width":c,...o(c)}}}));t([{".container":Object.assign({width:"100%"},i("container.center",!1)?{marginRight:"auto",marginLeft:"auto"}:{},o(0))},...u])}})(),accessibility:({addUtilities:r})=>{r({".sr-only":{position:"absolute",width:"1px",height:"1px",padding:"0",margin:"-1px",overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",borderWidth:"0"},".not-sr-only":{position:"static",width:"auto",height:"auto",padding:"0",margin:"0",overflow:"visible",clip:"auto",whiteSpace:"normal"}})},pointerEvents:({addUtilities:r})=>{r({".pointer-events-none":{"pointer-events":"none"},".pointer-events-auto":{"pointer-events":"auto"}})},visibility:({addUtilities:r})=>{r({".visible":{visibility:"visible"},".invisible":{visibility:"hidden"},".collapse":{visibility:"collapse"}})},position:({addUtilities:r})=>{r({".static":{position:"static"},".fixed":{position:"fixed"},".absolute":{position:"absolute"},".relative":{position:"relative"},".sticky":{position:"sticky"}})},inset:P("inset",[["inset",["inset"]],[["inset-x",["left","right"]],["inset-y",["top","bottom"]]],[["start",["inset-inline-start"]],["end",["inset-inline-end"]],["top",["top"]],["right",["right"]],["bottom",["bottom"]],["left",["left"]]]],{supportsNegativeValues:!0}),isolation:({addUtilities:r})=>{r({".isolate":{isolation:"isolate"},".isolation-auto":{isolation:"auto"}})},zIndex:P("zIndex",[["z",["zIndex"]]],{supportsNegativeValues:!0}),order:P("order",void 0,{supportsNegativeValues:!0}),gridColumn:P("gridColumn",[["col",["gridColumn"]]]),gridColumnStart:P("gridColumnStart",[["col-start",["gridColumnStart"]]]),gridColumnEnd:P("gridColumnEnd",[["col-end",["gridColumnEnd"]]]),gridRow:P("gridRow",[["row",["gridRow"]]]),gridRowStart:P("gridRowStart",[["row-start",["gridRowStart"]]]),gridRowEnd:P("gridRowEnd",[["row-end",["gridRowEnd"]]]),float:({addUtilities:r})=>{r({".float-start":{float:"inline-start"},".float-end":{float:"inline-end"},".float-right":{float:"right"},".float-left":{float:"left"},".float-none":{float:"none"}})},clear:({addUtilities:r})=>{r({".clear-start":{clear:"inline-start"},".clear-end":{clear:"inline-end"},".clear-left":{clear:"left"},".clear-right":{clear:"right"},".clear-both":{clear:"both"},".clear-none":{clear:"none"}})},margin:P("margin",[["m",["margin"]],[["mx",["margin-left","margin-right"]],["my",["margin-top","margin-bottom"]]],[["ms",["margin-inline-start"]],["me",["margin-inline-end"]],["mt",["margin-top"]],["mr",["margin-right"]],["mb",["margin-bottom"]],["ml",["margin-left"]]]],{supportsNegativeValues:!0}),boxSizing:({addUtilities:r})=>{r({".box-border":{"box-sizing":"border-box"},".box-content":{"box-sizing":"content-box"}})},lineClamp:({matchUtilities:r,addUtilities:e,theme:t})=>{r({"line-clamp":i=>({overflow:"hidden",display:"-webkit-box","-webkit-box-orient":"vertical","-webkit-line-clamp":`${i}`})},{values:t("lineClamp")}),e({".line-clamp-none":{overflow:"visible",display:"block","-webkit-box-orient":"horizontal","-webkit-line-clamp":"none"}})},display:({addUtilities:r})=>{r({".block":{display:"block"},".inline-block":{display:"inline-block"},".inline":{display:"inline"},".flex":{display:"flex"},".inline-flex":{display:"inline-flex"},".table":{display:"table"},".inline-table":{display:"inline-table"},".table-caption":{display:"table-caption"},".table-cell":{display:"table-cell"},".table-column":{display:"table-column"},".table-column-group":{display:"table-column-group"},".table-footer-group":{display:"table-footer-group"},".table-header-group":{display:"table-header-group"},".table-row-group":{display:"table-row-group"},".table-row":{display:"table-row"},".flow-root":{display:"flow-root"},".grid":{display:"grid"},".inline-grid":{display:"inline-grid"},".contents":{display:"contents"},".list-item":{display:"list-item"},".hidden":{display:"none"}})},aspectRatio:P("aspectRatio",[["aspect",["aspect-ratio"]]]),size:P("size",[["size",["width","height"]]]),height:P("height",[["h",["height"]]]),maxHeight:P("maxHeight",[["max-h",["maxHeight"]]]),minHeight:P("minHeight",[["min-h",["minHeight"]]]),width:P("width",[["w",["width"]]]),minWidth:P("minWidth",[["min-w",["minWidth"]]]),maxWidth:P("maxWidth",[["max-w",["maxWidth"]]]),flex:P("flex"),flexShrink:P("flexShrink",[["flex-shrink",["flex-shrink"]],["shrink",["flex-shrink"]]]),flexGrow:P("flexGrow",[["flex-grow",["flex-grow"]],["grow",["flex-grow"]]]),flexBasis:P("flexBasis",[["basis",["flex-basis"]]]),tableLayout:({addUtilities:r})=>{r({".table-auto":{"table-layout":"auto"},".table-fixed":{"table-layout":"fixed"}})},captionSide:({addUtilities:r})=>{r({".caption-top":{"caption-side":"top"},".caption-bottom":{"caption-side":"bottom"}})},borderCollapse:({addUtilities:r})=>{r({".border-collapse":{"border-collapse":"collapse"},".border-separate":{"border-collapse":"separate"}})},borderSpacing:({addDefaults:r,matchUtilities:e,theme:t})=>{r("border-spacing",{"--tw-border-spacing-x":0,"--tw-border-spacing-y":0}),e({"border-spacing":i=>({"--tw-border-spacing-x":i,"--tw-border-spacing-y":i,"@defaults border-spacing":{},"border-spacing":"var(--tw-border-spacing-x) var(--tw-border-spacing-y)"}),"border-spacing-x":i=>({"--tw-border-spacing-x":i,"@defaults border-spacing":{},"border-spacing":"var(--tw-border-spacing-x) var(--tw-border-spacing-y)"}),"border-spacing-y":i=>({"--tw-border-spacing-y":i,"@defaults border-spacing":{},"border-spacing":"var(--tw-border-spacing-x) var(--tw-border-spacing-y)"})},{values:t("borderSpacing")})},transformOrigin:P("transformOrigin",[["origin",["transformOrigin"]]]),translate:P("translate",[[["translate-x",[["@defaults transform",{}],"--tw-translate-x",["transform",Te]]],["translate-y",[["@defaults transform",{}],"--tw-translate-y",["transform",Te]]]]],{supportsNegativeValues:!0}),rotate:P("rotate",[["rotate",[["@defaults transform",{}],"--tw-rotate",["transform",Te]]]],{supportsNegativeValues:!0}),skew:P("skew",[[["skew-x",[["@defaults transform",{}],"--tw-skew-x",["transform",Te]]],["skew-y",[["@defaults transform",{}],"--tw-skew-y",["transform",Te]]]]],{supportsNegativeValues:!0}),scale:P("scale",[["scale",[["@defaults transform",{}],"--tw-scale-x","--tw-scale-y",["transform",Te]]],[["scale-x",[["@defaults transform",{}],"--tw-scale-x",["transform",Te]]],["scale-y",[["@defaults transform",{}],"--tw-scale-y",["transform",Te]]]]],{supportsNegativeValues:!0}),transform:({addDefaults:r,addUtilities:e})=>{r("transform",{"--tw-translate-x":"0","--tw-translate-y":"0","--tw-rotate":"0","--tw-skew-x":"0","--tw-skew-y":"0","--tw-scale-x":"1","--tw-scale-y":"1"}),e({".transform":{"@defaults transform":{},transform:Te},".transform-cpu":{transform:Te},".transform-gpu":{transform:Te.replace("translate(var(--tw-translate-x), var(--tw-translate-y))","translate3d(var(--tw-translate-x), var(--tw-translate-y), 0)")},".transform-none":{transform:"none"}})},animation:({matchUtilities:r,theme:e,config:t})=>{let i=a=>ce(t("prefix")+a),n=Object.fromEntries(Object.entries(e("keyframes")??{}).map(([a,s])=>[a,{[`@keyframes ${i(a)}`]:s}]));r({animate:a=>{let s=Ka(a);return[...s.flatMap(o=>n[o.name]),{animation:s.map(({name:o,value:u})=>o===void 0||n[o]===void 0?u:u.replace(o,i(o))).join(", ")}]}},{values:e("animation")})},cursor:P("cursor"),touchAction:({addDefaults:r,addUtilities:e})=>{r("touch-action",{"--tw-pan-x":" ","--tw-pan-y":" ","--tw-pinch-zoom":" "});let t="var(--tw-pan-x) var(--tw-pan-y) var(--tw-pinch-zoom)";e({".touch-auto":{"touch-action":"auto"},".touch-none":{"touch-action":"none"},".touch-pan-x":{"@defaults touch-action":{},"--tw-pan-x":"pan-x","touch-action":t},".touch-pan-left":{"@defaults touch-action":{},"--tw-pan-x":"pan-left","touch-action":t},".touch-pan-right":{"@defaults touch-action":{},"--tw-pan-x":"pan-right","touch-action":t},".touch-pan-y":{"@defaults touch-action":{},"--tw-pan-y":"pan-y","touch-action":t},".touch-pan-up":{"@defaults touch-action":{},"--tw-pan-y":"pan-up","touch-action":t},".touch-pan-down":{"@defaults touch-action":{},"--tw-pan-y":"pan-down","touch-action":t},".touch-pinch-zoom":{"@defaults touch-action":{},"--tw-pinch-zoom":"pinch-zoom","touch-action":t},".touch-manipulation":{"touch-action":"manipulation"}})},userSelect:({addUtilities:r})=>{r({".select-none":{"user-select":"none"},".select-text":{"user-select":"text"},".select-all":{"user-select":"all"},".select-auto":{"user-select":"auto"}})},resize:({addUtilities:r})=>{r({".resize-none":{resize:"none"},".resize-y":{resize:"vertical"},".resize-x":{resize:"horizontal"},".resize":{resize:"both"}})},scrollSnapType:({addDefaults:r,addUtilities:e})=>{r("scroll-snap-type",{"--tw-scroll-snap-strictness":"proximity"}),e({".snap-none":{"scroll-snap-type":"none"},".snap-x":{"@defaults scroll-snap-type":{},"scroll-snap-type":"x var(--tw-scroll-snap-strictness)"},".snap-y":{"@defaults scroll-snap-type":{},"scroll-snap-type":"y var(--tw-scroll-snap-strictness)"},".snap-both":{"@defaults scroll-snap-type":{},"scroll-snap-type":"both var(--tw-scroll-snap-strictness)"},".snap-mandatory":{"--tw-scroll-snap-strictness":"mandatory"},".snap-proximity":{"--tw-scroll-snap-strictness":"proximity"}})},scrollSnapAlign:({addUtilities:r})=>{r({".snap-start":{"scroll-snap-align":"start"},".snap-end":{"scroll-snap-align":"end"},".snap-center":{"scroll-snap-align":"center"},".snap-align-none":{"scroll-snap-align":"none"}})},scrollSnapStop:({addUtilities:r})=>{r({".snap-normal":{"scroll-snap-stop":"normal"},".snap-always":{"scroll-snap-stop":"always"}})},scrollMargin:P("scrollMargin",[["scroll-m",["scroll-margin"]],[["scroll-mx",["scroll-margin-left","scroll-margin-right"]],["scroll-my",["scroll-margin-top","scroll-margin-bottom"]]],[["scroll-ms",["scroll-margin-inline-start"]],["scroll-me",["scroll-margin-inline-end"]],["scroll-mt",["scroll-margin-top"]],["scroll-mr",["scroll-margin-right"]],["scroll-mb",["scroll-margin-bottom"]],["scroll-ml",["scroll-margin-left"]]]],{supportsNegativeValues:!0}),scrollPadding:P("scrollPadding",[["scroll-p",["scroll-padding"]],[["scroll-px",["scroll-padding-left","scroll-padding-right"]],["scroll-py",["scroll-padding-top","scroll-padding-bottom"]]],[["scroll-ps",["scroll-padding-inline-start"]],["scroll-pe",["scroll-padding-inline-end"]],["scroll-pt",["scroll-padding-top"]],["scroll-pr",["scroll-padding-right"]],["scroll-pb",["scroll-padding-bottom"]],["scroll-pl",["scroll-padding-left"]]]]),listStylePosition:({addUtilities:r})=>{r({".list-inside":{"list-style-position":"inside"},".list-outside":{"list-style-position":"outside"}})},listStyleType:P("listStyleType",[["list",["listStyleType"]]]),listStyleImage:P("listStyleImage",[["list-image",["listStyleImage"]]]),appearance:({addUtilities:r})=>{r({".appearance-none":{appearance:"none"},".appearance-auto":{appearance:"auto"}})},columns:P("columns",[["columns",["columns"]]]),breakBefore:({addUtilities:r})=>{r({".break-before-auto":{"break-before":"auto"},".break-before-avoid":{"break-before":"avoid"},".break-before-all":{"break-before":"all"},".break-before-avoid-page":{"break-before":"avoid-page"},".break-before-page":{"break-before":"page"},".break-before-left":{"break-before":"left"},".break-before-right":{"break-before":"right"},".break-before-column":{"break-before":"column"}})},breakInside:({addUtilities:r})=>{r({".break-inside-auto":{"break-inside":"auto"},".break-inside-avoid":{"break-inside":"avoid"},".break-inside-avoid-page":{"break-inside":"avoid-page"},".break-inside-avoid-column":{"break-inside":"avoid-column"}})},breakAfter:({addUtilities:r})=>{r({".break-after-auto":{"break-after":"auto"},".break-after-avoid":{"break-after":"avoid"},".break-after-all":{"break-after":"all"},".break-after-avoid-page":{"break-after":"avoid-page"},".break-after-page":{"break-after":"page"},".break-after-left":{"break-after":"left"},".break-after-right":{"break-after":"right"},".break-after-column":{"break-after":"column"}})},gridAutoColumns:P("gridAutoColumns",[["auto-cols",["gridAutoColumns"]]]),gridAutoFlow:({addUtilities:r})=>{r({".grid-flow-row":{gridAutoFlow:"row"},".grid-flow-col":{gridAutoFlow:"column"},".grid-flow-dense":{gridAutoFlow:"dense"},".grid-flow-row-dense":{gridAutoFlow:"row dense"},".grid-flow-col-dense":{gridAutoFlow:"column dense"}})},gridAutoRows:P("gridAutoRows",[["auto-rows",["gridAutoRows"]]]),gridTemplateColumns:P("gridTemplateColumns",[["grid-cols",["gridTemplateColumns"]]]),gridTemplateRows:P("gridTemplateRows",[["grid-rows",["gridTemplateRows"]]]),flexDirection:({addUtilities:r})=>{r({".flex-row":{"flex-direction":"row"},".flex-row-reverse":{"flex-direction":"row-reverse"},".flex-col":{"flex-direction":"column"},".flex-col-reverse":{"flex-direction":"column-reverse"}})},flexWrap:({addUtilities:r})=>{r({".flex-wrap":{"flex-wrap":"wrap"},".flex-wrap-reverse":{"flex-wrap":"wrap-reverse"},".flex-nowrap":{"flex-wrap":"nowrap"}})},placeContent:({addUtilities:r})=>{r({".place-content-center":{"place-content":"center"},".place-content-start":{"place-content":"start"},".place-content-end":{"place-content":"end"},".place-content-between":{"place-content":"space-between"},".place-content-around":{"place-content":"space-around"},".place-content-evenly":{"place-content":"space-evenly"},".place-content-baseline":{"place-content":"baseline"},".place-content-stretch":{"place-content":"stretch"}})},placeItems:({addUtilities:r})=>{r({".place-items-start":{"place-items":"start"},".place-items-end":{"place-items":"end"},".place-items-center":{"place-items":"center"},".place-items-baseline":{"place-items":"baseline"},".place-items-stretch":{"place-items":"stretch"}})},alignContent:({addUtilities:r})=>{r({".content-normal":{"align-content":"normal"},".content-center":{"align-content":"center"},".content-start":{"align-content":"flex-start"},".content-end":{"align-content":"flex-end"},".content-between":{"align-content":"space-between"},".content-around":{"align-content":"space-around"},".content-evenly":{"align-content":"space-evenly"},".content-baseline":{"align-content":"baseline"},".content-stretch":{"align-content":"stretch"}})},alignItems:({addUtilities:r})=>{r({".items-start":{"align-items":"flex-start"},".items-end":{"align-items":"flex-end"},".items-center":{"align-items":"center"},".items-baseline":{"align-items":"baseline"},".items-stretch":{"align-items":"stretch"}})},justifyContent:({addUtilities:r})=>{r({".justify-normal":{"justify-content":"normal"},".justify-start":{"justify-content":"flex-start"},".justify-end":{"justify-content":"flex-end"},".justify-center":{"justify-content":"center"},".justify-between":{"justify-content":"space-between"},".justify-around":{"justify-content":"space-around"},".justify-evenly":{"justify-content":"space-evenly"},".justify-stretch":{"justify-content":"stretch"}})},justifyItems:({addUtilities:r})=>{r({".justify-items-start":{"justify-items":"start"},".justify-items-end":{"justify-items":"end"},".justify-items-center":{"justify-items":"center"},".justify-items-stretch":{"justify-items":"stretch"}})},gap:P("gap",[["gap",["gap"]],[["gap-x",["columnGap"]],["gap-y",["rowGap"]]]]),space:({matchUtilities:r,addUtilities:e,theme:t})=>{r({"space-x":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"--tw-space-x-reverse":"0","margin-right":`calc(${i} * var(--tw-space-x-reverse))`,"margin-left":`calc(${i} * calc(1 - var(--tw-space-x-reverse)))`}}),"space-y":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"--tw-space-y-reverse":"0","margin-top":`calc(${i} * calc(1 - var(--tw-space-y-reverse)))`,"margin-bottom":`calc(${i} * var(--tw-space-y-reverse))`}})},{values:t("space"),supportsNegativeValues:!0}),e({".space-y-reverse > :not([hidden]) ~ :not([hidden])":{"--tw-space-y-reverse":"1"},".space-x-reverse > :not([hidden]) ~ :not([hidden])":{"--tw-space-x-reverse":"1"}})},divideWidth:({matchUtilities:r,addUtilities:e,theme:t})=>{r({"divide-x":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-x-reverse":"0","border-right-width":`calc(${i} * var(--tw-divide-x-reverse))`,"border-left-width":`calc(${i} * calc(1 - var(--tw-divide-x-reverse)))`}}),"divide-y":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-y-reverse":"0","border-top-width":`calc(${i} * calc(1 - var(--tw-divide-y-reverse)))`,"border-bottom-width":`calc(${i} * var(--tw-divide-y-reverse))`}})},{values:t("divideWidth"),type:["line-width","length","any"]}),e({".divide-y-reverse > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-y-reverse":"1"},".divide-x-reverse > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-x-reverse":"1"}})},divideStyle:({addUtilities:r})=>{r({".divide-solid > :not([hidden]) ~ :not([hidden])":{"border-style":"solid"},".divide-dashed > :not([hidden]) ~ :not([hidden])":{"border-style":"dashed"},".divide-dotted > :not([hidden]) ~ :not([hidden])":{"border-style":"dotted"},".divide-double > :not([hidden]) ~ :not([hidden])":{"border-style":"double"},".divide-none > :not([hidden]) ~ :not([hidden])":{"border-style":"none"}})},divideColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({divide:i=>t("divideOpacity")?{["& > :not([hidden]) ~ :not([hidden])"]:se({color:i,property:"border-color",variable:"--tw-divide-opacity"})}:{["& > :not([hidden]) ~ :not([hidden])"]:{"border-color":N(i)}}},{values:(({DEFAULT:i,...n})=>n)(re(e("divideColor"))),type:["color","any"]})},divideOpacity:({matchUtilities:r,theme:e})=>{r({"divide-opacity":t=>({["& > :not([hidden]) ~ :not([hidden])"]:{"--tw-divide-opacity":t}})},{values:e("divideOpacity")})},placeSelf:({addUtilities:r})=>{r({".place-self-auto":{"place-self":"auto"},".place-self-start":{"place-self":"start"},".place-self-end":{"place-self":"end"},".place-self-center":{"place-self":"center"},".place-self-stretch":{"place-self":"stretch"}})},alignSelf:({addUtilities:r})=>{r({".self-auto":{"align-self":"auto"},".self-start":{"align-self":"flex-start"},".self-end":{"align-self":"flex-end"},".self-center":{"align-self":"center"},".self-stretch":{"align-self":"stretch"},".self-baseline":{"align-self":"baseline"}})},justifySelf:({addUtilities:r})=>{r({".justify-self-auto":{"justify-self":"auto"},".justify-self-start":{"justify-self":"start"},".justify-self-end":{"justify-self":"end"},".justify-self-center":{"justify-self":"center"},".justify-self-stretch":{"justify-self":"stretch"}})},overflow:({addUtilities:r})=>{r({".overflow-auto":{overflow:"auto"},".overflow-hidden":{overflow:"hidden"},".overflow-clip":{overflow:"clip"},".overflow-visible":{overflow:"visible"},".overflow-scroll":{overflow:"scroll"},".overflow-x-auto":{"overflow-x":"auto"},".overflow-y-auto":{"overflow-y":"auto"},".overflow-x-hidden":{"overflow-x":"hidden"},".overflow-y-hidden":{"overflow-y":"hidden"},".overflow-x-clip":{"overflow-x":"clip"},".overflow-y-clip":{"overflow-y":"clip"},".overflow-x-visible":{"overflow-x":"visible"},".overflow-y-visible":{"overflow-y":"visible"},".overflow-x-scroll":{"overflow-x":"scroll"},".overflow-y-scroll":{"overflow-y":"scroll"}})},overscrollBehavior:({addUtilities:r})=>{r({".overscroll-auto":{"overscroll-behavior":"auto"},".overscroll-contain":{"overscroll-behavior":"contain"},".overscroll-none":{"overscroll-behavior":"none"},".overscroll-y-auto":{"overscroll-behavior-y":"auto"},".overscroll-y-contain":{"overscroll-behavior-y":"contain"},".overscroll-y-none":{"overscroll-behavior-y":"none"},".overscroll-x-auto":{"overscroll-behavior-x":"auto"},".overscroll-x-contain":{"overscroll-behavior-x":"contain"},".overscroll-x-none":{"overscroll-behavior-x":"none"}})},scrollBehavior:({addUtilities:r})=>{r({".scroll-auto":{"scroll-behavior":"auto"},".scroll-smooth":{"scroll-behavior":"smooth"}})},textOverflow:({addUtilities:r})=>{r({".truncate":{overflow:"hidden","text-overflow":"ellipsis","white-space":"nowrap"},".overflow-ellipsis":{"text-overflow":"ellipsis"},".text-ellipsis":{"text-overflow":"ellipsis"},".text-clip":{"text-overflow":"clip"}})},hyphens:({addUtilities:r})=>{r({".hyphens-none":{hyphens:"none"},".hyphens-manual":{hyphens:"manual"},".hyphens-auto":{hyphens:"auto"}})},whitespace:({addUtilities:r})=>{r({".whitespace-normal":{"white-space":"normal"},".whitespace-nowrap":{"white-space":"nowrap"},".whitespace-pre":{"white-space":"pre"},".whitespace-pre-line":{"white-space":"pre-line"},".whitespace-pre-wrap":{"white-space":"pre-wrap"},".whitespace-break-spaces":{"white-space":"break-spaces"}})},textWrap:({addUtilities:r})=>{r({".text-wrap":{"text-wrap":"wrap"},".text-nowrap":{"text-wrap":"nowrap"},".text-balance":{"text-wrap":"balance"},".text-pretty":{"text-wrap":"pretty"}})},wordBreak:({addUtilities:r})=>{r({".break-normal":{"overflow-wrap":"normal","word-break":"normal"},".break-words":{"overflow-wrap":"break-word"},".break-all":{"word-break":"break-all"},".break-keep":{"word-break":"keep-all"}})},borderRadius:P("borderRadius",[["rounded",["border-radius"]],[["rounded-s",["border-start-start-radius","border-end-start-radius"]],["rounded-e",["border-start-end-radius","border-end-end-radius"]],["rounded-t",["border-top-left-radius","border-top-right-radius"]],["rounded-r",["border-top-right-radius","border-bottom-right-radius"]],["rounded-b",["border-bottom-right-radius","border-bottom-left-radius"]],["rounded-l",["border-top-left-radius","border-bottom-left-radius"]]],[["rounded-ss",["border-start-start-radius"]],["rounded-se",["border-start-end-radius"]],["rounded-ee",["border-end-end-radius"]],["rounded-es",["border-end-start-radius"]],["rounded-tl",["border-top-left-radius"]],["rounded-tr",["border-top-right-radius"]],["rounded-br",["border-bottom-right-radius"]],["rounded-bl",["border-bottom-left-radius"]]]]),borderWidth:P("borderWidth",[["border",[["@defaults border-width",{}],"border-width"]],[["border-x",[["@defaults border-width",{}],"border-left-width","border-right-width"]],["border-y",[["@defaults border-width",{}],"border-top-width","border-bottom-width"]]],[["border-s",[["@defaults border-width",{}],"border-inline-start-width"]],["border-e",[["@defaults border-width",{}],"border-inline-end-width"]],["border-t",[["@defaults border-width",{}],"border-top-width"]],["border-r",[["@defaults border-width",{}],"border-right-width"]],["border-b",[["@defaults border-width",{}],"border-bottom-width"]],["border-l",[["@defaults border-width",{}],"border-left-width"]]]],{type:["line-width","length"]}),borderStyle:({addUtilities:r})=>{r({".border-solid":{"border-style":"solid"},".border-dashed":{"border-style":"dashed"},".border-dotted":{"border-style":"dotted"},".border-double":{"border-style":"double"},".border-hidden":{"border-style":"hidden"},".border-none":{"border-style":"none"}})},borderColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({border:i=>t("borderOpacity")?se({color:i,property:"border-color",variable:"--tw-border-opacity"}):{"border-color":N(i)}},{values:(({DEFAULT:i,...n})=>n)(re(e("borderColor"))),type:["color","any"]}),r({"border-x":i=>t("borderOpacity")?se({color:i,property:["border-left-color","border-right-color"],variable:"--tw-border-opacity"}):{"border-left-color":N(i),"border-right-color":N(i)},"border-y":i=>t("borderOpacity")?se({color:i,property:["border-top-color","border-bottom-color"],variable:"--tw-border-opacity"}):{"border-top-color":N(i),"border-bottom-color":N(i)}},{values:(({DEFAULT:i,...n})=>n)(re(e("borderColor"))),type:["color","any"]}),r({"border-s":i=>t("borderOpacity")?se({color:i,property:"border-inline-start-color",variable:"--tw-border-opacity"}):{"border-inline-start-color":N(i)},"border-e":i=>t("borderOpacity")?se({color:i,property:"border-inline-end-color",variable:"--tw-border-opacity"}):{"border-inline-end-color":N(i)},"border-t":i=>t("borderOpacity")?se({color:i,property:"border-top-color",variable:"--tw-border-opacity"}):{"border-top-color":N(i)},"border-r":i=>t("borderOpacity")?se({color:i,property:"border-right-color",variable:"--tw-border-opacity"}):{"border-right-color":N(i)},"border-b":i=>t("borderOpacity")?se({color:i,property:"border-bottom-color",variable:"--tw-border-opacity"}):{"border-bottom-color":N(i)},"border-l":i=>t("borderOpacity")?se({color:i,property:"border-left-color",variable:"--tw-border-opacity"}):{"border-left-color":N(i)}},{values:(({DEFAULT:i,...n})=>n)(re(e("borderColor"))),type:["color","any"]})},borderOpacity:P("borderOpacity",[["border-opacity",["--tw-border-opacity"]]]),backgroundColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({bg:i=>t("backgroundOpacity")?se({color:i,property:"background-color",variable:"--tw-bg-opacity"}):{"background-color":N(i)}},{values:re(e("backgroundColor")),type:["color","any"]})},backgroundOpacity:P("backgroundOpacity",[["bg-opacity",["--tw-bg-opacity"]]]),backgroundImage:P("backgroundImage",[["bg",["background-image"]]],{type:["lookup","image","url"]}),gradientColorStops:(()=>{function r(e){return Ie(e,0,"rgb(255 255 255 / 0)")}return function({matchUtilities:e,theme:t,addDefaults:i}){i("gradient-color-stops",{"--tw-gradient-from-position":" ","--tw-gradient-via-position":" ","--tw-gradient-to-position":" "});let n={values:re(t("gradientColorStops")),type:["color","any"]},a={values:t("gradientColorStopPositions"),type:["length","percentage"]};e({from:s=>{let o=r(s);return{"@defaults gradient-color-stops":{},"--tw-gradient-from":`${N(s)} var(--tw-gradient-from-position)`,"--tw-gradient-to":`${o} var(--tw-gradient-to-position)`,"--tw-gradient-stops":"var(--tw-gradient-from), var(--tw-gradient-to)"}}},n),e({from:s=>({"--tw-gradient-from-position":s})},a),e({via:s=>{let o=r(s);return{"@defaults gradient-color-stops":{},"--tw-gradient-to":`${o} var(--tw-gradient-to-position)`,"--tw-gradient-stops":`var(--tw-gradient-from), ${N(s)} var(--tw-gradient-via-position), var(--tw-gradient-to)`}}},n),e({via:s=>({"--tw-gradient-via-position":s})},a),e({to:s=>({"@defaults gradient-color-stops":{},"--tw-gradient-to":`${N(s)} var(--tw-gradient-to-position)`})},n),e({to:s=>({"--tw-gradient-to-position":s})},a)}})(),boxDecorationBreak:({addUtilities:r})=>{r({".decoration-slice":{"box-decoration-break":"slice"},".decoration-clone":{"box-decoration-break":"clone"},".box-decoration-slice":{"box-decoration-break":"slice"},".box-decoration-clone":{"box-decoration-break":"clone"}})},backgroundSize:P("backgroundSize",[["bg",["background-size"]]],{type:["lookup","length","percentage","size"]}),backgroundAttachment:({addUtilities:r})=>{r({".bg-fixed":{"background-attachment":"fixed"},".bg-local":{"background-attachment":"local"},".bg-scroll":{"background-attachment":"scroll"}})},backgroundClip:({addUtilities:r})=>{r({".bg-clip-border":{"background-clip":"border-box"},".bg-clip-padding":{"background-clip":"padding-box"},".bg-clip-content":{"background-clip":"content-box"},".bg-clip-text":{"background-clip":"text"}})},backgroundPosition:P("backgroundPosition",[["bg",["background-position"]]],{type:["lookup",["position",{preferOnConflict:!0}]]}),backgroundRepeat:({addUtilities:r})=>{r({".bg-repeat":{"background-repeat":"repeat"},".bg-no-repeat":{"background-repeat":"no-repeat"},".bg-repeat-x":{"background-repeat":"repeat-x"},".bg-repeat-y":{"background-repeat":"repeat-y"},".bg-repeat-round":{"background-repeat":"round"},".bg-repeat-space":{"background-repeat":"space"}})},backgroundOrigin:({addUtilities:r})=>{r({".bg-origin-border":{"background-origin":"border-box"},".bg-origin-padding":{"background-origin":"padding-box"},".bg-origin-content":{"background-origin":"content-box"}})},fill:({matchUtilities:r,theme:e})=>{r({fill:t=>({fill:N(t)})},{values:re(e("fill")),type:["color","any"]})},stroke:({matchUtilities:r,theme:e})=>{r({stroke:t=>({stroke:N(t)})},{values:re(e("stroke")),type:["color","url","any"]})},strokeWidth:P("strokeWidth",[["stroke",["stroke-width"]]],{type:["length","number","percentage"]}),objectFit:({addUtilities:r})=>{r({".object-contain":{"object-fit":"contain"},".object-cover":{"object-fit":"cover"},".object-fill":{"object-fit":"fill"},".object-none":{"object-fit":"none"},".object-scale-down":{"object-fit":"scale-down"}})},objectPosition:P("objectPosition",[["object",["object-position"]]]),padding:P("padding",[["p",["padding"]],[["px",["padding-left","padding-right"]],["py",["padding-top","padding-bottom"]]],[["ps",["padding-inline-start"]],["pe",["padding-inline-end"]],["pt",["padding-top"]],["pr",["padding-right"]],["pb",["padding-bottom"]],["pl",["padding-left"]]]]),textAlign:({addUtilities:r})=>{r({".text-left":{"text-align":"left"},".text-center":{"text-align":"center"},".text-right":{"text-align":"right"},".text-justify":{"text-align":"justify"},".text-start":{"text-align":"start"},".text-end":{"text-align":"end"}})},textIndent:P("textIndent",[["indent",["text-indent"]]],{supportsNegativeValues:!0}),verticalAlign:({addUtilities:r,matchUtilities:e})=>{r({".align-baseline":{"vertical-align":"baseline"},".align-top":{"vertical-align":"top"},".align-middle":{"vertical-align":"middle"},".align-bottom":{"vertical-align":"bottom"},".align-text-top":{"vertical-align":"text-top"},".align-text-bottom":{"vertical-align":"text-bottom"},".align-sub":{"vertical-align":"sub"},".align-super":{"vertical-align":"super"}}),e({align:t=>({"vertical-align":t})})},fontFamily:({matchUtilities:r,theme:e})=>{r({font:t=>{let[i,n={}]=Array.isArray(t)&&ie(t[1])?t:[t],{fontFeatureSettings:a,fontVariationSettings:s}=n;return{"font-family":Array.isArray(i)?i.join(", "):i,...a===void 0?{}:{"font-feature-settings":a},...s===void 0?{}:{"font-variation-settings":s}}}},{values:e("fontFamily"),type:["lookup","generic-name","family-name"]})},fontSize:({matchUtilities:r,theme:e})=>{r({text:(t,{modifier:i})=>{let[n,a]=Array.isArray(t)?t:[t];if(i)return{"font-size":n,"line-height":i};let{lineHeight:s,letterSpacing:o,fontWeight:u}=ie(a)?a:{lineHeight:a};return{"font-size":n,...s===void 0?{}:{"line-height":s},...o===void 0?{}:{"letter-spacing":o},...u===void 0?{}:{"font-weight":u}}}},{values:e("fontSize"),modifiers:e("lineHeight"),type:["absolute-size","relative-size","length","percentage"]})},fontWeight:P("fontWeight",[["font",["fontWeight"]]],{type:["lookup","number","any"]}),textTransform:({addUtilities:r})=>{r({".uppercase":{"text-transform":"uppercase"},".lowercase":{"text-transform":"lowercase"},".capitalize":{"text-transform":"capitalize"},".normal-case":{"text-transform":"none"}})},fontStyle:({addUtilities:r})=>{r({".italic":{"font-style":"italic"},".not-italic":{"font-style":"normal"}})},fontVariantNumeric:({addDefaults:r,addUtilities:e})=>{let t="var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)";r("font-variant-numeric",{"--tw-ordinal":" ","--tw-slashed-zero":" ","--tw-numeric-figure":" ","--tw-numeric-spacing":" ","--tw-numeric-fraction":" "}),e({".normal-nums":{"font-variant-numeric":"normal"},".ordinal":{"@defaults font-variant-numeric":{},"--tw-ordinal":"ordinal","font-variant-numeric":t},".slashed-zero":{"@defaults font-variant-numeric":{},"--tw-slashed-zero":"slashed-zero","font-variant-numeric":t},".lining-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-figure":"lining-nums","font-variant-numeric":t},".oldstyle-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-figure":"oldstyle-nums","font-variant-numeric":t},".proportional-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-spacing":"proportional-nums","font-variant-numeric":t},".tabular-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-spacing":"tabular-nums","font-variant-numeric":t},".diagonal-fractions":{"@defaults font-variant-numeric":{},"--tw-numeric-fraction":"diagonal-fractions","font-variant-numeric":t},".stacked-fractions":{"@defaults font-variant-numeric":{},"--tw-numeric-fraction":"stacked-fractions","font-variant-numeric":t}})},lineHeight:P("lineHeight",[["leading",["lineHeight"]]]),letterSpacing:P("letterSpacing",[["tracking",["letterSpacing"]]],{supportsNegativeValues:!0}),textColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({text:i=>t("textOpacity")?se({color:i,property:"color",variable:"--tw-text-opacity"}):{color:N(i)}},{values:re(e("textColor")),type:["color","any"]})},textOpacity:P("textOpacity",[["text-opacity",["--tw-text-opacity"]]]),textDecoration:({addUtilities:r})=>{r({".underline":{"text-decoration-line":"underline"},".overline":{"text-decoration-line":"overline"},".line-through":{"text-decoration-line":"line-through"},".no-underline":{"text-decoration-line":"none"}})},textDecorationColor:({matchUtilities:r,theme:e})=>{r({decoration:t=>({"text-decoration-color":N(t)})},{values:re(e("textDecorationColor")),type:["color","any"]})},textDecorationStyle:({addUtilities:r})=>{r({".decoration-solid":{"text-decoration-style":"solid"},".decoration-double":{"text-decoration-style":"double"},".decoration-dotted":{"text-decoration-style":"dotted"},".decoration-dashed":{"text-decoration-style":"dashed"},".decoration-wavy":{"text-decoration-style":"wavy"}})},textDecorationThickness:P("textDecorationThickness",[["decoration",["text-decoration-thickness"]]],{type:["length","percentage"]}),textUnderlineOffset:P("textUnderlineOffset",[["underline-offset",["text-underline-offset"]]],{type:["length","percentage","any"]}),fontSmoothing:({addUtilities:r})=>{r({".antialiased":{"-webkit-font-smoothing":"antialiased","-moz-osx-font-smoothing":"grayscale"},".subpixel-antialiased":{"-webkit-font-smoothing":"auto","-moz-osx-font-smoothing":"auto"}})},placeholderColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({placeholder:i=>t("placeholderOpacity")?{"&::placeholder":se({color:i,property:"color",variable:"--tw-placeholder-opacity"})}:{"&::placeholder":{color:N(i)}}},{values:re(e("placeholderColor")),type:["color","any"]})},placeholderOpacity:({matchUtilities:r,theme:e})=>{r({"placeholder-opacity":t=>({["&::placeholder"]:{"--tw-placeholder-opacity":t}})},{values:e("placeholderOpacity")})},caretColor:({matchUtilities:r,theme:e})=>{r({caret:t=>({"caret-color":N(t)})},{values:re(e("caretColor")),type:["color","any"]})},accentColor:({matchUtilities:r,theme:e})=>{r({accent:t=>({"accent-color":N(t)})},{values:re(e("accentColor")),type:["color","any"]})},opacity:P("opacity",[["opacity",["opacity"]]]),backgroundBlendMode:({addUtilities:r})=>{r({".bg-blend-normal":{"background-blend-mode":"normal"},".bg-blend-multiply":{"background-blend-mode":"multiply"},".bg-blend-screen":{"background-blend-mode":"screen"},".bg-blend-overlay":{"background-blend-mode":"overlay"},".bg-blend-darken":{"background-blend-mode":"darken"},".bg-blend-lighten":{"background-blend-mode":"lighten"},".bg-blend-color-dodge":{"background-blend-mode":"color-dodge"},".bg-blend-color-burn":{"background-blend-mode":"color-burn"},".bg-blend-hard-light":{"background-blend-mode":"hard-light"},".bg-blend-soft-light":{"background-blend-mode":"soft-light"},".bg-blend-difference":{"background-blend-mode":"difference"},".bg-blend-exclusion":{"background-blend-mode":"exclusion"},".bg-blend-hue":{"background-blend-mode":"hue"},".bg-blend-saturation":{"background-blend-mode":"saturation"},".bg-blend-color":{"background-blend-mode":"color"},".bg-blend-luminosity":{"background-blend-mode":"luminosity"}})},mixBlendMode:({addUtilities:r})=>{r({".mix-blend-normal":{"mix-blend-mode":"normal"},".mix-blend-multiply":{"mix-blend-mode":"multiply"},".mix-blend-screen":{"mix-blend-mode":"screen"},".mix-blend-overlay":{"mix-blend-mode":"overlay"},".mix-blend-darken":{"mix-blend-mode":"darken"},".mix-blend-lighten":{"mix-blend-mode":"lighten"},".mix-blend-color-dodge":{"mix-blend-mode":"color-dodge"},".mix-blend-color-burn":{"mix-blend-mode":"color-burn"},".mix-blend-hard-light":{"mix-blend-mode":"hard-light"},".mix-blend-soft-light":{"mix-blend-mode":"soft-light"},".mix-blend-difference":{"mix-blend-mode":"difference"},".mix-blend-exclusion":{"mix-blend-mode":"exclusion"},".mix-blend-hue":{"mix-blend-mode":"hue"},".mix-blend-saturation":{"mix-blend-mode":"saturation"},".mix-blend-color":{"mix-blend-mode":"color"},".mix-blend-luminosity":{"mix-blend-mode":"luminosity"},".mix-blend-plus-darker":{"mix-blend-mode":"plus-darker"},".mix-blend-plus-lighter":{"mix-blend-mode":"plus-lighter"}})},boxShadow:(()=>{let r=Ge("boxShadow"),e=["var(--tw-ring-offset-shadow, 0 0 #0000)","var(--tw-ring-shadow, 0 0 #0000)","var(--tw-shadow)"].join(", ");return function({matchUtilities:t,addDefaults:i,theme:n}){i("box-shadow",{"--tw-ring-offset-shadow":"0 0 #0000","--tw-ring-shadow":"0 0 #0000","--tw-shadow":"0 0 #0000","--tw-shadow-colored":"0 0 #0000"}),t({shadow:a=>{a=r(a);let s=xi(a);for(let o of s)!o.valid||(o.color="var(--tw-shadow-color)");return{"@defaults box-shadow":{},"--tw-shadow":a==="none"?"0 0 #0000":a,"--tw-shadow-colored":a==="none"?"0 0 #0000":Lu(s),"box-shadow":e}}},{values:n("boxShadow"),type:["shadow"]})}})(),boxShadowColor:({matchUtilities:r,theme:e})=>{r({shadow:t=>({"--tw-shadow-color":N(t),"--tw-shadow":"var(--tw-shadow-colored)"})},{values:re(e("boxShadowColor")),type:["color","any"]})},outlineStyle:({addUtilities:r})=>{r({".outline-none":{outline:"2px solid transparent","outline-offset":"2px"},".outline":{"outline-style":"solid"},".outline-dashed":{"outline-style":"dashed"},".outline-dotted":{"outline-style":"dotted"},".outline-double":{"outline-style":"double"}})},outlineWidth:P("outlineWidth",[["outline",["outline-width"]]],{type:["length","number","percentage"]}),outlineOffset:P("outlineOffset",[["outline-offset",["outline-offset"]]],{type:["length","number","percentage","any"],supportsNegativeValues:!0}),outlineColor:({matchUtilities:r,theme:e})=>{r({outline:t=>({"outline-color":N(t)})},{values:re(e("outlineColor")),type:["color","any"]})},ringWidth:({matchUtilities:r,addDefaults:e,addUtilities:t,theme:i,config:n})=>{let a=(()=>{if(K(n(),"respectDefaultRingColorOpacity"))return i("ringColor.DEFAULT");let s=i("ringOpacity.DEFAULT","0.5");return i("ringColor")?.DEFAULT?Ie(i("ringColor")?.DEFAULT,s,`rgb(147 197 253 / ${s})`):`rgb(147 197 253 / ${s})`})();e("ring-width",{"--tw-ring-inset":" ","--tw-ring-offset-width":i("ringOffsetWidth.DEFAULT","0px"),"--tw-ring-offset-color":i("ringOffsetColor.DEFAULT","#fff"),"--tw-ring-color":a,"--tw-ring-offset-shadow":"0 0 #0000","--tw-ring-shadow":"0 0 #0000","--tw-shadow":"0 0 #0000","--tw-shadow-colored":"0 0 #0000"}),r({ring:s=>({"@defaults ring-width":{},"--tw-ring-offset-shadow":"var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)","--tw-ring-shadow":`var(--tw-ring-inset) 0 0 0 calc(${s} + var(--tw-ring-offset-width)) var(--tw-ring-color)`,"box-shadow":["var(--tw-ring-offset-shadow)","var(--tw-ring-shadow)","var(--tw-shadow, 0 0 #0000)"].join(", ")})},{values:i("ringWidth"),type:"length"}),t({".ring-inset":{"@defaults ring-width":{},"--tw-ring-inset":"inset"}})},ringColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({ring:i=>t("ringOpacity")?se({color:i,property:"--tw-ring-color",variable:"--tw-ring-opacity"}):{"--tw-ring-color":N(i)}},{values:Object.fromEntries(Object.entries(re(e("ringColor"))).filter(([i])=>i!=="DEFAULT")),type:["color","any"]})},ringOpacity:r=>{let{config:e}=r;return P("ringOpacity",[["ring-opacity",["--tw-ring-opacity"]]],{filterDefault:!K(e(),"respectDefaultRingColorOpacity")})(r)},ringOffsetWidth:P("ringOffsetWidth",[["ring-offset",["--tw-ring-offset-width"]]],{type:"length"}),ringOffsetColor:({matchUtilities:r,theme:e})=>{r({"ring-offset":t=>({"--tw-ring-offset-color":N(t)})},{values:re(e("ringOffsetColor")),type:["color","any"]})},blur:({matchUtilities:r,theme:e})=>{r({blur:t=>({"--tw-blur":`blur(${t})`,"@defaults filter":{},filter:Be})},{values:e("blur")})},brightness:({matchUtilities:r,theme:e})=>{r({brightness:t=>({"--tw-brightness":`brightness(${t})`,"@defaults filter":{},filter:Be})},{values:e("brightness")})},contrast:({matchUtilities:r,theme:e})=>{r({contrast:t=>({"--tw-contrast":`contrast(${t})`,"@defaults filter":{},filter:Be})},{values:e("contrast")})},dropShadow:({matchUtilities:r,theme:e})=>{r({"drop-shadow":t=>({"--tw-drop-shadow":Array.isArray(t)?t.map(i=>`drop-shadow(${i})`).join(" "):`drop-shadow(${t})`,"@defaults filter":{},filter:Be})},{values:e("dropShadow")})},grayscale:({matchUtilities:r,theme:e})=>{r({grayscale:t=>({"--tw-grayscale":`grayscale(${t})`,"@defaults filter":{},filter:Be})},{values:e("grayscale")})},hueRotate:({matchUtilities:r,theme:e})=>{r({"hue-rotate":t=>({"--tw-hue-rotate":`hue-rotate(${t})`,"@defaults filter":{},filter:Be})},{values:e("hueRotate"),supportsNegativeValues:!0})},invert:({matchUtilities:r,theme:e})=>{r({invert:t=>({"--tw-invert":`invert(${t})`,"@defaults filter":{},filter:Be})},{values:e("invert")})},saturate:({matchUtilities:r,theme:e})=>{r({saturate:t=>({"--tw-saturate":`saturate(${t})`,"@defaults filter":{},filter:Be})},{values:e("saturate")})},sepia:({matchUtilities:r,theme:e})=>{r({sepia:t=>({"--tw-sepia":`sepia(${t})`,"@defaults filter":{},filter:Be})},{values:e("sepia")})},filter:({addDefaults:r,addUtilities:e})=>{r("filter",{"--tw-blur":" ","--tw-brightness":" ","--tw-contrast":" ","--tw-grayscale":" ","--tw-hue-rotate":" ","--tw-invert":" ","--tw-saturate":" ","--tw-sepia":" ","--tw-drop-shadow":" "}),e({".filter":{"@defaults filter":{},filter:Be},".filter-none":{filter:"none"}})},backdropBlur:({matchUtilities:r,theme:e})=>{r({"backdrop-blur":t=>({"--tw-backdrop-blur":`blur(${t})`,"@defaults backdrop-filter":{},"backdrop-filter":Fe})},{values:e("backdropBlur")})},backdropBrightness:({matchUtilities:r,theme:e})=>{r({"backdrop-brightness":t=>({"--tw-backdrop-brightness":`brightness(${t})`,"@defaults backdrop-filter":{},"backdrop-filter":Fe})},{values:e("backdropBrightness")})},backdropContrast:({matchUtilities:r,theme:e})=>{r({"backdrop-contrast":t=>({"--tw-backdrop-contrast":`contrast(${t})`,"@defaults backdrop-filter":{},"backdrop-filter":Fe})},{values:e("backdropContrast")})},backdropGrayscale:({matchUtilities:r,theme:e})=>{r({"backdrop-grayscale":t=>({"--tw-backdrop-grayscale":`grayscale(${t})`,"@defaults backdrop-filter":{},"backdrop-filter":Fe})},{values:e("backdropGrayscale")})},backdropHueRotate:({matchUtilities:r,theme:e})=>{r({"backdrop-hue-rotate":t=>({"--tw-backdrop-hue-rotate":`hue-rotate(${t})`,"@defaults backdrop-filter":{},"backdrop-filter":Fe})},{values:e("backdropHueRotate"),supportsNegativeValues:!0})},backdropInvert:({matchUtilities:r,theme:e})=>{r({"backdrop-invert":t=>({"--tw-backdrop-invert":`invert(${t})`,"@defaults backdrop-filter":{},"backdrop-filter":Fe})},{values:e("backdropInvert")})},backdropOpacity:({matchUtilities:r,theme:e})=>{r({"backdrop-opacity":t=>({"--tw-backdrop-opacity":`opacity(${t})`,"@defaults backdrop-filter":{},"backdrop-filter":Fe})},{values:e("backdropOpacity")})},backdropSaturate:({matchUtilities:r,theme:e})=>{r({"backdrop-saturate":t=>({"--tw-backdrop-saturate":`saturate(${t})`,"@defaults backdrop-filter":{},"backdrop-filter":Fe})},{values:e("backdropSaturate")})},backdropSepia:({matchUtilities:r,theme:e})=>{r({"backdrop-sepia":t=>({"--tw-backdrop-sepia":`sepia(${t})`,"@defaults backdrop-filter":{},"backdrop-filter":Fe})},{values:e("backdropSepia")})},backdropFilter:({addDefaults:r,addUtilities:e})=>{r("backdrop-filter",{"--tw-backdrop-blur":" ","--tw-backdrop-brightness":" ","--tw-backdrop-contrast":" ","--tw-backdrop-grayscale":" ","--tw-backdrop-hue-rotate":" ","--tw-backdrop-invert":" ","--tw-backdrop-opacity":" ","--tw-backdrop-saturate":" ","--tw-backdrop-sepia":" "}),e({".backdrop-filter":{"@defaults backdrop-filter":{},"backdrop-filter":Fe},".backdrop-filter-none":{"backdrop-filter":"none"}})},transitionProperty:({matchUtilities:r,theme:e})=>{let t=e("transitionTimingFunction.DEFAULT"),i=e("transitionDuration.DEFAULT");r({transition:n=>({"transition-property":n,...n==="none"?{}:{"transition-timing-function":t,"transition-duration":i}})},{values:e("transitionProperty")})},transitionDelay:P("transitionDelay",[["delay",["transitionDelay"]]]),transitionDuration:P("transitionDuration",[["duration",["transitionDuration"]]],{filterDefault:!0}),transitionTimingFunction:P("transitionTimingFunction",[["ease",["transitionTimingFunction"]]],{filterDefault:!0}),willChange:P("willChange",[["will-change",["will-change"]]]),contain:({addDefaults:r,addUtilities:e})=>{let t="var(--tw-contain-size) var(--tw-contain-layout) var(--tw-contain-paint) var(--tw-contain-style)";r("contain",{"--tw-contain-size":" ","--tw-contain-layout":" ","--tw-contain-paint":" ","--tw-contain-style":" "}),e({".contain-none":{contain:"none"},".contain-content":{contain:"content"},".contain-strict":{contain:"strict"},".contain-size":{"@defaults contain":{},"--tw-contain-size":"size",contain:t},".contain-inline-size":{"@defaults contain":{},"--tw-contain-size":"inline-size",contain:t},".contain-layout":{"@defaults contain":{},"--tw-contain-layout":"layout",contain:t},".contain-paint":{"@defaults contain":{},"--tw-contain-paint":"paint",contain:t},".contain-style":{"@defaults contain":{},"--tw-contain-style":"style",contain:t}})},content:P("content",[["content",["--tw-content",["content","var(--tw-content)"]]]]),forcedColorAdjust:({addUtilities:r})=>{r({".forced-color-adjust-auto":{"forced-color-adjust":"auto"},".forced-color-adjust-none":{"forced-color-adjust":"none"}})}}});function h2(r){if(r===void 0)return!1;if(r==="true"||r==="1")return!0;if(r==="false"||r==="0")return!1;if(r==="*")return!0;let e=r.split(",").map(t=>t.split(":")[0]);return e.includes("-tailwindcss")?!1:!!e.includes("tailwindcss")}var Pe,Ad,_d,vn,Za,He,ti,lt=C(()=>{l();Pe=typeof h!="undefined"?{NODE_ENV:"production",DEBUG:h2(h.env.DEBUG)}:{NODE_ENV:"production",DEBUG:!1},Ad=new Map,_d=new Map,vn=new Map,Za=new Map,He=new String("*"),ti=Symbol("__NONE__")});function zt(r){let e=[],t=!1;for(let i=0;i0)}var Od,Ed,m2,eo=C(()=>{l();Od=new Map([["{","}"],["[","]"],["(",")"]]),Ed=new Map(Array.from(Od.entries()).map(([r,e])=>[e,r])),m2=new Set(['"',"'","`"])});function jt(r){let[e]=Td(r);return e.forEach(([t,i])=>t.removeChild(i)),r.nodes.push(...e.map(([,t])=>t)),r}function Td(r){let e=[],t=null;for(let i of r.nodes)if(i.type==="combinator")e=e.filter(([,n])=>ro(n).includes("jumpable")),t=null;else if(i.type==="pseudo"){g2(i)?(t=i,e.push([r,i,null])):t&&y2(i,t)?e.push([r,i,t]):t=null;for(let n of i.nodes??[]){let[a,s]=Td(n);t=s||t,e.push(...a)}}return[e,t]}function Pd(r){return r.value.startsWith("::")||to[r.value]!==void 0}function g2(r){return Pd(r)&&ro(r).includes("terminal")}function y2(r,e){return r.type!=="pseudo"||Pd(r)?!1:ro(e).includes("actionable")}function ro(r){return to[r.value]??to.__default__}var to,xn=C(()=>{l();to={"::after":["terminal","jumpable"],"::backdrop":["terminal","jumpable"],"::before":["terminal","jumpable"],"::cue":["terminal"],"::cue-region":["terminal"],"::first-letter":["terminal","jumpable"],"::first-line":["terminal","jumpable"],"::grammar-error":["terminal"],"::marker":["terminal","jumpable"],"::part":["terminal","actionable"],"::placeholder":["terminal","jumpable"],"::selection":["terminal","jumpable"],"::slotted":["terminal"],"::spelling-error":["terminal"],"::target-text":["terminal"],"::file-selector-button":["terminal","actionable"],"::deep":["actionable"],"::v-deep":["actionable"],"::ng-deep":["actionable"],":after":["terminal","jumpable"],":before":["terminal","jumpable"],":first-letter":["terminal","jumpable"],":first-line":["terminal","jumpable"],":where":[],":is":[],":has":[],__default__:["terminal","actionable"]}});function Vt(r,{context:e,candidate:t}){let i=e?.tailwindConfig.prefix??"",n=r.map(s=>{let o=(0,Le.default)().astSync(s.format);return{...s,ast:s.respectPrefix?Nt(i,o):o}}),a=Le.default.root({nodes:[Le.default.selector({nodes:[Le.default.className({value:ce(t)})]})]});for(let{ast:s}of n)[a,s]=b2(a,s),s.walkNesting(o=>o.replaceWith(...a.nodes[0].nodes)),a=s;return a}function Id(r){let e=[];for(;r.prev()&&r.prev().type!=="combinator";)r=r.prev();for(;r&&r.type!=="combinator";)e.push(r),r=r.next();return e}function w2(r){return r.sort((e,t)=>e.type==="tag"&&t.type==="class"?-1:e.type==="class"&&t.type==="tag"?1:e.type==="class"&&t.type==="pseudo"&&t.value.startsWith("::")?-1:e.type==="pseudo"&&e.value.startsWith("::")&&t.type==="class"?1:r.index(e)-r.index(t)),r}function no(r,e){let t=!1;r.walk(i=>{if(i.type==="class"&&i.value===e)return t=!0,!1}),t||r.remove()}function kn(r,e,{context:t,candidate:i,base:n}){let a=t?.tailwindConfig?.separator??":";n=n??ae(i,a).pop();let s=(0,Le.default)().astSync(r);if(s.walkClasses(f=>{f.raws&&f.value.includes(n)&&(f.raws.value=ce((0,Dd.default)(f.raws.value)))}),s.each(f=>no(f,n)),s.length===0)return null;let o=Array.isArray(e)?Vt(e,{context:t,candidate:i}):e;if(o===null)return s.toString();let u=Le.default.comment({value:"/*__simple__*/"}),c=Le.default.comment({value:"/*__simple__*/"});return s.walkClasses(f=>{if(f.value!==n)return;let d=f.parent,p=o.nodes[0].nodes;if(d.nodes.length===1){f.replaceWith(...p);return}let m=Id(f);d.insertBefore(m[0],u),d.insertAfter(m[m.length-1],c);for(let x of p)d.insertBefore(m[0],x.clone());f.remove(),m=Id(u);let b=d.index(u);d.nodes.splice(b,m.length,...w2(Le.default.selector({nodes:m})).nodes),u.remove(),c.remove()}),s.walkPseudos(f=>{f.value===io&&f.replaceWith(f.nodes)}),s.each(f=>jt(f)),s.toString()}function b2(r,e){let t=[];return r.walkPseudos(i=>{i.value===io&&t.push({pseudo:i,value:i.nodes[0].toString()})}),e.walkPseudos(i=>{if(i.value!==io)return;let n=i.nodes[0].toString(),a=t.find(c=>c.value===n);if(!a)return;let s=[],o=i.next();for(;o&&o.type!=="combinator";)s.push(o),o=o.next();let u=o;a.pseudo.parent.insertAfter(a.pseudo,Le.default.selector({nodes:s.map(c=>c.clone())})),i.remove(),s.forEach(c=>c.remove()),u&&u.type==="combinator"&&u.remove()}),[r,e]}var Le,Dd,io,so=C(()=>{l();Le=X(Me()),Dd=X(Ki());$t();dn();xn();_t();io=":merge"});function Sn(r,e){let t=(0,ao.default)().astSync(r);return t.each(i=>{i.nodes[0].type==="pseudo"&&i.nodes[0].value===":is"&&i.nodes.every(a=>a.type!=="combinator")||(i.nodes=[ao.default.pseudo({value:":is",nodes:[i.clone()]})]),jt(i)}),`${e} ${t.toString()}`}var ao,oo=C(()=>{l();ao=X(Me());xn()});function lo(r){return v2.transformSync(r)}function*x2(r){let e=1/0;for(;e>=0;){let t,i=!1;if(e===1/0&&r.endsWith("]")){let s=r.indexOf("[");r[s-1]==="-"?t=s-1:r[s-1]==="/"?(t=s-1,i=!0):t=-1}else e===1/0&&r.includes("/")?(t=r.lastIndexOf("/"),i=!0):t=r.lastIndexOf("-",e);if(t<0)break;let n=r.slice(0,t),a=r.slice(i?t:t+1);e=t-1,!(n===""||a==="/")&&(yield[n,a])}}function k2(r,e){if(r.length===0||e.tailwindConfig.prefix==="")return r;for(let t of r){let[i]=t;if(i.options.respectPrefix){let n=j.root({nodes:[t[1].clone()]}),a=t[1].raws.tailwind.classCandidate;n.walkRules(s=>{let o=a.startsWith("-");s.selector=Nt(e.tailwindConfig.prefix,s.selector,o)}),t[1]=n.nodes[0]}}return r}function S2(r,e){if(r.length===0)return r;let t=[];function i(n){return n.parent&&n.parent.type==="atrule"&&n.parent.name==="keyframes"}for(let[n,a]of r){let s=j.root({nodes:[a.clone()]});s.walkRules(o=>{if(i(o))return;let u=(0,Cn.default)().astSync(o.selector);u.each(c=>no(c,e)),Ju(u,c=>c===e?`!${c}`:c),o.selector=u.toString(),o.walkDecls(c=>c.important=!0)}),t.push([{...n,important:!0},s.nodes[0]])}return t}function C2(r,e,t){if(e.length===0)return e;let i={modifier:null,value:ti};{let[n,...a]=ae(r,"/");if(a.length>1&&(n=n+"/"+a.slice(0,-1).join("/"),a=a.slice(-1)),a.length&&!t.variantMap.has(r)&&(r=n,i.modifier=a[0],!K(t.tailwindConfig,"generalizedModifiers")))return[]}if(r.endsWith("]")&&!r.startsWith("[")){let n=/(.)(-?)\[(.*)\]/g.exec(r);if(n){let[,a,s,o]=n;if(a==="@"&&s==="-")return[];if(a!=="@"&&s==="")return[];r=r.replace(`${s}[${o}]`,""),i.value=o}}if(co(r)&&!t.variantMap.has(r)){let n=t.offsets.recordVariant(r),a=L(r.slice(1,-1)),s=ae(a,",");if(s.length>1)return[];if(!s.every(En))return[];let o=s.map((u,c)=>[t.offsets.applyParallelOffset(n,c),ri(u.trim())]);t.variantMap.set(r,o)}if(t.variantMap.has(r)){let n=co(r),a=t.variantOptions.get(r)?.[Zr]??{},s=t.variantMap.get(r).slice(),o=[],u=(()=>!(n||a.respectPrefix===!1))();for(let[c,f]of e){if(c.layer==="user")continue;let d=j.root({nodes:[f.clone()]});for(let[p,m,b]of s){let w=function(){x.raws.neededBackup||(x.raws.neededBackup=!0,x.walkRules(E=>E.raws.originalSelector=E.selector))},k=function(E){return w(),x.each(I=>{I.type==="rule"&&(I.selectors=I.selectors.map(q=>E({get className(){return lo(q)},selector:q})))}),x},x=(b??d).clone(),y=[],S=m({get container(){return w(),x},separator:t.tailwindConfig.separator,modifySelectors:k,wrap(E){let I=x.nodes;x.removeAll(),E.append(I),x.append(E)},format(E){y.push({format:E,respectPrefix:u})},args:i});if(Array.isArray(S)){for(let[E,I]of S.entries())s.push([t.offsets.applyParallelOffset(p,E),I,x.clone()]);continue}if(typeof S=="string"&&y.push({format:S,respectPrefix:u}),S===null)continue;x.raws.neededBackup&&(delete x.raws.neededBackup,x.walkRules(E=>{let I=E.raws.originalSelector;if(!I||(delete E.raws.originalSelector,I===E.selector))return;let q=E.selector,R=(0,Cn.default)(J=>{J.walkClasses(ue=>{ue.value=`${r}${t.tailwindConfig.separator}${ue.value}`})}).processSync(I);y.push({format:q.replace(R,"&"),respectPrefix:u}),E.selector=I})),x.nodes[0].raws.tailwind={...x.nodes[0].raws.tailwind,parentLayer:c.layer};let _=[{...c,sort:t.offsets.applyVariantOffset(c.sort,p,Object.assign(i,t.variantOptions.get(r))),collectedFormats:(c.collectedFormats??[]).concat(y)},x.nodes[0]];o.push(_)}}return o}return[]}function uo(r,e,t={}){return!ie(r)&&!Array.isArray(r)?[[r],t]:Array.isArray(r)?uo(r[0],e,r[1]):(e.has(r)||e.set(r,Lt(r)),[e.get(r),t])}function _2(r){return A2.test(r)}function O2(r){if(!r.includes("://"))return!1;try{let e=new URL(r);return e.scheme!==""&&e.host!==""}catch(e){return!1}}function qd(r){let e=!0;return r.walkDecls(t=>{if(!Rd(t.prop,t.value))return e=!1,!1}),e}function Rd(r,e){if(O2(`${r}:${e}`))return!1;try{return j.parse(`a{${r}:${e}}`).toResult(),!0}catch(t){return!1}}function E2(r,e){let[,t,i]=r.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/)??[];if(i===void 0||!_2(t)||!zt(i))return null;let n=L(i,{property:t});return Rd(t,n)?[[{sort:e.offsets.arbitraryProperty(r),layer:"utilities",options:{respectImportant:!0}},()=>({[Ja(r)]:{[t]:n}})]]:null}function*T2(r,e){e.candidateRuleMap.has(r)&&(yield[e.candidateRuleMap.get(r),"DEFAULT"]),yield*function*(o){o!==null&&(yield[o,"DEFAULT"])}(E2(r,e));let t=r,i=!1,n=e.tailwindConfig.prefix,a=n.length,s=t.startsWith(n)||t.startsWith(`-${n}`);t[a]==="-"&&s&&(i=!0,t=n+t.slice(a+1)),i&&e.candidateRuleMap.has(t)&&(yield[e.candidateRuleMap.get(t),"-DEFAULT"]);for(let[o,u]of x2(t))e.candidateRuleMap.has(o)&&(yield[e.candidateRuleMap.get(o),i?`-${u}`:u])}function P2(r,e){return r===He?[He]:ae(r,e)}function*D2(r,e){for(let t of r)t[1].raws.tailwind={...t[1].raws.tailwind,classCandidate:e,preserveSource:t[0].options?.preserveSource??!1},yield t}function*fo(r,e){let t=e.tailwindConfig.separator,[i,...n]=P2(r,t).reverse(),a=!1;i.startsWith("!")&&(a=!0,i=i.slice(1));for(let s of T2(i,e)){let o=[],u=new Map,[c,f]=s,d=c.length===1;for(let[p,m]of c){let b=[];if(typeof m=="function")for(let x of[].concat(m(f,{isOnlyPlugin:d}))){let[y,w]=uo(x,e.postCssNodeCache);for(let k of y)b.push([{...p,options:{...p.options,...w}},k])}else if(f==="DEFAULT"||f==="-DEFAULT"){let x=m,[y,w]=uo(x,e.postCssNodeCache);for(let k of y)b.push([{...p,options:{...p.options,...w}},k])}if(b.length>0){let x=Array.from(ys(p.options?.types??[],f,p.options??{},e.tailwindConfig)).map(([y,w])=>w);x.length>0&&u.set(b,x),o.push(b)}}if(co(f)){if(o.length>1){let b=function(y){return y.length===1?y[0]:y.find(w=>{let k=u.get(w);return w.some(([{options:S},_])=>qd(_)?S.types.some(({type:E,preferOnConflict:I})=>k.includes(E)&&I):!1)})},[p,m]=o.reduce((y,w)=>(w.some(([{options:S}])=>S.types.some(({type:_})=>_==="any"))?y[0].push(w):y[1].push(w),y),[[],[]]),x=b(m)??b(p);if(x)o=[x];else{let y=o.map(k=>new Set([...u.get(k)??[]]));for(let k of y)for(let S of k){let _=!1;for(let E of y)k!==E&&E.has(S)&&(E.delete(S),_=!0);_&&k.delete(S)}let w=[];for(let[k,S]of y.entries())for(let _ of S){let E=o[k].map(([,I])=>I).flat().map(I=>I.toString().split(` +`).slice(1,-1).map(q=>q.trim()).map(q=>` ${q}`).join(` +`)).join(` + +`);w.push(` Use \`${r.replace("[",`[${_}:`)}\` for \`${E.trim()}\``);break}F.warn([`The class \`${r}\` is ambiguous and matches multiple utilities.`,...w,`If this is content and not a class, replace it with \`${r.replace("[","[").replace("]","]")}\` to silence this warning.`]);continue}}o=o.map(p=>p.filter(m=>qd(m[1])))}o=o.flat(),o=Array.from(D2(o,i)),o=k2(o,e),a&&(o=S2(o,i));for(let p of n)o=C2(p,o,e);for(let p of o)p[1].raws.tailwind={...p[1].raws.tailwind,candidate:r},p=I2(p,{context:e,candidate:r}),p!==null&&(yield p)}}function I2(r,{context:e,candidate:t}){if(!r[0].collectedFormats)return r;let i=!0,n;try{n=Vt(r[0].collectedFormats,{context:e,candidate:t})}catch{return null}let a=j.root({nodes:[r[1].clone()]});return a.walkRules(s=>{if(!An(s))try{let o=kn(s.selector,n,{candidate:t,context:e});if(o===null){s.remove();return}s.selector=o}catch{return i=!1,!1}}),!i||a.nodes.length===0?null:(r[1]=a.nodes[0],r)}function An(r){return r.parent&&r.parent.type==="atrule"&&r.parent.name==="keyframes"}function q2(r){if(r===!0)return e=>{An(e)||e.walkDecls(t=>{t.parent.type==="rule"&&!An(t.parent)&&(t.important=!0)})};if(typeof r=="string")return e=>{An(e)||(e.selectors=e.selectors.map(t=>Sn(t,r)))}}function _n(r,e,t=!1){let i=[],n=q2(e.tailwindConfig.important);for(let a of r){if(e.notClassCache.has(a))continue;if(e.candidateRuleCache.has(a)){i=i.concat(Array.from(e.candidateRuleCache.get(a)));continue}let s=Array.from(fo(a,e));if(s.length===0){e.notClassCache.add(a);continue}e.classCache.set(a,s);let o=e.candidateRuleCache.get(a)??new Set;e.candidateRuleCache.set(a,o);for(let u of s){let[{sort:c,options:f},d]=u;if(f.respectImportant&&n){let m=j.root({nodes:[d.clone()]});m.walkRules(n),d=m.nodes[0]}let p=[c,t?d.clone():d];o.add(p),e.ruleCache.add(p),i.push(p)}}return i}function co(r){return r.startsWith("[")&&r.endsWith("]")}var Cn,v2,A2,On=C(()=>{l();st();Cn=X(Me());Qa();At();dn();hr();Oe();lt();so();Xa();dr();ei();eo();_t();je();oo();v2=(0,Cn.default)(r=>r.first.filter(({type:e})=>e==="class").pop().value);A2=/^[a-z_-]/});var Md,Bd=C(()=>{l();Md={}});function R2(r){try{return Md.createHash("md5").update(r,"utf-8").digest("binary")}catch(e){return""}}function Fd(r,e){let t=e.toString();if(!t.includes("@tailwind"))return!1;let i=Za.get(r),n=R2(t),a=i!==n;return Za.set(r,n),a}var Ld=C(()=>{l();Bd();lt()});function Tn(r){return(r>0n)-(r<0n)}var Nd=C(()=>{l()});function $d(r,e){let t=0n,i=0n;for(let[n,a]of e)r&n&&(t=t|n,i=i|a);return r&~t|i}var zd=C(()=>{l()});function jd(r){let e=null;for(let t of r)e=e??t,e=e>t?e:t;return e}function M2(r,e){let t=r.length,i=e.length,n=t{l();Nd();zd();po=class{constructor(){this.offsets={defaults:0n,base:0n,components:0n,utilities:0n,variants:0n,user:0n},this.layerPositions={defaults:0n,base:1n,components:2n,utilities:3n,user:4n,variants:5n},this.reservedVariantBits=0n,this.variantOffsets=new Map}create(e){return{layer:e,parentLayer:e,arbitrary:0n,variants:0n,parallelIndex:0n,index:this.offsets[e]++,propertyOffset:0n,property:"",options:[]}}arbitraryProperty(e){return{...this.create("utilities"),arbitrary:1n,property:e}}forVariant(e,t=0){let i=this.variantOffsets.get(e);if(i===void 0)throw new Error(`Cannot find offset for unknown variant ${e}`);return{...this.create("variants"),variants:i<n.startsWith("[")).sort(([n],[a])=>M2(n,a)),t=e.map(([,n])=>n).sort((n,a)=>Tn(n-a));return e.map(([,n],a)=>[n,t[a]]).filter(([n,a])=>n!==a)}remapArbitraryVariantOffsets(e){let t=this.recalculateVariantOffsets();return t.length===0?e:e.map(i=>{let[n,a]=i;return n={...n,variants:$d(n.variants,t)},[n,a]})}sortArbitraryProperties(e){let t=new Set;for(let[s]of e)s.arbitrary===1n&&t.add(s.property);if(t.size===0)return e;let i=Array.from(t).sort(),n=new Map,a=1n;for(let s of i)n.set(s,a++);return e.map(s=>{let[o,u]=s;return o={...o,propertyOffset:n.get(o.property)??0n},[o,u]})}sort(e){return e=this.remapArbitraryVariantOffsets(e),e=this.sortArbitraryProperties(e),e.sort(([t],[i])=>Tn(this.compare(t,i)))}}});function yo(r,e){let t=r.tailwindConfig.prefix;return typeof t=="function"?t(e):t+e}function Wd({type:r="any",...e}){let t=[].concat(r);return{...e,types:t.map(i=>Array.isArray(i)?{type:i[0],...i[1]}:{type:i,preferOnConflict:!1})}}function B2(r){let e=[],t="",i=0;for(let n=0;n0&&e.push(t.trim()),e=e.filter(n=>n!==""),e}function F2(r,e,{before:t=[]}={}){if(t=[].concat(t),t.length<=0){r.push(e);return}let i=r.length-1;for(let n of t){let a=r.indexOf(n);a!==-1&&(i=Math.min(i,a))}r.splice(i,0,e)}function Gd(r){return Array.isArray(r)?r.flatMap(e=>!Array.isArray(e)&&!ie(e)?e:Lt(e)):Gd([r])}function L2(r,e){return(0,ho.default)(i=>{let n=[];return e&&e(i),i.walkClasses(a=>{n.push(a.value)}),n}).transformSync(r)}function N2(r){r.walkPseudos(e=>{e.value===":not"&&e.remove()})}function $2(r,e={containsNonOnDemandable:!1},t=0){let i=[],n=[];r.type==="rule"?n.push(...r.selectors):r.type==="atrule"&&r.walkRules(a=>n.push(...a.selectors));for(let a of n){let s=L2(a,N2);s.length===0&&(e.containsNonOnDemandable=!0);for(let o of s)i.push(o)}return t===0?[e.containsNonOnDemandable||i.length===0,i]:i}function Pn(r){return Gd(r).flatMap(e=>{let t=new Map,[i,n]=$2(e);return i&&n.unshift(He),n.map(a=>(t.has(e)||t.set(e,e),[a,t.get(e)]))})}function En(r){return r.startsWith("@")||r.includes("&")}function ri(r){r=r.replace(/\n+/g,"").replace(/\s{1,}/g," ").trim();let e=B2(r).map(t=>{if(!t.startsWith("@"))return({format:a})=>a(t);let[,i,n]=/@(\S*)( .+|[({].*)?/g.exec(t);return({wrap:a})=>a(j.atRule({name:i,params:n?.trim()??""}))}).reverse();return t=>{for(let i of e)i(t)}}function z2(r,e,{variantList:t,variantMap:i,offsets:n,classList:a}){function s(p,m){return p?(0,Ud.default)(r,p,m):r}function o(p){return Nt(r.prefix,p)}function u(p,m){return p===He?He:m.respectPrefix?e.tailwindConfig.prefix+p:p}function c(p,m,b={}){let x=Ze(p),y=s(["theme",...x],m);return Ge(x[0])(y,b)}let f=0,d={postcss:j,prefix:o,e:ce,config:s,theme:c,corePlugins:p=>Array.isArray(r.corePlugins)?r.corePlugins.includes(p):s(["corePlugins",p],!0),variants:()=>[],addBase(p){for(let[m,b]of Pn(p)){let x=u(m,{}),y=n.create("base");e.candidateRuleMap.has(x)||e.candidateRuleMap.set(x,[]),e.candidateRuleMap.get(x).push([{sort:y,layer:"base"},b])}},addDefaults(p,m){let b={[`@defaults ${p}`]:m};for(let[x,y]of Pn(b)){let w=u(x,{});e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push([{sort:n.create("defaults"),layer:"defaults"},y])}},addComponents(p,m){m=Object.assign({},{preserveSource:!1,respectPrefix:!0,respectImportant:!1},Array.isArray(m)?{}:m);for(let[x,y]of Pn(p)){let w=u(x,m);a.add(w),e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push([{sort:n.create("components"),layer:"components",options:m},y])}},addUtilities(p,m){m=Object.assign({},{preserveSource:!1,respectPrefix:!0,respectImportant:!0},Array.isArray(m)?{}:m);for(let[x,y]of Pn(p)){let w=u(x,m);a.add(w),e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push([{sort:n.create("utilities"),layer:"utilities",options:m},y])}},matchUtilities:function(p,m){m=Wd({...{respectPrefix:!0,respectImportant:!0,modifiers:!1},...m});let x=n.create("utilities");for(let y in p){let S=function(E,{isOnlyPlugin:I}){let[q,R,J]=gs(m.types,E,m,r);if(q===void 0)return[];if(!m.types.some(({type:ee})=>ee===R))if(I)F.warn([`Unnecessary typehint \`${R}\` in \`${y}-${E}\`.`,`You can safely update it to \`${y}-${E.replace(R+":","")}\`.`]);else return[];if(!zt(q))return[];let ue={get modifier(){return m.modifiers||F.warn(`modifier-used-without-options-for-${y}`,["Your plugin must set `modifiers: true` in its options to support modifiers."]),J}},de=K(r,"generalizedModifiers");return[].concat(de?k(q,ue):k(q)).filter(Boolean).map(ee=>({[hn(y,E)]:ee}))},w=u(y,m),k=p[y];a.add([w,m]);let _=[{sort:x,layer:"utilities",options:m},S];e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push(_)}},matchComponents:function(p,m){m=Wd({...{respectPrefix:!0,respectImportant:!1,modifiers:!1},...m});let x=n.create("components");for(let y in p){let S=function(E,{isOnlyPlugin:I}){let[q,R,J]=gs(m.types,E,m,r);if(q===void 0)return[];if(!m.types.some(({type:ee})=>ee===R))if(I)F.warn([`Unnecessary typehint \`${R}\` in \`${y}-${E}\`.`,`You can safely update it to \`${y}-${E.replace(R+":","")}\`.`]);else return[];if(!zt(q))return[];let ue={get modifier(){return m.modifiers||F.warn(`modifier-used-without-options-for-${y}`,["Your plugin must set `modifiers: true` in its options to support modifiers."]),J}},de=K(r,"generalizedModifiers");return[].concat(de?k(q,ue):k(q)).filter(Boolean).map(ee=>({[hn(y,E)]:ee}))},w=u(y,m),k=p[y];a.add([w,m]);let _=[{sort:x,layer:"components",options:m},S];e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push(_)}},addVariant(p,m,b={}){m=[].concat(m).map(x=>{if(typeof x!="string")return(y={})=>{let{args:w,modifySelectors:k,container:S,separator:_,wrap:E,format:I}=y,q=x(Object.assign({modifySelectors:k,container:S,separator:_},b.type===mo.MatchVariant&&{args:w,wrap:E,format:I}));if(typeof q=="string"&&!En(q))throw new Error(`Your custom variant \`${p}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.`);return Array.isArray(q)?q.filter(R=>typeof R=="string").map(R=>ri(R)):q&&typeof q=="string"&&ri(q)(y)};if(!En(x))throw new Error(`Your custom variant \`${p}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.`);return ri(x)}),F2(t,p,b),i.set(p,m),e.variantOptions.set(p,b)},matchVariant(p,m,b){let x=b?.id??++f,y=p==="@",w=K(r,"generalizedModifiers");for(let[S,_]of Object.entries(b?.values??{}))S!=="DEFAULT"&&d.addVariant(y?`${p}${S}`:`${p}-${S}`,({args:E,container:I})=>m(_,w?{modifier:E?.modifier,container:I}:{container:I}),{...b,value:_,id:x,type:mo.MatchVariant,variantInfo:go.Base});let k="DEFAULT"in(b?.values??{});d.addVariant(p,({args:S,container:_})=>S?.value===ti&&!k?null:m(S?.value===ti?b.values.DEFAULT:S?.value??(typeof S=="string"?S:""),w?{modifier:S?.modifier,container:_}:{container:_}),{...b,id:x,type:mo.MatchVariant,variantInfo:go.Dynamic})}};return d}function Dn(r){return wo.has(r)||wo.set(r,new Map),wo.get(r)}function Hd(r,e){let t=!1,i=new Map;for(let n of r){if(!n)continue;let a=Ss.parse(n),s=a.hash?a.href.replace(a.hash,""):a.href;s=a.search?s.replace(a.search,""):s;let o=te.statSync(decodeURIComponent(s),{throwIfNoEntry:!1})?.mtimeMs;!o||((!e.has(n)||o>e.get(n))&&(t=!0),i.set(n,o))}return[t,i]}function Yd(r){r.walkAtRules(e=>{["responsive","variants"].includes(e.name)&&(Yd(e),e.before(e.nodes),e.remove())})}function j2(r){let e=[];return r.each(t=>{t.type==="atrule"&&["responsive","variants"].includes(t.name)&&(t.name="layer",t.params="utilities")}),r.walkAtRules("layer",t=>{if(Yd(t),t.params==="base"){for(let i of t.nodes)e.push(function({addBase:n}){n(i,{respectPrefix:!1})});t.remove()}else if(t.params==="components"){for(let i of t.nodes)e.push(function({addComponents:n}){n(i,{respectPrefix:!1,preserveSource:!0})});t.remove()}else if(t.params==="utilities"){for(let i of t.nodes)e.push(function({addUtilities:n}){n(i,{respectPrefix:!1,preserveSource:!0})});t.remove()}}),e}function V2(r,e){let t=Object.entries({...H,...Sd}).map(([u,c])=>r.tailwindConfig.corePlugins.includes(u)?c:null).filter(Boolean),i=r.tailwindConfig.plugins.map(u=>(u.__isOptionsFunction&&(u=u()),typeof u=="function"?u:u.handler)),n=j2(e),a=[H.childVariant,H.pseudoElementVariants,H.pseudoClassVariants,H.hasVariants,H.ariaVariants,H.dataVariants],s=[H.supportsVariants,H.reducedMotionVariants,H.prefersContrastVariants,H.screenVariants,H.orientationVariants,H.directionVariants,H.darkVariants,H.forcedColorsVariants,H.printVariant];return(r.tailwindConfig.darkMode==="class"||Array.isArray(r.tailwindConfig.darkMode)&&r.tailwindConfig.darkMode[0]==="class")&&(s=[H.supportsVariants,H.reducedMotionVariants,H.prefersContrastVariants,H.darkVariants,H.screenVariants,H.orientationVariants,H.directionVariants,H.forcedColorsVariants,H.printVariant]),[...t,...a,...i,...s,...n]}function U2(r,e){let t=[],i=new Map;e.variantMap=i;let n=new po;e.offsets=n;let a=new Set,s=z2(e.tailwindConfig,e,{variantList:t,variantMap:i,offsets:n,classList:a});for(let f of r)if(Array.isArray(f))for(let d of f)d(s);else f?.(s);n.recordVariants(t,f=>i.get(f).length);for(let[f,d]of i.entries())e.variantMap.set(f,d.map((p,m)=>[n.forVariant(f,m),p]));let o=(e.tailwindConfig.safelist??[]).filter(Boolean);if(o.length>0){let f=[];for(let d of o){if(typeof d=="string"){e.changedContent.push({content:d,extension:"html"});continue}if(d instanceof RegExp){F.warn("root-regex",["Regular expressions in `safelist` work differently in Tailwind CSS v3.0.","Update your `safelist` configuration to eliminate this warning.","https://tailwindcss.com/docs/content-configuration#safelisting-classes"]);continue}f.push(d)}if(f.length>0){let d=new Map,p=e.tailwindConfig.prefix.length,m=f.some(b=>b.pattern.source.includes("!"));for(let b of a){let x=Array.isArray(b)?(()=>{let[y,w]=b,S=Object.keys(w?.values??{}).map(_=>Kr(y,_));return w?.supportsNegativeValues&&(S=[...S,...S.map(_=>"-"+_)],S=[...S,...S.map(_=>_.slice(0,p)+"-"+_.slice(p))]),w.types.some(({type:_})=>_==="color")&&(S=[...S,...S.flatMap(_=>Object.keys(e.tailwindConfig.theme.opacity).map(E=>`${_}/${E}`))]),m&&w?.respectImportant&&(S=[...S,...S.map(_=>"!"+_)]),S})():[b];for(let y of x)for(let{pattern:w,variants:k=[]}of f)if(w.lastIndex=0,d.has(w)||d.set(w,0),!!w.test(y)){d.set(w,d.get(w)+1),e.changedContent.push({content:y,extension:"html"});for(let S of k)e.changedContent.push({content:S+e.tailwindConfig.separator+y,extension:"html"})}}for(let[b,x]of d.entries())x===0&&F.warn([`The safelist pattern \`${b}\` doesn't match any Tailwind CSS classes.`,"Fix this pattern or remove it from your `safelist` configuration.","https://tailwindcss.com/docs/content-configuration#safelisting-classes"])}}let u=[].concat(e.tailwindConfig.darkMode??"media")[1]??"dark",c=[yo(e,u),yo(e,"group"),yo(e,"peer")];e.getClassOrder=function(d){let p=[...d].sort((y,w)=>y===w?0:y[y,null])),b=_n(new Set(p),e,!0);b=e.offsets.sort(b);let x=BigInt(c.length);for(let[,y]of b){let w=y.raws.tailwind.candidate;m.set(w,m.get(w)??x++)}return d.map(y=>{let w=m.get(y)??null,k=c.indexOf(y);return w===null&&k!==-1&&(w=BigInt(k)),[y,w]})},e.getClassList=function(d={}){let p=[];for(let m of a)if(Array.isArray(m)){let[b,x]=m,y=[],w=Object.keys(x?.modifiers??{});x?.types?.some(({type:_})=>_==="color")&&w.push(...Object.keys(e.tailwindConfig.theme.opacity??{}));let k={modifiers:w},S=d.includeMetadata&&w.length>0;for(let[_,E]of Object.entries(x?.values??{})){if(E==null)continue;let I=Kr(b,_);if(p.push(S?[I,k]:I),x?.supportsNegativeValues&&Ke(E)){let q=Kr(b,`-${_}`);y.push(S?[q,k]:q)}}p.push(...y)}else p.push(m);return p},e.getVariants=function(){let d=Math.random().toString(36).substring(7).toUpperCase(),p=[];for(let[m,b]of e.variantOptions.entries())b.variantInfo!==go.Base&&p.push({name:m,isArbitrary:b.type===Symbol.for("MATCH_VARIANT"),values:Object.keys(b.values??{}),hasDash:m!=="@",selectors({modifier:x,value:y}={}){let w=`TAILWINDPLACEHOLDER${d}`,k=j.rule({selector:`.${w}`}),S=j.root({nodes:[k.clone()]}),_=S.toString(),E=(e.variantMap.get(m)??[]).flatMap(([oe,he])=>he),I=[];for(let oe of E){let he=[],ui={args:{modifier:x,value:b.values?.[y]??y},separator:e.tailwindConfig.separator,modifySelectors(Ce){return S.each(ts=>{ts.type==="rule"&&(ts.selectors=ts.selectors.map(mu=>Ce({get className(){return lo(mu)},selector:mu})))}),S},format(Ce){he.push(Ce)},wrap(Ce){he.push(`@${Ce.name} ${Ce.params} { & }`)},container:S},fi=oe(ui);if(he.length>0&&I.push(he),Array.isArray(fi))for(let Ce of fi)he=[],Ce(ui),I.push(he)}let q=[],R=S.toString();_!==R&&(S.walkRules(oe=>{let he=oe.selector,ui=(0,ho.default)(fi=>{fi.walkClasses(Ce=>{Ce.value=`${m}${e.tailwindConfig.separator}${Ce.value}`})}).processSync(he);q.push(he.replace(ui,"&").replace(w,"&"))}),S.walkAtRules(oe=>{q.push(`@${oe.name} (${oe.params}) { & }`)}));let J=!(y in(b.values??{})),ue=b[Zr]??{},de=(()=>!(J||ue.respectPrefix===!1))();I=I.map(oe=>oe.map(he=>({format:he,respectPrefix:de}))),q=q.map(oe=>({format:oe,respectPrefix:de}));let De={candidate:w,context:e},ee=I.map(oe=>kn(`.${w}`,Vt(oe,De),De).replace(`.${w}`,"&").replace("{ & }","").trim());return q.length>0&&ee.push(Vt(q,De).toString().replace(`.${w}`,"&")),ee}});return p}}function Qd(r,e){!r.classCache.has(e)||(r.notClassCache.add(e),r.classCache.delete(e),r.applyClassCache.delete(e),r.candidateRuleMap.delete(e),r.candidateRuleCache.delete(e),r.stylesheetCache=null)}function W2(r,e){let t=e.raws.tailwind.candidate;if(!!t){for(let i of r.ruleCache)i[1].raws.tailwind.candidate===t&&r.ruleCache.delete(i);Qd(r,t)}}function bo(r,e=[],t=j.root()){let i={disposables:[],ruleCache:new Set,candidateRuleCache:new Map,classCache:new Map,applyClassCache:new Map,notClassCache:new Set(r.blocklist??[]),postCssNodeCache:new Map,candidateRuleMap:new Map,tailwindConfig:r,changedContent:e,variantMap:new Map,stylesheetCache:null,variantOptions:new Map,markInvalidUtilityCandidate:a=>Qd(i,a),markInvalidUtilityNode:a=>W2(i,a)},n=V2(i,t);return U2(n,i),i}function Jd(r,e,t,i,n,a){let s=e.opts.from,o=i!==null;Pe.DEBUG&&console.log("Source path:",s);let u;if(o&&Ut.has(s))u=Ut.get(s);else if(ii.has(n)){let p=ii.get(n);ut.get(p).add(s),Ut.set(s,p),u=p}let c=Fd(s,r);if(u){let[p,m]=Hd([...a],Dn(u));if(!p&&!c)return[u,!1,m]}if(Ut.has(s)){let p=Ut.get(s);if(ut.has(p)&&(ut.get(p).delete(s),ut.get(p).size===0)){ut.delete(p);for(let[m,b]of ii)b===p&&ii.delete(m);for(let m of p.disposables.splice(0))m(p)}}Pe.DEBUG&&console.log("Setting up new context...");let f=bo(t,[],r);Object.assign(f,{userConfigPath:i});let[,d]=Hd([...a],Dn(f));return ii.set(n,f),Ut.set(s,f),ut.has(f)||ut.set(f,new Set),ut.get(f).add(s),[f,!0,d]}var Ud,ho,Zr,mo,go,wo,Ut,ii,ut,ei=C(()=>{l();ze();Cs();st();Ud=X(Gs()),ho=X(Me());Jr();Qa();dn();At();$t();Xa();hr();Cd();lt();lt();gi();Oe();di();eo();On();Ld();Vd();je();so();Zr=Symbol(),mo={AddVariant:Symbol.for("ADD_VARIANT"),MatchVariant:Symbol.for("MATCH_VARIANT")},go={Base:1<<0,Dynamic:1<<1};wo=new WeakMap;Ut=Ad,ii=_d,ut=vn});function vo(r){return r.ignore?[]:r.glob?h.env.ROLLUP_WATCH==="true"?[{type:"dependency",file:r.base}]:[{type:"dir-dependency",dir:r.base,glob:r.glob}]:[{type:"dependency",file:r.base}]}var Xd=C(()=>{l()});function Kd(r,e){return{handler:r,config:e}}var Zd,eh=C(()=>{l();Kd.withOptions=function(r,e=()=>({})){let t=function(i){return{__options:i,handler:r(i),config:e(i)}};return t.__isOptionsFunction=!0,t.__pluginFunction=r,t.__configFunction=e,t};Zd=Kd});var In={};Ae(In,{default:()=>G2});var G2,qn=C(()=>{l();eh();G2=Zd});var rh=v((M6,th)=>{l();var H2=(qn(),In).default,Y2={overflow:"hidden",display:"-webkit-box","-webkit-box-orient":"vertical"},Q2=H2(function({matchUtilities:r,addUtilities:e,theme:t,variants:i}){let n=t("lineClamp");r({"line-clamp":a=>({...Y2,"-webkit-line-clamp":`${a}`})},{values:n}),e([{".line-clamp-none":{"-webkit-line-clamp":"unset"}}],i("lineClamp"))},{theme:{lineClamp:{1:"1",2:"2",3:"3",4:"4",5:"5",6:"6"}},variants:{lineClamp:["responsive"]}});th.exports=Q2});function xo(r){r.content.files.length===0&&F.warn("content-problems",["The `content` option in your Tailwind CSS configuration is missing or empty.","Configure your content sources or your generated CSS will be missing styles.","https://tailwindcss.com/docs/content-configuration"]);try{let e=rh();r.plugins.includes(e)&&(F.warn("line-clamp-in-core",["As of Tailwind CSS v3.3, the `@tailwindcss/line-clamp` plugin is now included by default.","Remove it from the `plugins` array in your configuration to eliminate this warning."]),r.plugins=r.plugins.filter(t=>t!==e))}catch{}return r}var ih=C(()=>{l();Oe()});var nh,sh=C(()=>{l();nh=()=>!1});var Rn,ah=C(()=>{l();Rn={sync:r=>[].concat(r),generateTasks:r=>[{dynamic:!1,base:".",negative:[],positive:[].concat(r),patterns:[].concat(r)}],escapePath:r=>r}});var ko,oh=C(()=>{l();ko=r=>r});var lh,uh=C(()=>{l();lh=()=>""});function fh(r){let e=r,t=lh(r);return t!=="."&&(e=r.substr(t.length),e.charAt(0)==="/"&&(e=e.substr(1))),e.substr(0,2)==="./"&&(e=e.substr(2)),e.charAt(0)==="/"&&(e=e.substr(1)),{base:t,glob:e}}var ch=C(()=>{l();uh()});function ph(r,e){let t=e.content.files;t=t.filter(o=>typeof o=="string"),t=t.map(ko);let i=Rn.generateTasks(t),n=[],a=[];for(let o of i)n.push(...o.positive.map(u=>dh(u,!1))),a.push(...o.negative.map(u=>dh(u,!0)));let s=[...n,...a];return s=X2(r,s),s=s.flatMap(K2),s=s.map(J2),s}function dh(r,e){let t={original:r,base:r,ignore:e,pattern:r,glob:null};return nh(r)&&Object.assign(t,fh(r)),t}function J2(r){let e=ko(r.base);return e=Rn.escapePath(e),r.pattern=r.glob?`${e}/${r.glob}`:e,r.pattern=r.ignore?`!${r.pattern}`:r.pattern,r}function X2(r,e){let t=[];return r.userConfigPath&&r.tailwindConfig.content.relative&&(t=[Z.dirname(r.userConfigPath)]),e.map(i=>(i.base=Z.resolve(...t,i.base),i))}function K2(r){let e=[r];try{let t=te.realpathSync(r.base);t!==r.base&&e.push({...r,base:t})}catch{}return e}function hh(r,e,t){let i=r.tailwindConfig.content.files.filter(s=>typeof s.raw=="string").map(({raw:s,extension:o="html"})=>({content:s,extension:o})),[n,a]=Z2(e,t);for(let s of n){let o=Z.extname(s).slice(1);i.push({file:s,extension:o})}return[i,a]}function Z2(r,e){let t=r.map(s=>s.pattern),i=new Map,n=new Set;Pe.DEBUG&&console.time("Finding changed files");let a=Rn.sync(t,{absolute:!0});for(let s of a){let o=e.get(s)||-1/0,u=te.statSync(s).mtimeMs;u>o&&(n.add(s),i.set(s,u))}return Pe.DEBUG&&console.timeEnd("Finding changed files"),[n,i]}var mh=C(()=>{l();ze();bt();sh();ah();oh();ch();lt()});function gh(){}var yh=C(()=>{l()});function iC(r,e){for(let t of e){let i=`${r}${t}`;if(te.existsSync(i)&&te.statSync(i).isFile())return i}for(let t of e){let i=`${r}/index${t}`;if(te.existsSync(i))return i}return null}function*wh(r,e,t,i=Z.extname(r)){let n=iC(Z.resolve(e,r),eC.includes(i)?tC:rC);if(n===null||t.has(n))return;t.add(n),yield n,e=Z.dirname(n),i=Z.extname(n);let a=te.readFileSync(n,"utf-8");for(let s of[...a.matchAll(/import[\s\S]*?['"](.{3,}?)['"]/gi),...a.matchAll(/import[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi),...a.matchAll(/require\(['"`](.+)['"`]\)/gi)])!s[1].startsWith(".")||(yield*wh(s[1],e,t,i))}function So(r){return r===null?new Set:new Set(wh(r,Z.dirname(r),new Set))}var eC,tC,rC,bh=C(()=>{l();ze();bt();eC=[".js",".cjs",".mjs"],tC=["",".js",".cjs",".mjs",".ts",".cts",".mts",".jsx",".tsx"],rC=["",".ts",".cts",".mts",".tsx",".js",".cjs",".mjs",".jsx"]});function nC(r,e){if(Co.has(r))return Co.get(r);let t=ph(r,e);return Co.set(r,t).get(r)}function sC(r){let e=ks(r);if(e!==null){let[i,n,a,s]=xh.get(e)||[],o=So(e),u=!1,c=new Map;for(let p of o){let m=te.statSync(p).mtimeMs;c.set(p,m),(!s||!s.has(p)||m>s.get(p))&&(u=!0)}if(!u)return[i,e,n,a];for(let p of o)delete yu.cache[p];let f=xo(gr(gh(e))),d=pi(f);return xh.set(e,[f,d,o,c]),[f,e,d,o]}let t=gr(r?.config??r??{});return t=xo(t),[t,null,pi(t),[]]}function Ao(r){return({tailwindDirectives:e,registerDependency:t})=>(i,n)=>{let[a,s,o,u]=sC(r),c=new Set(u);if(e.size>0){c.add(n.opts.from);for(let b of n.messages)b.type==="dependency"&&c.add(b.file)}let[f,,d]=Jd(i,n,a,s,o,c),p=Dn(f),m=nC(f,a);if(e.size>0){for(let y of m)for(let w of vo(y))t(w);let[b,x]=hh(f,m,p);for(let y of b)f.changedContent.push(y);for(let[y,w]of x.entries())d.set(y,w)}for(let b of u)t({type:"dependency",file:b});for(let[b,x]of d.entries())p.set(b,x);return f}}var vh,xh,Co,kh=C(()=>{l();ze();vh=X(rs());ku();xs();cf();ei();Xd();ih();mh();yh();bh();xh=new vh.default({maxSize:100}),Co=new WeakMap});function _o(r){let e=new Set,t=new Set,i=new Set;if(r.walkAtRules(n=>{n.name==="apply"&&i.add(n),n.name==="import"&&(n.params==='"tailwindcss/base"'||n.params==="'tailwindcss/base'"?(n.name="tailwind",n.params="base"):n.params==='"tailwindcss/components"'||n.params==="'tailwindcss/components'"?(n.name="tailwind",n.params="components"):n.params==='"tailwindcss/utilities"'||n.params==="'tailwindcss/utilities'"?(n.name="tailwind",n.params="utilities"):(n.params==='"tailwindcss/screens"'||n.params==="'tailwindcss/screens'"||n.params==='"tailwindcss/variants"'||n.params==="'tailwindcss/variants'")&&(n.name="tailwind",n.params="variants")),n.name==="tailwind"&&(n.params==="screens"&&(n.params="variants"),e.add(n.params)),["layer","responsive","variants"].includes(n.name)&&(["responsive","variants"].includes(n.name)&&F.warn(`${n.name}-at-rule-deprecated`,[`The \`@${n.name}\` directive has been deprecated in Tailwind CSS v3.0.`,"Use `@layer utilities` or `@layer components` instead.","https://tailwindcss.com/docs/upgrade-guide#replace-variants-with-layer"]),t.add(n))}),!e.has("base")||!e.has("components")||!e.has("utilities")){for(let n of t)if(n.name==="layer"&&["base","components","utilities"].includes(n.params)){if(!e.has(n.params))throw n.error(`\`@layer ${n.params}\` is used but no matching \`@tailwind ${n.params}\` directive is present.`)}else if(n.name==="responsive"){if(!e.has("utilities"))throw n.error("`@responsive` is used but `@tailwind utilities` is missing.")}else if(n.name==="variants"&&!e.has("utilities"))throw n.error("`@variants` is used but `@tailwind utilities` is missing.")}return{tailwindDirectives:e,applyDirectives:i}}var Sh=C(()=>{l();Oe()});function St(r,e=void 0,t=void 0){return r.map(i=>{let n=i.clone();return t!==void 0&&(n.raws.tailwind={...n.raws.tailwind,...t}),e!==void 0&&Ch(n,a=>{if(a.raws.tailwind?.preserveSource===!0&&a.source)return!1;a.source=e}),n})}function Ch(r,e){e(r)!==!1&&r.each?.(t=>Ch(t,e))}var Ah=C(()=>{l()});function Oo(r){return r=Array.isArray(r)?r:[r],r=r.map(e=>e instanceof RegExp?e.source:e),r.join("")}function ye(r){return new RegExp(Oo(r),"g")}function ft(r){return`(?:${r.map(Oo).join("|")})`}function Eo(r){return`(?:${Oo(r)})?`}function Oh(r){return r&&aC.test(r)?r.replace(_h,"\\$&"):r||""}var _h,aC,Eh=C(()=>{l();_h=/[\\^$.*+?()[\]{}|]/g,aC=RegExp(_h.source)});function Th(r){let e=Array.from(oC(r));return t=>{let i=[];for(let n of e)for(let a of t.match(n)??[])i.push(fC(a));return i}}function*oC(r){let e=r.tailwindConfig.separator,t=r.tailwindConfig.prefix!==""?Eo(ye([/-?/,Oh(r.tailwindConfig.prefix)])):"",i=ft([/\[[^\s:'"`]+:[^\s\[\]]+\]/,/\[[^\s:'"`\]]+:[^\s]+?\[[^\s]+\][^\s]+?\]/,ye([ft([/-?(?:\w+)/,/@(?:\w+)/]),Eo(ft([ye([ft([/-(?:\w+-)*\['[^\s]+'\]/,/-(?:\w+-)*\["[^\s]+"\]/,/-(?:\w+-)*\[`[^\s]+`\]/,/-(?:\w+-)*\[(?:[^\s\[\]]+\[[^\s\[\]]+\])*[^\s:\[\]]+\]/]),/(?![{([]])/,/(?:\/[^\s'"`\\><$]*)?/]),ye([ft([/-(?:\w+-)*\['[^\s]+'\]/,/-(?:\w+-)*\["[^\s]+"\]/,/-(?:\w+-)*\[`[^\s]+`\]/,/-(?:\w+-)*\[(?:[^\s\[\]]+\[[^\s\[\]]+\])*[^\s\[\]]+\]/]),/(?![{([]])/,/(?:\/[^\s'"`\\$]*)?/]),/[-\/][^\s'"`\\$={><]*/]))])]),n=[ft([ye([/@\[[^\s"'`]+\](\/[^\s"'`]+)?/,e]),ye([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]\/[\w_-]+/,e]),ye([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]/,e]),ye([/[^\s"'`\[\\]+/,e])]),ft([ye([/([^\s"'`\[\\]+-)?\[[^\s`]+\]\/[\w_-]+/,e]),ye([/([^\s"'`\[\\]+-)?\[[^\s`]+\]/,e]),ye([/[^\s`\[\\]+/,e])])];for(let a of n)yield ye(["((?=((",a,")+))\\2)?",/!?/,t,i]);yield/[^<>"'`\s.(){}[\]#=%$]*[^<>"'`\s.(){}[\]#=%:$]/g}function fC(r){if(!r.includes("-["))return r;let e=0,t=[],i=r.matchAll(lC);i=Array.from(i).flatMap(n=>{let[,...a]=n;return a.map((s,o)=>Object.assign([],n,{index:n.index+o,0:s}))});for(let n of i){let a=n[0],s=t[t.length-1];if(a===s?t.pop():(a==="'"||a==='"'||a==="`")&&t.push(a),!s){if(a==="["){e++;continue}else if(a==="]"){e--;continue}if(e<0)return r.substring(0,n.index-1);if(e===0&&!uC.test(a))return r.substring(0,n.index)}}return r}var lC,uC,Ph=C(()=>{l();Eh();lC=/([\[\]'"`])([^\[\]'"`])?/g,uC=/[^"'`\s<>\]]+/});function cC(r,e){let t=r.tailwindConfig.content.extract;return t[e]||t.DEFAULT||Ih[e]||Ih.DEFAULT(r)}function pC(r,e){let t=r.content.transform;return t[e]||t.DEFAULT||qh[e]||qh.DEFAULT}function dC(r,e,t,i){ni.has(e)||ni.set(e,new Dh.default({maxSize:25e3}));for(let n of r.split(` +`))if(n=n.trim(),!i.has(n))if(i.add(n),ni.get(e).has(n))for(let a of ni.get(e).get(n))t.add(a);else{let a=e(n).filter(o=>o!=="!*"),s=new Set(a);for(let o of s)t.add(o);ni.get(e).set(n,s)}}function hC(r,e){let t=e.offsets.sort(r),i={base:new Set,defaults:new Set,components:new Set,utilities:new Set,variants:new Set};for(let[n,a]of t)i[n.layer].add(a);return i}function To(r){return async e=>{let t={base:null,components:null,utilities:null,variants:null};if(e.walkAtRules(y=>{y.name==="tailwind"&&Object.keys(t).includes(y.params)&&(t[y.params]=y)}),Object.values(t).every(y=>y===null))return e;let i=new Set([...r.candidates??[],He]),n=new Set;Ye.DEBUG&&console.time("Reading changed files");let a=[];for(let y of r.changedContent){let w=pC(r.tailwindConfig,y.extension),k=cC(r,y.extension);a.push([y,{transformer:w,extractor:k}])}let s=500;for(let y=0;y{S=k?await te.promises.readFile(k,"utf8"):S,dC(_(S),E,i,n)}))}Ye.DEBUG&&console.timeEnd("Reading changed files");let o=r.classCache.size;Ye.DEBUG&&console.time("Generate rules"),Ye.DEBUG&&console.time("Sorting candidates");let u=new Set([...i].sort((y,w)=>y===w?0:y{let w=y.raws.tailwind?.parentLayer;return w==="components"?t.components!==null:w==="utilities"?t.utilities!==null:!0});t.variants?(t.variants.before(St(b,t.variants.source,{layer:"variants"})),t.variants.remove()):b.length>0&&e.append(St(b,e.source,{layer:"variants"})),e.source.end=e.source.end??e.source.start;let x=b.some(y=>y.raws.tailwind?.parentLayer==="utilities");t.utilities&&p.size===0&&!x&&F.warn("content-problems",["No utility classes were detected in your source files. If this is unexpected, double-check the `content` option in your Tailwind CSS configuration.","https://tailwindcss.com/docs/content-configuration"]),Ye.DEBUG&&(console.log("Potential classes: ",i.size),console.log("Active contexts: ",vn.size)),r.changedContent=[],e.walkAtRules("layer",y=>{Object.keys(t).includes(y.params)&&y.remove()})}}var Dh,Ye,Ih,qh,ni,Rh=C(()=>{l();ze();Dh=X(rs());lt();On();Oe();Ah();Ph();Ye=Pe,Ih={DEFAULT:Th},qh={DEFAULT:r=>r,svelte:r=>r.replace(/(?:^|\s)class:/g," ")};ni=new WeakMap});function Bn(r){let e=new Map;j.root({nodes:[r.clone()]}).walkRules(a=>{(0,Mn.default)(s=>{s.walkClasses(o=>{let u=o.parent.toString(),c=e.get(u);c||e.set(u,c=new Set),c.add(o.value)})}).processSync(a.selector)});let i=Array.from(e.values(),a=>Array.from(a)),n=i.flat();return Object.assign(n,{groups:i})}function Po(r){return mC.astSync(r)}function Mh(r,e){let t=new Set;for(let i of r)t.add(i.split(e).pop());return Array.from(t)}function Bh(r,e){let t=r.tailwindConfig.prefix;return typeof t=="function"?t(e):t+e}function*Fh(r){for(yield r;r.parent;)yield r.parent,r=r.parent}function gC(r,e={}){let t=r.nodes;r.nodes=[];let i=r.clone(e);return r.nodes=t,i}function yC(r){for(let e of Fh(r))if(r!==e){if(e.type==="root")break;r=gC(e,{nodes:[r]})}return r}function wC(r,e){let t=new Map;return r.walkRules(i=>{for(let s of Fh(i))if(s.raws.tailwind?.layer!==void 0)return;let n=yC(i),a=e.offsets.create("user");for(let s of Bn(i)){let o=t.get(s)||[];t.set(s,o),o.push([{layer:"user",sort:a,important:!1},n])}}),t}function bC(r,e){for(let t of r){if(e.notClassCache.has(t)||e.applyClassCache.has(t))continue;if(e.classCache.has(t)){e.applyClassCache.set(t,e.classCache.get(t).map(([n,a])=>[n,a.clone()]));continue}let i=Array.from(fo(t,e));if(i.length===0){e.notClassCache.add(t);continue}e.applyClassCache.set(t,i)}return e.applyClassCache}function vC(r){let e=null;return{get:t=>(e=e||r(),e.get(t)),has:t=>(e=e||r(),e.has(t))}}function xC(r){return{get:e=>r.flatMap(t=>t.get(e)||[]),has:e=>r.some(t=>t.has(e))}}function Lh(r){let e=r.split(/[\s\t\n]+/g);return e[e.length-1]==="!important"?[e.slice(0,-1),!0]:[e,!1]}function Nh(r,e,t){let i=new Set,n=[];if(r.walkAtRules("apply",u=>{let[c]=Lh(u.params);for(let f of c)i.add(f);n.push(u)}),n.length===0)return;let a=xC([t,bC(i,e)]);function s(u,c,f){let d=Po(u),p=Po(c),b=Po(`.${ce(f)}`).nodes[0].nodes[0];return d.each(x=>{let y=new Set;p.each(w=>{let k=!1;w=w.clone(),w.walkClasses(S=>{S.value===b.value&&(k||(S.replaceWith(...x.nodes.map(_=>_.clone())),y.add(w),k=!0))})});for(let w of y){let k=[[]];for(let S of w.nodes)S.type==="combinator"?(k.push(S),k.push([])):k[k.length-1].push(S);w.nodes=[];for(let S of k)Array.isArray(S)&&S.sort((_,E)=>_.type==="tag"&&E.type==="class"?-1:_.type==="class"&&E.type==="tag"?1:_.type==="class"&&E.type==="pseudo"&&E.value.startsWith("::")?-1:_.type==="pseudo"&&_.value.startsWith("::")&&E.type==="class"?1:0),w.nodes=w.nodes.concat(S)}x.replaceWith(...y)}),d.toString()}let o=new Map;for(let u of n){let[c]=o.get(u.parent)||[[],u.source];o.set(u.parent,[c,u.source]);let[f,d]=Lh(u.params);if(u.parent.type==="atrule"){if(u.parent.name==="screen"){let p=u.parent.params;throw u.error(`@apply is not supported within nested at-rules like @screen. We suggest you write this as @apply ${f.map(m=>`${p}:${m}`).join(" ")} instead.`)}throw u.error(`@apply is not supported within nested at-rules like @${u.parent.name}. You can fix this by un-nesting @${u.parent.name}.`)}for(let p of f){if([Bh(e,"group"),Bh(e,"peer")].includes(p))throw u.error(`@apply should not be used with the '${p}' utility`);if(!a.has(p))throw u.error(`The \`${p}\` class does not exist. If \`${p}\` is a custom class, make sure it is defined within a \`@layer\` directive.`);let m=a.get(p);for(let[,b]of m)b.type!=="atrule"&&b.walkRules(()=>{throw u.error([`The \`${p}\` class cannot be used with \`@apply\` because \`@apply\` does not currently support nested CSS.`,"Rewrite the selector without nesting or configure the `tailwindcss/nesting` plugin:","https://tailwindcss.com/docs/using-with-preprocessors#nesting"].join(` +`))});c.push([p,d,m])}}for(let[u,[c,f]]of o){let d=[];for(let[m,b,x]of c){let y=[m,...Mh([m],e.tailwindConfig.separator)];for(let[w,k]of x){let S=Bn(u),_=Bn(k);if(_=_.groups.filter(R=>R.some(J=>y.includes(J))).flat(),_=_.concat(Mh(_,e.tailwindConfig.separator)),S.some(R=>_.includes(R)))throw k.error(`You cannot \`@apply\` the \`${m}\` utility here because it creates a circular dependency.`);let I=j.root({nodes:[k.clone()]});I.walk(R=>{R.source=f}),(k.type!=="atrule"||k.type==="atrule"&&k.name!=="keyframes")&&I.walkRules(R=>{if(!Bn(R).some(ee=>ee===m)){R.remove();return}let J=typeof e.tailwindConfig.important=="string"?e.tailwindConfig.important:null,de=u.raws.tailwind!==void 0&&J&&u.selector.indexOf(J)===0?u.selector.slice(J.length):u.selector;de===""&&(de=u.selector),R.selector=s(de,R.selector,m),J&&de!==u.selector&&(R.selector=Sn(R.selector,J)),R.walkDecls(ee=>{ee.important=w.important||b});let De=(0,Mn.default)().astSync(R.selector);De.each(ee=>jt(ee)),R.selector=De.toString()}),!!I.nodes[0]&&d.push([w.sort,I.nodes[0]])}}let p=e.offsets.sort(d).map(m=>m[1]);u.after(p)}for(let u of n)u.parent.nodes.length>1?u.remove():u.parent.remove();Nh(r,e,t)}function Do(r){return e=>{let t=vC(()=>wC(e,r));Nh(e,r,t)}}var Mn,mC,$h=C(()=>{l();st();Mn=X(Me());On();$t();oo();xn();mC=(0,Mn.default)()});var zh=v((I4,Fn)=>{l();(function(){"use strict";function r(i,n,a){if(!i)return null;r.caseSensitive||(i=i.toLowerCase());var s=r.threshold===null?null:r.threshold*i.length,o=r.thresholdAbsolute,u;s!==null&&o!==null?u=Math.min(s,o):s!==null?u=s:o!==null?u=o:u=null;var c,f,d,p,m,b=n.length;for(m=0;ma)return a+1;var u=[],c,f,d,p,m;for(c=0;c<=o;c++)u[c]=[c];for(f=0;f<=s;f++)u[0][f]=f;for(c=1;c<=o;c++){for(d=e,p=1,c>a&&(p=c-a),m=o+1,m>a+c&&(m=a+c),f=1;f<=s;f++)fm?u[c][f]=a+1:n.charAt(c-1)===i.charAt(f-1)?u[c][f]=u[c-1][f-1]:u[c][f]=Math.min(u[c-1][f-1]+1,Math.min(u[c][f-1]+1,u[c-1][f]+1)),u[c][f]a)return a+1}return u[o][s]}})()});var Vh=v((q4,jh)=>{l();var Io="(".charCodeAt(0),qo=")".charCodeAt(0),Ln="'".charCodeAt(0),Ro='"'.charCodeAt(0),Mo="\\".charCodeAt(0),Wt="/".charCodeAt(0),Bo=",".charCodeAt(0),Fo=":".charCodeAt(0),Nn="*".charCodeAt(0),kC="u".charCodeAt(0),SC="U".charCodeAt(0),CC="+".charCodeAt(0),AC=/^[a-f0-9?-]+$/i;jh.exports=function(r){for(var e=[],t=r,i,n,a,s,o,u,c,f,d=0,p=t.charCodeAt(d),m=t.length,b=[{nodes:e}],x=0,y,w="",k="",S="";d{l();Uh.exports=function r(e,t,i){var n,a,s,o;for(n=0,a=e.length;n{l();function Gh(r,e){var t=r.type,i=r.value,n,a;return e&&(a=e(r))!==void 0?a:t==="word"||t==="space"?i:t==="string"?(n=r.quote||"",n+i+(r.unclosed?"":n)):t==="comment"?"/*"+i+(r.unclosed?"":"*/"):t==="div"?(r.before||"")+i+(r.after||""):Array.isArray(r.nodes)?(n=Hh(r.nodes,e),t!=="function"?n:i+"("+(r.before||"")+n+(r.after||"")+(r.unclosed?"":")")):i}function Hh(r,e){var t,i;if(Array.isArray(r)){for(t="",i=r.length-1;~i;i-=1)t=Gh(r[i],e)+t;return t}return Gh(r,e)}Yh.exports=Hh});var Xh=v((B4,Jh)=>{l();var $n="-".charCodeAt(0),zn="+".charCodeAt(0),Lo=".".charCodeAt(0),_C="e".charCodeAt(0),OC="E".charCodeAt(0);function EC(r){var e=r.charCodeAt(0),t;if(e===zn||e===$n){if(t=r.charCodeAt(1),t>=48&&t<=57)return!0;var i=r.charCodeAt(2);return t===Lo&&i>=48&&i<=57}return e===Lo?(t=r.charCodeAt(1),t>=48&&t<=57):e>=48&&e<=57}Jh.exports=function(r){var e=0,t=r.length,i,n,a;if(t===0||!EC(r))return!1;for(i=r.charCodeAt(e),(i===zn||i===$n)&&e++;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),i===Lo&&n>=48&&n<=57)for(e+=2;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),a=r.charCodeAt(e+2),(i===_C||i===OC)&&(n>=48&&n<=57||(n===zn||n===$n)&&a>=48&&a<=57))for(e+=n===zn||n===$n?3:2;e57));)e+=1;return{number:r.slice(0,e),unit:r.slice(e)}}});var tm=v((F4,em)=>{l();var TC=Vh(),Kh=Wh(),Zh=Qh();function ct(r){return this instanceof ct?(this.nodes=TC(r),this):new ct(r)}ct.prototype.toString=function(){return Array.isArray(this.nodes)?Zh(this.nodes):""};ct.prototype.walk=function(r,e){return Kh(this.nodes,r,e),this};ct.unit=Xh();ct.walk=Kh;ct.stringify=Zh;em.exports=ct});function $o(r){return typeof r=="object"&&r!==null}function PC(r,e){let t=Ze(e);do if(t.pop(),(0,si.default)(r,t)!==void 0)break;while(t.length);return t.length?t:void 0}function Gt(r){return typeof r=="string"?r:r.reduce((e,t,i)=>t.includes(".")?`${e}[${t}]`:i===0?t:`${e}.${t}`,"")}function im(r){return r.map(e=>`'${e}'`).join(", ")}function nm(r){return im(Object.keys(r))}function zo(r,e,t,i={}){let n=Array.isArray(e)?Gt(e):e.replace(/^['"]+|['"]+$/g,""),a=Array.isArray(e)?e:Ze(n),s=(0,si.default)(r.theme,a,t);if(s===void 0){let u=`'${n}' does not exist in your theme config.`,c=a.slice(0,-1),f=(0,si.default)(r.theme,c);if($o(f)){let d=Object.keys(f).filter(m=>zo(r,[...c,m]).isValid),p=(0,rm.default)(a[a.length-1],d);p?u+=` Did you mean '${Gt([...c,p])}'?`:d.length>0&&(u+=` '${Gt(c)}' has the following valid keys: ${im(d)}`)}else{let d=PC(r.theme,n);if(d){let p=(0,si.default)(r.theme,d);$o(p)?u+=` '${Gt(d)}' has the following keys: ${nm(p)}`:u+=` '${Gt(d)}' is not an object.`}else u+=` Your theme has the following top-level keys: ${nm(r.theme)}`}return{isValid:!1,error:u}}if(!(typeof s=="string"||typeof s=="number"||typeof s=="function"||s instanceof String||s instanceof Number||Array.isArray(s))){let u=`'${n}' was found but does not resolve to a string.`;if($o(s)){let c=Object.keys(s).filter(f=>zo(r,[...a,f]).isValid);c.length&&(u+=` Did you mean something like '${Gt([...a,c[0]])}'?`)}return{isValid:!1,error:u}}let[o]=a;return{isValid:!0,value:Ge(o)(s,i)}}function DC(r,e,t){e=e.map(n=>sm(r,n,t));let i=[""];for(let n of e)n.type==="div"&&n.value===","?i.push(""):i[i.length-1]+=No.default.stringify(n);return i}function sm(r,e,t){if(e.type==="function"&&t[e.value]!==void 0){let i=DC(r,e.nodes,t);e.type="word",e.value=t[e.value](r,...i)}return e}function IC(r,e,t){return Object.keys(t).some(n=>e.includes(`${n}(`))?(0,No.default)(e).walk(n=>{sm(r,n,t)}).toString():e}function*RC(r){r=r.replace(/^['"]+|['"]+$/g,"");let e=r.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]+))$/),t;yield[r,void 0],e&&(r=e[1],t=e[2],yield[r,t])}function MC(r,e,t){let i=Array.from(RC(e)).map(([n,a])=>Object.assign(zo(r,n,t,{opacityValue:a}),{resolvedPath:n,alpha:a}));return i.find(n=>n.isValid)??i[0]}function am(r){let e=r.tailwindConfig,t={theme:(i,n,...a)=>{let{isValid:s,value:o,error:u,alpha:c}=MC(e,n,a.length?a:void 0);if(!s){let p=i.parent,m=p?.raws.tailwind?.candidate;if(p&&m!==void 0){r.markInvalidUtilityNode(p),p.remove(),F.warn("invalid-theme-key-in-class",[`The utility \`${m}\` contains an invalid theme value and was not generated.`]);return}throw i.error(u)}let f=Ot(o),d=f!==void 0&&typeof f=="function";return(c!==void 0||d)&&(c===void 0&&(c=1),o=Ie(f,c,f)),o},screen:(i,n)=>{n=n.replace(/^['"]+/g,"").replace(/['"]+$/g,"");let s=ot(e.theme.screens).find(({name:o})=>o===n);if(!s)throw i.error(`The '${n}' screen does not exist in your theme.`);return at(s)}};return i=>{i.walk(n=>{let a=qC[n.type];a!==void 0&&(n[a]=IC(n,n[a],t))})}}var si,rm,No,qC,om=C(()=>{l();si=X(Gs()),rm=X(zh());Jr();No=X(tm());wn();mn();gi();fr();hr();Oe();qC={atrule:"params",decl:"value"}});function lm({tailwindConfig:{theme:r}}){return function(e){e.walkAtRules("screen",t=>{let i=t.params,a=ot(r.screens).find(({name:s})=>s===i);if(!a)throw t.error(`No \`${i}\` screen found.`);t.name="media",t.params=at(a)})}}var um=C(()=>{l();wn();mn()});function BC(r){let e=r.filter(o=>o.type!=="pseudo"||o.nodes.length>0?!0:o.value.startsWith("::")||[":before",":after",":first-line",":first-letter"].includes(o.value)).reverse(),t=new Set(["tag","class","id","attribute"]),i=e.findIndex(o=>t.has(o.type));if(i===-1)return e.reverse().join("").trim();let n=e[i],a=fm[n.type]?fm[n.type](n):n;e=e.slice(0,i);let s=e.findIndex(o=>o.type==="combinator"&&o.value===">");return s!==-1&&(e.splice(0,s),e.unshift(jn.default.universal())),[a,...e.reverse()].join("").trim()}function LC(r){return jo.has(r)||jo.set(r,FC.transformSync(r)),jo.get(r)}function Vo({tailwindConfig:r}){return e=>{let t=new Map,i=new Set;if(e.walkAtRules("defaults",n=>{if(n.nodes&&n.nodes.length>0){i.add(n);return}let a=n.params;t.has(a)||t.set(a,new Set),t.get(a).add(n.parent),n.remove()}),K(r,"optimizeUniversalDefaults"))for(let n of i){let a=new Map,s=t.get(n.params)??[];for(let o of s)for(let u of LC(o.selector)){let c=u.includes(":-")||u.includes("::-")||u.includes(":has")?u:"__DEFAULT__",f=a.get(c)??new Set;a.set(c,f),f.add(u)}if(K(r,"optimizeUniversalDefaults")){if(a.size===0){n.remove();continue}for(let[,o]of a){let u=j.rule({source:n.source});u.selectors=[...o],u.append(n.nodes.map(c=>c.clone())),n.before(u)}}n.remove()}else if(i.size){let n=j.rule({selectors:["*","::before","::after"]});for(let s of i)n.append(s.nodes),n.parent||s.before(n),n.source||(n.source=s.source),s.remove();let a=n.clone({selectors:["::backdrop"]});n.after(a)}}}var jn,fm,FC,jo,cm=C(()=>{l();st();jn=X(Me());je();fm={id(r){return jn.default.attribute({attribute:"id",operator:"=",value:r.value,quoteMark:'"'})}};FC=(0,jn.default)(r=>r.map(e=>{let t=e.split(i=>i.type==="combinator"&&i.value===" ").pop();return BC(t)})),jo=new Map});function Uo(){function r(e){let t=null;e.each(i=>{if(!NC.has(i.type)){t=null;return}if(t===null){t=i;return}let n=pm[i.type];i.type==="atrule"&&i.name==="font-face"?t=i:n.every(a=>(i[a]??"").replace(/\s+/g," ")===(t[a]??"").replace(/\s+/g," "))?(i.nodes&&t.append(i.nodes),i.remove()):t=i}),e.each(i=>{i.type==="atrule"&&r(i)})}return e=>{r(e)}}var pm,NC,dm=C(()=>{l();pm={atrule:["name","params"],rule:["selector"]},NC=new Set(Object.keys(pm))});function Wo(){return r=>{r.walkRules(e=>{let t=new Map,i=new Set([]),n=new Map;e.walkDecls(a=>{if(a.parent===e){if(t.has(a.prop)){if(t.get(a.prop).value===a.value){i.add(t.get(a.prop)),t.set(a.prop,a);return}n.has(a.prop)||n.set(a.prop,new Set),n.get(a.prop).add(t.get(a.prop)),n.get(a.prop).add(a)}t.set(a.prop,a)}});for(let a of i)a.remove();for(let a of n.values()){let s=new Map;for(let o of a){let u=zC(o.value);u!==null&&(s.has(u)||s.set(u,new Set),s.get(u).add(o))}for(let o of s.values()){let u=Array.from(o).slice(0,-1);for(let c of u)c.remove()}}})}}function zC(r){let e=/^-?\d*.?\d+([\w%]+)?$/g.exec(r);return e?e[1]??$C:null}var $C,hm=C(()=>{l();$C=Symbol("unitless-number")});function jC(r){if(!r.walkAtRules)return;let e=new Set;if(r.walkAtRules("apply",t=>{e.add(t.parent)}),e.size!==0)for(let t of e){let i=[],n=[];for(let a of t.nodes)a.type==="atrule"&&a.name==="apply"?(n.length>0&&(i.push(n),n=[]),i.push([a])):n.push(a);if(n.length>0&&i.push(n),i.length!==1){for(let a of[...i].reverse()){let s=t.clone({nodes:[]});s.append(a),t.after(s)}t.remove()}}}function Vn(){return r=>{jC(r)}}var mm=C(()=>{l()});function Un(r){return async function(e,t){let{tailwindDirectives:i,applyDirectives:n}=_o(e);Vn()(e,t);let a=r({tailwindDirectives:i,applyDirectives:n,registerDependency(s){t.messages.push({plugin:"tailwindcss",parent:t.opts.from,...s})},createContext(s,o){return bo(s,o,e)}})(e,t);if(a.tailwindConfig.separator==="-")throw new Error("The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead.");Iu(a.tailwindConfig),await To(a)(e,t),Vn()(e,t),Do(a)(e,t),am(a)(e,t),lm(a)(e,t),Vo(a)(e,t),Uo(a)(e,t),Wo(a)(e,t)}}var gm=C(()=>{l();Sh();Rh();$h();om();um();cm();dm();hm();mm();ei();je()});function ym(r,e){let t=null,i=null;return r.walkAtRules("config",n=>{if(i=n.source?.input.file??e.opts.from??null,i===null)throw n.error("The `@config` directive cannot be used without setting `from` in your PostCSS config.");if(t)throw n.error("Only one `@config` directive is allowed per file.");let a=n.params.match(/(['"])(.*?)\1/);if(!a)throw n.error("A path is required when using the `@config` directive.");let s=a[2];if(Z.isAbsolute(s))throw n.error("The `@config` directive cannot be used with an absolute path.");if(t=Z.resolve(Z.dirname(i),s),!te.existsSync(t))throw n.error(`The config file at "${s}" does not exist. Make sure the path is correct and the file exists.`);n.remove()}),t||null}var wm=C(()=>{l();ze();bt()});var bm=v((vD,Go)=>{l();kh();gm();lt();wm();Go.exports=function(e){return{postcssPlugin:"tailwindcss",plugins:[Pe.DEBUG&&function(t){return console.log(` +`),console.time("JIT TOTAL"),t},async function(t,i){e=ym(t,i)??e;let n=Ao(e);if(t.type==="document"){let a=t.nodes.filter(s=>s.type==="root");for(let s of a)s.type==="root"&&await Un(n)(s,i);return}await Un(n)(t,i)},Pe.DEBUG&&function(t){return console.timeEnd("JIT TOTAL"),console.log(` +`),t}].filter(Boolean)}};Go.exports.postcss=!0});var xm=v((xD,vm)=>{l();vm.exports=bm()});var Ho=v((kD,km)=>{l();km.exports=()=>["and_chr 114","and_uc 15.5","chrome 114","chrome 113","chrome 109","edge 114","firefox 114","ios_saf 16.5","ios_saf 16.4","ios_saf 16.3","ios_saf 16.1","opera 99","safari 16.5","samsung 21"]});var Wn={};Ae(Wn,{agents:()=>VC,feature:()=>UC});function UC(){return{status:"cr",title:"CSS Feature Queries",stats:{ie:{"6":"n","7":"n","8":"n","9":"n","10":"n","11":"n","5.5":"n"},edge:{"12":"y","13":"y","14":"y","15":"y","16":"y","17":"y","18":"y","79":"y","80":"y","81":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","101":"y","102":"y","103":"y","104":"y","105":"y","106":"y","107":"y","108":"y","109":"y","110":"y","111":"y","112":"y","113":"y","114":"y"},firefox:{"2":"n","3":"n","4":"n","5":"n","6":"n","7":"n","8":"n","9":"n","10":"n","11":"n","12":"n","13":"n","14":"n","15":"n","16":"n","17":"n","18":"n","19":"n","20":"n","21":"n","22":"y","23":"y","24":"y","25":"y","26":"y","27":"y","28":"y","29":"y","30":"y","31":"y","32":"y","33":"y","34":"y","35":"y","36":"y","37":"y","38":"y","39":"y","40":"y","41":"y","42":"y","43":"y","44":"y","45":"y","46":"y","47":"y","48":"y","49":"y","50":"y","51":"y","52":"y","53":"y","54":"y","55":"y","56":"y","57":"y","58":"y","59":"y","60":"y","61":"y","62":"y","63":"y","64":"y","65":"y","66":"y","67":"y","68":"y","69":"y","70":"y","71":"y","72":"y","73":"y","74":"y","75":"y","76":"y","77":"y","78":"y","79":"y","80":"y","81":"y","82":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","101":"y","102":"y","103":"y","104":"y","105":"y","106":"y","107":"y","108":"y","109":"y","110":"y","111":"y","112":"y","113":"y","114":"y","115":"y","116":"y","117":"y","3.5":"n","3.6":"n"},chrome:{"4":"n","5":"n","6":"n","7":"n","8":"n","9":"n","10":"n","11":"n","12":"n","13":"n","14":"n","15":"n","16":"n","17":"n","18":"n","19":"n","20":"n","21":"n","22":"n","23":"n","24":"n","25":"n","26":"n","27":"n","28":"y","29":"y","30":"y","31":"y","32":"y","33":"y","34":"y","35":"y","36":"y","37":"y","38":"y","39":"y","40":"y","41":"y","42":"y","43":"y","44":"y","45":"y","46":"y","47":"y","48":"y","49":"y","50":"y","51":"y","52":"y","53":"y","54":"y","55":"y","56":"y","57":"y","58":"y","59":"y","60":"y","61":"y","62":"y","63":"y","64":"y","65":"y","66":"y","67":"y","68":"y","69":"y","70":"y","71":"y","72":"y","73":"y","74":"y","75":"y","76":"y","77":"y","78":"y","79":"y","80":"y","81":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","101":"y","102":"y","103":"y","104":"y","105":"y","106":"y","107":"y","108":"y","109":"y","110":"y","111":"y","112":"y","113":"y","114":"y","115":"y","116":"y","117":"y"},safari:{"4":"n","5":"n","6":"n","7":"n","8":"n","9":"y","10":"y","11":"y","12":"y","13":"y","14":"y","15":"y","17":"y","9.1":"y","10.1":"y","11.1":"y","12.1":"y","13.1":"y","14.1":"y","15.1":"y","15.2-15.3":"y","15.4":"y","15.5":"y","15.6":"y","16.0":"y","16.1":"y","16.2":"y","16.3":"y","16.4":"y","16.5":"y","16.6":"y",TP:"y","3.1":"n","3.2":"n","5.1":"n","6.1":"n","7.1":"n"},opera:{"9":"n","11":"n","12":"n","15":"y","16":"y","17":"y","18":"y","19":"y","20":"y","21":"y","22":"y","23":"y","24":"y","25":"y","26":"y","27":"y","28":"y","29":"y","30":"y","31":"y","32":"y","33":"y","34":"y","35":"y","36":"y","37":"y","38":"y","39":"y","40":"y","41":"y","42":"y","43":"y","44":"y","45":"y","46":"y","47":"y","48":"y","49":"y","50":"y","51":"y","52":"y","53":"y","54":"y","55":"y","56":"y","57":"y","58":"y","60":"y","62":"y","63":"y","64":"y","65":"y","66":"y","67":"y","68":"y","69":"y","70":"y","71":"y","72":"y","73":"y","74":"y","75":"y","76":"y","77":"y","78":"y","79":"y","80":"y","81":"y","82":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","12.1":"y","9.5-9.6":"n","10.0-10.1":"n","10.5":"n","10.6":"n","11.1":"n","11.5":"n","11.6":"n"},ios_saf:{"8":"n","17":"y","9.0-9.2":"y","9.3":"y","10.0-10.2":"y","10.3":"y","11.0-11.2":"y","11.3-11.4":"y","12.0-12.1":"y","12.2-12.5":"y","13.0-13.1":"y","13.2":"y","13.3":"y","13.4-13.7":"y","14.0-14.4":"y","14.5-14.8":"y","15.0-15.1":"y","15.2-15.3":"y","15.4":"y","15.5":"y","15.6":"y","16.0":"y","16.1":"y","16.2":"y","16.3":"y","16.4":"y","16.5":"y","16.6":"y","3.2":"n","4.0-4.1":"n","4.2-4.3":"n","5.0-5.1":"n","6.0-6.1":"n","7.0-7.1":"n","8.1-8.4":"n"},op_mini:{all:"y"},android:{"3":"n","4":"n","114":"y","4.4":"y","4.4.3-4.4.4":"y","2.1":"n","2.2":"n","2.3":"n","4.1":"n","4.2-4.3":"n"},bb:{"7":"n","10":"n"},op_mob:{"10":"n","11":"n","12":"n","73":"y","11.1":"n","11.5":"n","12.1":"n"},and_chr:{"114":"y"},and_ff:{"115":"y"},ie_mob:{"10":"n","11":"n"},and_uc:{"15.5":"y"},samsung:{"4":"y","20":"y","21":"y","5.0-5.4":"y","6.2-6.4":"y","7.2-7.4":"y","8.2":"y","9.2":"y","10.1":"y","11.1-11.2":"y","12.0":"y","13.0":"y","14.0":"y","15.0":"y","16.0":"y","17.0":"y","18.0":"y","19.0":"y"},and_qq:{"13.1":"y"},baidu:{"13.18":"y"},kaios:{"2.5":"y","3.0-3.1":"y"}}}}var VC,Gn=C(()=>{l();VC={ie:{prefix:"ms"},edge:{prefix:"webkit",prefix_exceptions:{"12":"ms","13":"ms","14":"ms","15":"ms","16":"ms","17":"ms","18":"ms"}},firefox:{prefix:"moz"},chrome:{prefix:"webkit"},safari:{prefix:"webkit"},opera:{prefix:"webkit",prefix_exceptions:{"9":"o","11":"o","12":"o","9.5-9.6":"o","10.0-10.1":"o","10.5":"o","10.6":"o","11.1":"o","11.5":"o","11.6":"o","12.1":"o"}},ios_saf:{prefix:"webkit"},op_mini:{prefix:"o"},android:{prefix:"webkit"},bb:{prefix:"webkit"},op_mob:{prefix:"o",prefix_exceptions:{"73":"webkit"}},and_chr:{prefix:"webkit"},and_ff:{prefix:"moz"},ie_mob:{prefix:"ms"},and_uc:{prefix:"webkit",prefix_exceptions:{"15.5":"webkit"}},samsung:{prefix:"webkit"},and_qq:{prefix:"webkit"},baidu:{prefix:"webkit"},kaios:{prefix:"moz"}}});var Sm=v(()=>{l()});var le=v((AD,pt)=>{l();var{list:Yo}=ge();pt.exports.error=function(r){let e=new Error(r);throw e.autoprefixer=!0,e};pt.exports.uniq=function(r){return[...new Set(r)]};pt.exports.removeNote=function(r){return r.includes(" ")?r.split(" ")[0]:r};pt.exports.escapeRegexp=function(r){return r.replace(/[$()*+-.?[\\\]^{|}]/g,"\\$&")};pt.exports.regexp=function(r,e=!0){return e&&(r=this.escapeRegexp(r)),new RegExp(`(^|[\\s,(])(${r}($|[\\s(,]))`,"gi")};pt.exports.editList=function(r,e){let t=Yo.comma(r),i=e(t,[]);if(t===i)return r;let n=r.match(/,\s*/);return n=n?n[0]:", ",i.join(n)};pt.exports.splitSelector=function(r){return Yo.comma(r).map(e=>Yo.space(e).map(t=>t.split(/(?=\.|#)/g)))}});var dt=v((_D,_m)=>{l();var WC=Ho(),Cm=(Gn(),Wn).agents,GC=le(),Am=class{static prefixes(){if(this.prefixesCache)return this.prefixesCache;this.prefixesCache=[];for(let e in Cm)this.prefixesCache.push(`-${Cm[e].prefix}-`);return this.prefixesCache=GC.uniq(this.prefixesCache).sort((e,t)=>t.length-e.length),this.prefixesCache}static withPrefix(e){return this.prefixesRegexp||(this.prefixesRegexp=new RegExp(this.prefixes().join("|"))),this.prefixesRegexp.test(e)}constructor(e,t,i,n){this.data=e,this.options=i||{},this.browserslistOpts=n||{},this.selected=this.parse(t)}parse(e){let t={};for(let i in this.browserslistOpts)t[i]=this.browserslistOpts[i];return t.path=this.options.from,WC(e,t)}prefix(e){let[t,i]=e.split(" "),n=this.data[t],a=n.prefix_exceptions&&n.prefix_exceptions[i];return a||(a=n.prefix),`-${a}-`}isSelected(e){return this.selected.includes(e)}};_m.exports=Am});var ai=v((OD,Om)=>{l();Om.exports={prefix(r){let e=r.match(/^(-\w+-)/);return e?e[0]:""},unprefixed(r){return r.replace(/^-\w+-/,"")}}});var Ht=v((ED,Tm)=>{l();var HC=dt(),Em=ai(),YC=le();function Qo(r,e){let t=new r.constructor;for(let i of Object.keys(r||{})){let n=r[i];i==="parent"&&typeof n=="object"?e&&(t[i]=e):i==="source"||i===null?t[i]=n:Array.isArray(n)?t[i]=n.map(a=>Qo(a,t)):i!=="_autoprefixerPrefix"&&i!=="_autoprefixerValues"&&i!=="proxyCache"&&(typeof n=="object"&&n!==null&&(n=Qo(n,t)),t[i]=n)}return t}var Hn=class{static hack(e){return this.hacks||(this.hacks={}),e.names.map(t=>(this.hacks[t]=e,this.hacks[t]))}static load(e,t,i){let n=this.hacks&&this.hacks[e];return n?new n(e,t,i):new this(e,t,i)}static clone(e,t){let i=Qo(e);for(let n in t)i[n]=t[n];return i}constructor(e,t,i){this.prefixes=t,this.name=e,this.all=i}parentPrefix(e){let t;return typeof e._autoprefixerPrefix!="undefined"?t=e._autoprefixerPrefix:e.type==="decl"&&e.prop[0]==="-"?t=Em.prefix(e.prop):e.type==="root"?t=!1:e.type==="rule"&&e.selector.includes(":-")&&/:(-\w+-)/.test(e.selector)?t=e.selector.match(/:(-\w+-)/)[1]:e.type==="atrule"&&e.name[0]==="-"?t=Em.prefix(e.name):t=this.parentPrefix(e.parent),HC.prefixes().includes(t)||(t=!1),e._autoprefixerPrefix=t,e._autoprefixerPrefix}process(e,t){if(!this.check(e))return;let i=this.parentPrefix(e),n=this.prefixes.filter(s=>!i||i===YC.removeNote(s)),a=[];for(let s of n)this.add(e,s,a.concat([s]),t)&&a.push(s);return a}clone(e,t){return Hn.clone(e,t)}};Tm.exports=Hn});var M=v((TD,Im)=>{l();var QC=Ht(),JC=dt(),Pm=le(),Dm=class extends QC{check(){return!0}prefixed(e,t){return t+e}normalize(e){return e}otherPrefixes(e,t){for(let i of JC.prefixes())if(i!==t&&e.includes(i))return!0;return!1}set(e,t){return e.prop=this.prefixed(e.prop,t),e}needCascade(e){return e._autoprefixerCascade||(e._autoprefixerCascade=this.all.options.cascade!==!1&&e.raw("before").includes(` +`)),e._autoprefixerCascade}maxPrefixed(e,t){if(t._autoprefixerMax)return t._autoprefixerMax;let i=0;for(let n of e)n=Pm.removeNote(n),n.length>i&&(i=n.length);return t._autoprefixerMax=i,t._autoprefixerMax}calcBefore(e,t,i=""){let a=this.maxPrefixed(e,t)-Pm.removeNote(i).length,s=t.raw("before");return a>0&&(s+=Array(a).fill(" ").join("")),s}restoreBefore(e){let t=e.raw("before").split(` +`),i=t[t.length-1];this.all.group(e).up(n=>{let a=n.raw("before").split(` +`),s=a[a.length-1];s.lengths.prop===n.prop&&s.value===n.value)))return this.needCascade(e)&&(n.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,n)}isAlready(e,t){let i=this.all.group(e).up(n=>n.prop===t);return i||(i=this.all.group(e).down(n=>n.prop===t)),i}add(e,t,i,n){let a=this.prefixed(e.prop,t);if(!(this.isAlready(e,a)||this.otherPrefixes(e.value,t)))return this.insert(e,t,i,n)}process(e,t){if(!this.needCascade(e)){super.process(e,t);return}let i=super.process(e,t);!i||!i.length||(this.restoreBefore(e),e.raws.before=this.calcBefore(i,e))}old(e,t){return[this.prefixed(e,t)]}};Im.exports=Dm});var Rm=v((PD,qm)=>{l();qm.exports=function r(e){return{mul:t=>new r(e*t),div:t=>new r(e/t),simplify:()=>new r(e),toString:()=>e.toString()}}});var Fm=v((DD,Bm)=>{l();var XC=Rm(),KC=Ht(),Jo=le(),ZC=/(min|max)-resolution\s*:\s*\d*\.?\d+(dppx|dpcm|dpi|x)/gi,eA=/(min|max)-resolution(\s*:\s*)(\d*\.?\d+)(dppx|dpcm|dpi|x)/i,Mm=class extends KC{prefixName(e,t){return e==="-moz-"?t+"--moz-device-pixel-ratio":e+t+"-device-pixel-ratio"}prefixQuery(e,t,i,n,a){return n=new XC(n),a==="dpi"?n=n.div(96):a==="dpcm"&&(n=n.mul(2.54).div(96)),n=n.simplify(),e==="-o-"&&(n=n.n+"/"+n.d),this.prefixName(e,t)+i+n}clean(e){if(!this.bad){this.bad=[];for(let t of this.prefixes)this.bad.push(this.prefixName(t,"min")),this.bad.push(this.prefixName(t,"max"))}e.params=Jo.editList(e.params,t=>t.filter(i=>this.bad.every(n=>!i.includes(n))))}process(e){let t=this.parentPrefix(e),i=t?[t]:this.prefixes;e.params=Jo.editList(e.params,(n,a)=>{for(let s of n){if(!s.includes("min-resolution")&&!s.includes("max-resolution")){a.push(s);continue}for(let o of i){let u=s.replace(ZC,c=>{let f=c.match(eA);return this.prefixQuery(o,f[1],f[2],f[3],f[4])});a.push(u)}a.push(s)}return Jo.uniq(a)})}};Bm.exports=Mm});var Nm=v((ID,Lm)=>{l();var Xo="(".charCodeAt(0),Ko=")".charCodeAt(0),Yn="'".charCodeAt(0),Zo='"'.charCodeAt(0),el="\\".charCodeAt(0),Yt="/".charCodeAt(0),tl=",".charCodeAt(0),rl=":".charCodeAt(0),Qn="*".charCodeAt(0),tA="u".charCodeAt(0),rA="U".charCodeAt(0),iA="+".charCodeAt(0),nA=/^[a-f0-9?-]+$/i;Lm.exports=function(r){for(var e=[],t=r,i,n,a,s,o,u,c,f,d=0,p=t.charCodeAt(d),m=t.length,b=[{nodes:e}],x=0,y,w="",k="",S="";d{l();$m.exports=function r(e,t,i){var n,a,s,o;for(n=0,a=e.length;n{l();function jm(r,e){var t=r.type,i=r.value,n,a;return e&&(a=e(r))!==void 0?a:t==="word"||t==="space"?i:t==="string"?(n=r.quote||"",n+i+(r.unclosed?"":n)):t==="comment"?"/*"+i+(r.unclosed?"":"*/"):t==="div"?(r.before||"")+i+(r.after||""):Array.isArray(r.nodes)?(n=Vm(r.nodes,e),t!=="function"?n:i+"("+(r.before||"")+n+(r.after||"")+(r.unclosed?"":")")):i}function Vm(r,e){var t,i;if(Array.isArray(r)){for(t="",i=r.length-1;~i;i-=1)t=jm(r[i],e)+t;return t}return jm(r,e)}Um.exports=Vm});var Hm=v((MD,Gm)=>{l();var Jn="-".charCodeAt(0),Xn="+".charCodeAt(0),il=".".charCodeAt(0),sA="e".charCodeAt(0),aA="E".charCodeAt(0);function oA(r){var e=r.charCodeAt(0),t;if(e===Xn||e===Jn){if(t=r.charCodeAt(1),t>=48&&t<=57)return!0;var i=r.charCodeAt(2);return t===il&&i>=48&&i<=57}return e===il?(t=r.charCodeAt(1),t>=48&&t<=57):e>=48&&e<=57}Gm.exports=function(r){var e=0,t=r.length,i,n,a;if(t===0||!oA(r))return!1;for(i=r.charCodeAt(e),(i===Xn||i===Jn)&&e++;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),i===il&&n>=48&&n<=57)for(e+=2;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),a=r.charCodeAt(e+2),(i===sA||i===aA)&&(n>=48&&n<=57||(n===Xn||n===Jn)&&a>=48&&a<=57))for(e+=n===Xn||n===Jn?3:2;e57));)e+=1;return{number:r.slice(0,e),unit:r.slice(e)}}});var Kn=v((BD,Jm)=>{l();var lA=Nm(),Ym=zm(),Qm=Wm();function ht(r){return this instanceof ht?(this.nodes=lA(r),this):new ht(r)}ht.prototype.toString=function(){return Array.isArray(this.nodes)?Qm(this.nodes):""};ht.prototype.walk=function(r,e){return Ym(this.nodes,r,e),this};ht.unit=Hm();ht.walk=Ym;ht.stringify=Qm;Jm.exports=ht});var tg=v((FD,eg)=>{l();var{list:uA}=ge(),Xm=Kn(),fA=dt(),Km=ai(),Zm=class{constructor(e){this.props=["transition","transition-property"],this.prefixes=e}add(e,t){let i,n,a=this.prefixes.add[e.prop],s=this.ruleVendorPrefixes(e),o=s||a&&a.prefixes||[],u=this.parse(e.value),c=u.map(m=>this.findProp(m)),f=[];if(c.some(m=>m[0]==="-"))return;for(let m of u){if(n=this.findProp(m),n[0]==="-")continue;let b=this.prefixes.add[n];if(!(!b||!b.prefixes))for(i of b.prefixes){if(s&&!s.some(y=>i.includes(y)))continue;let x=this.prefixes.prefixed(n,i);x!=="-ms-transform"&&!c.includes(x)&&(this.disabled(n,i)||f.push(this.clone(n,x,m)))}}u=u.concat(f);let d=this.stringify(u),p=this.stringify(this.cleanFromUnprefixed(u,"-webkit-"));if(o.includes("-webkit-")&&this.cloneBefore(e,`-webkit-${e.prop}`,p),this.cloneBefore(e,e.prop,p),o.includes("-o-")){let m=this.stringify(this.cleanFromUnprefixed(u,"-o-"));this.cloneBefore(e,`-o-${e.prop}`,m)}for(i of o)if(i!=="-webkit-"&&i!=="-o-"){let m=this.stringify(this.cleanOtherPrefixes(u,i));this.cloneBefore(e,i+e.prop,m)}d!==e.value&&!this.already(e,e.prop,d)&&(this.checkForWarning(t,e),e.cloneBefore(),e.value=d)}findProp(e){let t=e[0].value;if(/^\d/.test(t)){for(let[i,n]of e.entries())if(i!==0&&n.type==="word")return n.value}return t}already(e,t,i){return e.parent.some(n=>n.prop===t&&n.value===i)}cloneBefore(e,t,i){this.already(e,t,i)||e.cloneBefore({prop:t,value:i})}checkForWarning(e,t){if(t.prop!=="transition-property")return;let i=!1,n=!1;t.parent.each(a=>{if(a.type!=="decl"||a.prop.indexOf("transition-")!==0)return;let s=uA.comma(a.value);if(a.prop==="transition-property"){s.forEach(o=>{let u=this.prefixes.add[o];u&&u.prefixes&&u.prefixes.length>0&&(i=!0)});return}return n=n||s.length>1,!1}),i&&n&&t.warn(e,"Replace transition-property to transition, because Autoprefixer could not support any cases of transition-property and other transition-*")}remove(e){let t=this.parse(e.value);t=t.filter(s=>{let o=this.prefixes.remove[this.findProp(s)];return!o||!o.remove});let i=this.stringify(t);if(e.value===i)return;if(t.length===0){e.remove();return}let n=e.parent.some(s=>s.prop===e.prop&&s.value===i),a=e.parent.some(s=>s!==e&&s.prop===e.prop&&s.value.length>i.length);if(n||a){e.remove();return}e.value=i}parse(e){let t=Xm(e),i=[],n=[];for(let a of t.nodes)n.push(a),a.type==="div"&&a.value===","&&(i.push(n),n=[]);return i.push(n),i.filter(a=>a.length>0)}stringify(e){if(e.length===0)return"";let t=[];for(let i of e)i[i.length-1].type!=="div"&&i.push(this.div(e)),t=t.concat(i);return t[0].type==="div"&&(t=t.slice(1)),t[t.length-1].type==="div"&&(t=t.slice(0,-2+1||void 0)),Xm.stringify({nodes:t})}clone(e,t,i){let n=[],a=!1;for(let s of i)!a&&s.type==="word"&&s.value===e?(n.push({type:"word",value:t}),a=!0):n.push(s);return n}div(e){for(let t of e)for(let i of t)if(i.type==="div"&&i.value===",")return i;return{type:"div",value:",",after:" "}}cleanOtherPrefixes(e,t){return e.filter(i=>{let n=Km.prefix(this.findProp(i));return n===""||n===t})}cleanFromUnprefixed(e,t){let i=e.map(a=>this.findProp(a)).filter(a=>a.slice(0,t.length)===t).map(a=>this.prefixes.unprefixed(a)),n=[];for(let a of e){let s=this.findProp(a),o=Km.prefix(s);!i.includes(s)&&(o===t||o==="")&&n.push(a)}return n}disabled(e,t){let i=["order","justify-content","align-self","align-content"];if(e.includes("flex")||i.includes(e)){if(this.prefixes.options.flexbox===!1)return!0;if(this.prefixes.options.flexbox==="no-2009")return t.includes("2009")}}ruleVendorPrefixes(e){let{parent:t}=e;if(t.type!=="rule")return!1;if(!t.selector.includes(":-"))return!1;let i=fA.prefixes().filter(n=>t.selector.includes(":"+n));return i.length>0?i:!1}};eg.exports=Zm});var Qt=v((LD,ig)=>{l();var cA=le(),rg=class{constructor(e,t,i,n){this.unprefixed=e,this.prefixed=t,this.string=i||t,this.regexp=n||cA.regexp(t)}check(e){return e.includes(this.string)?!!e.match(this.regexp):!1}};ig.exports=rg});var ke=v((ND,sg)=>{l();var pA=Ht(),dA=Qt(),hA=ai(),mA=le(),ng=class extends pA{static save(e,t){let i=t.prop,n=[];for(let a in t._autoprefixerValues){let s=t._autoprefixerValues[a];if(s===t.value)continue;let o,u=hA.prefix(i);if(u==="-pie-")continue;if(u===a){o=t.value=s,n.push(o);continue}let c=e.prefixed(i,a),f=t.parent;if(!f.every(b=>b.prop!==c)){n.push(o);continue}let d=s.replace(/\s+/," ");if(f.some(b=>b.prop===t.prop&&b.value.replace(/\s+/," ")===d)){n.push(o);continue}let m=this.clone(t,{value:s});o=t.parent.insertBefore(t,m),n.push(o)}return n}check(e){let t=e.value;return t.includes(this.name)?!!t.match(this.regexp()):!1}regexp(){return this.regexpCache||(this.regexpCache=mA.regexp(this.name))}replace(e,t){return e.replace(this.regexp(),`$1${t}$2`)}value(e){return e.raws.value&&e.raws.value.value===e.value?e.raws.value.raw:e.value}add(e,t){e._autoprefixerValues||(e._autoprefixerValues={});let i=e._autoprefixerValues[t]||this.value(e),n;do if(n=i,i=this.replace(i,t),i===!1)return;while(i!==n);e._autoprefixerValues[t]=i}old(e){return new dA(this.name,e+this.name)}};sg.exports=ng});var mt=v(($D,ag)=>{l();ag.exports={}});var sl=v((zD,ug)=>{l();var og=Kn(),gA=ke(),yA=mt().insertAreas,wA=/(^|[^-])linear-gradient\(\s*(top|left|right|bottom)/i,bA=/(^|[^-])radial-gradient\(\s*\d+(\w*|%)\s+\d+(\w*|%)\s*,/i,vA=/(!\s*)?autoprefixer:\s*ignore\s+next/i,xA=/(!\s*)?autoprefixer\s*grid:\s*(on|off|(no-)?autoplace)/i,kA=["width","height","min-width","max-width","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size"];function nl(r){return r.parent.some(e=>e.prop==="grid-template"||e.prop==="grid-template-areas")}function SA(r){let e=r.parent.some(i=>i.prop==="grid-template-rows"),t=r.parent.some(i=>i.prop==="grid-template-columns");return e&&t}var lg=class{constructor(e){this.prefixes=e}add(e,t){let i=this.prefixes.add["@resolution"],n=this.prefixes.add["@keyframes"],a=this.prefixes.add["@viewport"],s=this.prefixes.add["@supports"];e.walkAtRules(f=>{if(f.name==="keyframes"){if(!this.disabled(f,t))return n&&n.process(f)}else if(f.name==="viewport"){if(!this.disabled(f,t))return a&&a.process(f)}else if(f.name==="supports"){if(this.prefixes.options.supports!==!1&&!this.disabled(f,t))return s.process(f)}else if(f.name==="media"&&f.params.includes("-resolution")&&!this.disabled(f,t))return i&&i.process(f)}),e.walkRules(f=>{if(!this.disabled(f,t))return this.prefixes.add.selectors.map(d=>d.process(f,t))});function o(f){return f.parent.nodes.some(d=>{if(d.type!=="decl")return!1;let p=d.prop==="display"&&/(inline-)?grid/.test(d.value),m=d.prop.startsWith("grid-template"),b=/^grid-([A-z]+-)?gap/.test(d.prop);return p||m||b})}function u(f){return f.parent.some(d=>d.prop==="display"&&/(inline-)?flex/.test(d.value))}let c=this.gridStatus(e,t)&&this.prefixes.add["grid-area"]&&this.prefixes.add["grid-area"].prefixes;return e.walkDecls(f=>{if(this.disabledDecl(f,t))return;let d=f.parent,p=f.prop,m=f.value;if(p==="grid-row-span"){t.warn("grid-row-span is not part of final Grid Layout. Use grid-row.",{node:f});return}else if(p==="grid-column-span"){t.warn("grid-column-span is not part of final Grid Layout. Use grid-column.",{node:f});return}else if(p==="display"&&m==="box"){t.warn("You should write display: flex by final spec instead of display: box",{node:f});return}else if(p==="text-emphasis-position")(m==="under"||m==="over")&&t.warn("You should use 2 values for text-emphasis-position For example, `under left` instead of just `under`.",{node:f});else if(/^(align|justify|place)-(items|content)$/.test(p)&&u(f))(m==="start"||m==="end")&&t.warn(`${m} value has mixed support, consider using flex-${m} instead`,{node:f});else if(p==="text-decoration-skip"&&m==="ink")t.warn("Replace text-decoration-skip: ink to text-decoration-skip-ink: auto, because spec had been changed",{node:f});else{if(c&&this.gridStatus(f,t))if(f.value==="subgrid"&&t.warn("IE does not support subgrid",{node:f}),/^(align|justify|place)-items$/.test(p)&&o(f)){let x=p.replace("-items","-self");t.warn(`IE does not support ${p} on grid containers. Try using ${x} on child elements instead: ${f.parent.selector} > * { ${x}: ${f.value} }`,{node:f})}else if(/^(align|justify|place)-content$/.test(p)&&o(f))t.warn(`IE does not support ${f.prop} on grid containers`,{node:f});else if(p==="display"&&f.value==="contents"){t.warn("Please do not use display: contents; if you have grid setting enabled",{node:f});return}else if(f.prop==="grid-gap"){let x=this.gridStatus(f,t);x==="autoplace"&&!SA(f)&&!nl(f)?t.warn("grid-gap only works if grid-template(-areas) is being used or both rows and columns have been declared and cells have not been manually placed inside the explicit grid",{node:f}):(x===!0||x==="no-autoplace")&&!nl(f)&&t.warn("grid-gap only works if grid-template(-areas) is being used",{node:f})}else if(p==="grid-auto-columns"){t.warn("grid-auto-columns is not supported by IE",{node:f});return}else if(p==="grid-auto-rows"){t.warn("grid-auto-rows is not supported by IE",{node:f});return}else if(p==="grid-auto-flow"){let x=d.some(w=>w.prop==="grid-template-rows"),y=d.some(w=>w.prop==="grid-template-columns");nl(f)?t.warn("grid-auto-flow is not supported by IE",{node:f}):m.includes("dense")?t.warn("grid-auto-flow: dense is not supported by IE",{node:f}):!x&&!y&&t.warn("grid-auto-flow works only if grid-template-rows and grid-template-columns are present in the same rule",{node:f});return}else if(m.includes("auto-fit")){t.warn("auto-fit value is not supported by IE",{node:f,word:"auto-fit"});return}else if(m.includes("auto-fill")){t.warn("auto-fill value is not supported by IE",{node:f,word:"auto-fill"});return}else p.startsWith("grid-template")&&m.includes("[")&&t.warn("Autoprefixer currently does not support line names. Try using grid-template-areas instead.",{node:f,word:"["});if(m.includes("radial-gradient"))if(bA.test(f.value))t.warn("Gradient has outdated direction syntax. New syntax is like `closest-side at 0 0` instead of `0 0, closest-side`.",{node:f});else{let x=og(m);for(let y of x.nodes)if(y.type==="function"&&y.value==="radial-gradient")for(let w of y.nodes)w.type==="word"&&(w.value==="cover"?t.warn("Gradient has outdated direction syntax. Replace `cover` to `farthest-corner`.",{node:f}):w.value==="contain"&&t.warn("Gradient has outdated direction syntax. Replace `contain` to `closest-side`.",{node:f}))}m.includes("linear-gradient")&&wA.test(m)&&t.warn("Gradient has outdated direction syntax. New syntax is like `to left` instead of `right`.",{node:f})}kA.includes(f.prop)&&(f.value.includes("-fill-available")||(f.value.includes("fill-available")?t.warn("Replace fill-available to stretch, because spec had been changed",{node:f}):f.value.includes("fill")&&og(m).nodes.some(y=>y.type==="word"&&y.value==="fill")&&t.warn("Replace fill to stretch, because spec had been changed",{node:f})));let b;if(f.prop==="transition"||f.prop==="transition-property")return this.prefixes.transition.add(f,t);if(f.prop==="align-self"){if(this.displayType(f)!=="grid"&&this.prefixes.options.flexbox!==!1&&(b=this.prefixes.add["align-self"],b&&b.prefixes&&b.process(f)),this.gridStatus(f,t)!==!1&&(b=this.prefixes.add["grid-row-align"],b&&b.prefixes))return b.process(f,t)}else if(f.prop==="justify-self"){if(this.gridStatus(f,t)!==!1&&(b=this.prefixes.add["grid-column-align"],b&&b.prefixes))return b.process(f,t)}else if(f.prop==="place-self"){if(b=this.prefixes.add["place-self"],b&&b.prefixes&&this.gridStatus(f,t)!==!1)return b.process(f,t)}else if(b=this.prefixes.add[f.prop],b&&b.prefixes)return b.process(f,t)}),this.gridStatus(e,t)&&yA(e,this.disabled),e.walkDecls(f=>{if(this.disabledValue(f,t))return;let d=this.prefixes.unprefixed(f.prop),p=this.prefixes.values("add",d);if(Array.isArray(p))for(let m of p)m.process&&m.process(f,t);gA.save(this.prefixes,f)})}remove(e,t){let i=this.prefixes.remove["@resolution"];e.walkAtRules((n,a)=>{this.prefixes.remove[`@${n.name}`]?this.disabled(n,t)||n.parent.removeChild(a):n.name==="media"&&n.params.includes("-resolution")&&i&&i.clean(n)});for(let n of this.prefixes.remove.selectors)e.walkRules((a,s)=>{n.check(a)&&(this.disabled(a,t)||a.parent.removeChild(s))});return e.walkDecls((n,a)=>{if(this.disabled(n,t))return;let s=n.parent,o=this.prefixes.unprefixed(n.prop);if((n.prop==="transition"||n.prop==="transition-property")&&this.prefixes.transition.remove(n),this.prefixes.remove[n.prop]&&this.prefixes.remove[n.prop].remove){let u=this.prefixes.group(n).down(c=>this.prefixes.normalize(c.prop)===o);if(o==="flex-flow"&&(u=!0),n.prop==="-webkit-box-orient"){let c={"flex-direction":!0,"flex-flow":!0};if(!n.parent.some(f=>c[f.prop]))return}if(u&&!this.withHackValue(n)){n.raw("before").includes(` +`)&&this.reduceSpaces(n),s.removeChild(a);return}}for(let u of this.prefixes.values("remove",o)){if(!u.check||!u.check(n.value))continue;if(o=u.unprefixed,this.prefixes.group(n).down(f=>f.value.includes(o))){s.removeChild(a);return}}})}withHackValue(e){return e.prop==="-webkit-background-clip"&&e.value==="text"}disabledValue(e,t){return this.gridStatus(e,t)===!1&&e.type==="decl"&&e.prop==="display"&&e.value.includes("grid")||this.prefixes.options.flexbox===!1&&e.type==="decl"&&e.prop==="display"&&e.value.includes("flex")||e.type==="decl"&&e.prop==="content"?!0:this.disabled(e,t)}disabledDecl(e,t){if(this.gridStatus(e,t)===!1&&e.type==="decl"&&(e.prop.includes("grid")||e.prop==="justify-items"))return!0;if(this.prefixes.options.flexbox===!1&&e.type==="decl"){let i=["order","justify-content","align-items","align-content"];if(e.prop.includes("flex")||i.includes(e.prop))return!0}return this.disabled(e,t)}disabled(e,t){if(!e)return!1;if(e._autoprefixerDisabled!==void 0)return e._autoprefixerDisabled;if(e.parent){let n=e.prev();if(n&&n.type==="comment"&&vA.test(n.text))return e._autoprefixerDisabled=!0,e._autoprefixerSelfDisabled=!0,!0}let i=null;if(e.nodes){let n;e.each(a=>{a.type==="comment"&&/(!\s*)?autoprefixer:\s*(off|on)/i.test(a.text)&&(typeof n!="undefined"?t.warn("Second Autoprefixer control comment was ignored. Autoprefixer applies control comment to whole block, not to next rules.",{node:a}):n=/on/i.test(a.text))}),n!==void 0&&(i=!n)}if(!e.nodes||i===null)if(e.parent){let n=this.disabled(e.parent,t);e.parent._autoprefixerSelfDisabled===!0?i=!1:i=n}else i=!1;return e._autoprefixerDisabled=i,i}reduceSpaces(e){let t=!1;if(this.prefixes.group(e).up(()=>(t=!0,!0)),t)return;let i=e.raw("before").split(` +`),n=i[i.length-1].length,a=!1;this.prefixes.group(e).down(s=>{i=s.raw("before").split(` +`);let o=i.length-1;i[o].length>n&&(a===!1&&(a=i[o].length-n),i[o]=i[o].slice(0,-a),s.raws.before=i.join(` +`))})}displayType(e){for(let t of e.parent.nodes)if(t.prop==="display"){if(t.value.includes("flex"))return"flex";if(t.value.includes("grid"))return"grid"}return!1}gridStatus(e,t){if(!e)return!1;if(e._autoprefixerGridStatus!==void 0)return e._autoprefixerGridStatus;let i=null;if(e.nodes){let n;e.each(a=>{if(a.type==="comment"&&xA.test(a.text)){let s=/:\s*autoplace/i.test(a.text),o=/no-autoplace/i.test(a.text);typeof n!="undefined"?t.warn("Second Autoprefixer grid control comment was ignored. Autoprefixer applies control comments to the whole block, not to the next rules.",{node:a}):s?n="autoplace":o?n=!0:n=/on/i.test(a.text)}}),n!==void 0&&(i=n)}if(e.type==="atrule"&&e.name==="supports"){let n=e.params;n.includes("grid")&&n.includes("auto")&&(i=!1)}if(!e.nodes||i===null)if(e.parent){let n=this.gridStatus(e.parent,t);e.parent._autoprefixerSelfDisabled===!0?i=!1:i=n}else typeof this.prefixes.options.grid!="undefined"?i=this.prefixes.options.grid:typeof h.env.AUTOPREFIXER_GRID!="undefined"?h.env.AUTOPREFIXER_GRID==="autoplace"?i="autoplace":i=!0:i=!1;return e._autoprefixerGridStatus=i,i}};ug.exports=lg});var cg=v((jD,fg)=>{l();fg.exports={A:{A:{"2":"K E F G A B JC"},B:{"1":"C L M H N D O P Q R S T U V W X Y Z a b c d e f g h i j n o p q r s t u v w x y z I"},C:{"1":"2 3 4 5 6 7 8 9 AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB TB UB VB WB XB YB ZB aB bB cB 0B dB 1B eB fB gB hB iB jB kB lB mB nB oB m pB qB rB sB tB P Q R 2B S T U V W X Y Z a b c d e f g h i j n o p q r s t u v w x y z I uB 3B 4B","2":"0 1 KC zB J K E F G A B C L M H N D O k l LC MC"},D:{"1":"8 9 AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB TB UB VB WB XB YB ZB aB bB cB 0B dB 1B eB fB gB hB iB jB kB lB mB nB oB m pB qB rB sB tB P Q R S T U V W X Y Z a b c d e f g h i j n o p q r s t u v w x y z I uB 3B 4B","2":"0 1 2 3 4 5 6 7 J K E F G A B C L M H N D O k l"},E:{"1":"G A B C L M H D RC 6B vB wB 7B SC TC 8B 9B xB AC yB BC CC DC EC FC GC UC","2":"0 J K E F NC 5B OC PC QC"},F:{"1":"1 2 3 4 5 6 7 8 9 H N D O k l AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB TB UB VB WB XB YB ZB aB bB cB dB eB fB gB hB iB jB kB lB mB nB oB m pB qB rB sB tB P Q R 2B S T U V W X Y Z a b c d e f g h i j wB","2":"G B C VC WC XC YC vB HC ZC"},G:{"1":"D fC gC hC iC jC kC lC mC nC oC pC qC rC sC tC 8B 9B xB AC yB BC CC DC EC FC GC","2":"F 5B aC IC bC cC dC eC"},H:{"1":"uC"},I:{"1":"I zC 0C","2":"zB J vC wC xC yC IC"},J:{"2":"E A"},K:{"1":"m","2":"A B C vB HC wB"},L:{"1":"I"},M:{"1":"uB"},N:{"2":"A B"},O:{"1":"xB"},P:{"1":"J k l 1C 2C 3C 4C 5C 6B 6C 7C 8C 9C AD yB BD CD DD"},Q:{"1":"7B"},R:{"1":"ED"},S:{"1":"FD GD"}},B:4,C:"CSS Feature Queries"}});var mg=v((VD,hg)=>{l();function pg(r){return r[r.length-1]}var dg={parse(r){let e=[""],t=[e];for(let i of r){if(i==="("){e=[""],pg(t).push(e),t.push(e);continue}if(i===")"){t.pop(),e=pg(t),e.push("");continue}e[e.length-1]+=i}return t[0]},stringify(r){let e="";for(let t of r){if(typeof t=="object"){e+=`(${dg.stringify(t)})`;continue}e+=t}return e}};hg.exports=dg});var vg=v((UD,bg)=>{l();var CA=cg(),{feature:AA}=(Gn(),Wn),{parse:_A}=ge(),OA=dt(),al=mg(),EA=ke(),TA=le(),gg=AA(CA),yg=[];for(let r in gg.stats){let e=gg.stats[r];for(let t in e){let i=e[t];/y/.test(i)&&yg.push(r+" "+t)}}var wg=class{constructor(e,t){this.Prefixes=e,this.all=t}prefixer(){if(this.prefixerCache)return this.prefixerCache;let e=this.all.browsers.selected.filter(i=>yg.includes(i)),t=new OA(this.all.browsers.data,e,this.all.options);return this.prefixerCache=new this.Prefixes(this.all.data,t,this.all.options),this.prefixerCache}parse(e){let t=e.split(":"),i=t[0],n=t[1];return n||(n=""),[i.trim(),n.trim()]}virtual(e){let[t,i]=this.parse(e),n=_A("a{}").first;return n.append({prop:t,value:i,raws:{before:""}}),n}prefixed(e){let t=this.virtual(e);if(this.disabled(t.first))return t.nodes;let i={warn:()=>null},n=this.prefixer().add[t.first.prop];n&&n.process&&n.process(t.first,i);for(let a of t.nodes){for(let s of this.prefixer().values("add",t.first.prop))s.process(a);EA.save(this.all,a)}return t.nodes}isNot(e){return typeof e=="string"&&/not\s*/i.test(e)}isOr(e){return typeof e=="string"&&/\s*or\s*/i.test(e)}isProp(e){return typeof e=="object"&&e.length===1&&typeof e[0]=="string"}isHack(e,t){return!new RegExp(`(\\(|\\s)${TA.escapeRegexp(t)}:`).test(e)}toRemove(e,t){let[i,n]=this.parse(e),a=this.all.unprefixed(i),s=this.all.cleaner();if(s.remove[i]&&s.remove[i].remove&&!this.isHack(t,a))return!0;for(let o of s.values("remove",a))if(o.check(n))return!0;return!1}remove(e,t){let i=0;for(;itypeof t!="object"?t:t.length===1&&typeof t[0]=="object"?this.cleanBrackets(t[0]):this.cleanBrackets(t))}convert(e){let t=[""];for(let i of e)t.push([`${i.prop}: ${i.value}`]),t.push(" or ");return t[t.length-1]="",t}normalize(e){if(typeof e!="object")return e;if(e=e.filter(t=>t!==""),typeof e[0]=="string"){let t=e[0].trim();if(t.includes(":")||t==="selector"||t==="not selector")return[al.stringify(e)]}return e.map(t=>this.normalize(t))}add(e,t){return e.map(i=>{if(this.isProp(i)){let n=this.prefixed(i[0]);return n.length>1?this.convert(n):i}return typeof i=="object"?this.add(i,t):i})}process(e){let t=al.parse(e.params);t=this.normalize(t),t=this.remove(t,e.params),t=this.add(t,e.params),t=this.cleanBrackets(t),e.params=al.stringify(t)}disabled(e){if(!this.all.options.grid&&(e.prop==="display"&&e.value.includes("grid")||e.prop.includes("grid")||e.prop==="justify-items"))return!0;if(this.all.options.flexbox===!1){if(e.prop==="display"&&e.value.includes("flex"))return!0;let t=["order","justify-content","align-items","align-content"];if(e.prop.includes("flex")||t.includes(e.prop))return!0}return!1}};bg.exports=wg});var Sg=v((WD,kg)=>{l();var xg=class{constructor(e,t){this.prefix=t,this.prefixed=e.prefixed(this.prefix),this.regexp=e.regexp(this.prefix),this.prefixeds=e.possible().map(i=>[e.prefixed(i),e.regexp(i)]),this.unprefixed=e.name,this.nameRegexp=e.regexp()}isHack(e){let t=e.parent.index(e)+1,i=e.parent.nodes;for(;t{l();var{list:PA}=ge(),DA=Sg(),IA=Ht(),qA=dt(),RA=le(),Cg=class extends IA{constructor(e,t,i){super(e,t,i);this.regexpCache=new Map}check(e){return e.selector.includes(this.name)?!!e.selector.match(this.regexp()):!1}prefixed(e){return this.name.replace(/^(\W*)/,`$1${e}`)}regexp(e){if(!this.regexpCache.has(e)){let t=e?this.prefixed(e):this.name;this.regexpCache.set(e,new RegExp(`(^|[^:"'=])${RA.escapeRegexp(t)}`,"gi"))}return this.regexpCache.get(e)}possible(){return qA.prefixes()}prefixeds(e){if(e._autoprefixerPrefixeds){if(e._autoprefixerPrefixeds[this.name])return e._autoprefixerPrefixeds}else e._autoprefixerPrefixeds={};let t={};if(e.selector.includes(",")){let n=PA.comma(e.selector).filter(a=>a.includes(this.name));for(let a of this.possible())t[a]=n.map(s=>this.replace(s,a)).join(", ")}else for(let i of this.possible())t[i]=this.replace(e.selector,i);return e._autoprefixerPrefixeds[this.name]=t,e._autoprefixerPrefixeds}already(e,t,i){let n=e.parent.index(e)-1;for(;n>=0;){let a=e.parent.nodes[n];if(a.type!=="rule")return!1;let s=!1;for(let o in t[this.name]){let u=t[this.name][o];if(a.selector===u){if(i===o)return!0;s=!0;break}}if(!s)return!1;n-=1}return!1}replace(e,t){return e.replace(this.regexp(),`$1${this.prefixed(t)}`)}add(e,t){let i=this.prefixeds(e);if(this.already(e,i,t))return;let n=this.clone(e,{selector:i[this.name][t]});e.parent.insertBefore(e,n)}old(e){return new DA(this,e)}};Ag.exports=Cg});var Eg=v((HD,Og)=>{l();var MA=Ht(),_g=class extends MA{add(e,t){let i=t+e.name;if(e.parent.some(s=>s.name===i&&s.params===e.params))return;let a=this.clone(e,{name:i});return e.parent.insertBefore(e,a)}process(e){let t=this.parentPrefix(e);for(let i of this.prefixes)(!t||t===i)&&this.add(e,i)}};Og.exports=_g});var Pg=v((YD,Tg)=>{l();var BA=Jt(),ol=class extends BA{prefixed(e){return e==="-webkit-"?":-webkit-full-screen":e==="-moz-"?":-moz-full-screen":`:${e}fullscreen`}};ol.names=[":fullscreen"];Tg.exports=ol});var Ig=v((QD,Dg)=>{l();var FA=Jt(),ll=class extends FA{possible(){return super.possible().concat(["-moz- old","-ms- old"])}prefixed(e){return e==="-webkit-"?"::-webkit-input-placeholder":e==="-ms-"?"::-ms-input-placeholder":e==="-ms- old"?":-ms-input-placeholder":e==="-moz- old"?":-moz-placeholder":`::${e}placeholder`}};ll.names=["::placeholder"];Dg.exports=ll});var Rg=v((JD,qg)=>{l();var LA=Jt(),ul=class extends LA{prefixed(e){return e==="-ms-"?":-ms-input-placeholder":`:${e}placeholder-shown`}};ul.names=[":placeholder-shown"];qg.exports=ul});var Bg=v((XD,Mg)=>{l();var NA=Jt(),$A=le(),fl=class extends NA{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=$A.uniq(this.prefixes.map(n=>"-webkit-")))}prefixed(e){return e==="-webkit-"?"::-webkit-file-upload-button":`::${e}file-selector-button`}};fl.names=["::file-selector-button"];Mg.exports=fl});var pe=v((KD,Fg)=>{l();Fg.exports=function(r){let e;return r==="-webkit- 2009"||r==="-moz-"?e=2009:r==="-ms-"?e=2012:r==="-webkit-"&&(e="final"),r==="-webkit- 2009"&&(r="-webkit-"),[e,r]}});var zg=v((ZD,$g)=>{l();var Lg=ge().list,Ng=pe(),zA=M(),Xt=class extends zA{prefixed(e,t){let i;return[i,t]=Ng(t),i===2009?t+"box-flex":super.prefixed(e,t)}normalize(){return"flex"}set(e,t){let i=Ng(t)[0];if(i===2009)return e.value=Lg.space(e.value)[0],e.value=Xt.oldValues[e.value]||e.value,super.set(e,t);if(i===2012){let n=Lg.space(e.value);n.length===3&&n[2]==="0"&&(e.value=n.slice(0,2).concat("0px").join(" "))}return super.set(e,t)}};Xt.names=["flex","box-flex"];Xt.oldValues={auto:"1",none:"0"};$g.exports=Xt});var Ug=v((eI,Vg)=>{l();var jg=pe(),jA=M(),cl=class extends jA{prefixed(e,t){let i;return[i,t]=jg(t),i===2009?t+"box-ordinal-group":i===2012?t+"flex-order":super.prefixed(e,t)}normalize(){return"order"}set(e,t){return jg(t)[0]===2009&&/\d/.test(e.value)?(e.value=(parseInt(e.value)+1).toString(),super.set(e,t)):super.set(e,t)}};cl.names=["order","flex-order","box-ordinal-group"];Vg.exports=cl});var Gg=v((tI,Wg)=>{l();var VA=M(),pl=class extends VA{check(e){let t=e.value;return!t.toLowerCase().includes("alpha(")&&!t.includes("DXImageTransform.Microsoft")&&!t.includes("data:image/svg+xml")}};pl.names=["filter"];Wg.exports=pl});var Yg=v((rI,Hg)=>{l();var UA=M(),dl=class extends UA{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let a=this.clone(e),s=e.prop.replace(/end$/,"start"),o=t+e.prop.replace(/end$/,"span");if(!e.parent.some(u=>u.prop===o)){if(a.prop=o,e.value.includes("span"))a.value=e.value.replace(/span\s/i,"");else{let u;if(e.parent.walkDecls(s,c=>{u=c}),u){let c=Number(e.value)-Number(u.value)+"";a.value=c}else e.warn(n,`Can not prefix ${e.prop} (${s} is not found)`)}e.cloneBefore(a)}}};dl.names=["grid-row-end","grid-column-end"];Hg.exports=dl});var Jg=v((iI,Qg)=>{l();var WA=M(),hl=class extends WA{check(e){return!e.value.split(/\s+/).some(t=>{let i=t.toLowerCase();return i==="reverse"||i==="alternate-reverse"})}};hl.names=["animation","animation-direction"];Qg.exports=hl});var Kg=v((nI,Xg)=>{l();var GA=pe(),HA=M(),ml=class extends HA{insert(e,t,i){let n;if([n,t]=GA(t),n!==2009)return super.insert(e,t,i);let a=e.value.split(/\s+/).filter(d=>d!=="wrap"&&d!=="nowrap"&&"wrap-reverse");if(a.length===0||e.parent.some(d=>d.prop===t+"box-orient"||d.prop===t+"box-direction"))return;let o=a[0],u=o.includes("row")?"horizontal":"vertical",c=o.includes("reverse")?"reverse":"normal",f=this.clone(e);return f.prop=t+"box-orient",f.value=u,this.needCascade(e)&&(f.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,f),f=this.clone(e),f.prop=t+"box-direction",f.value=c,this.needCascade(e)&&(f.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,f)}};ml.names=["flex-flow","box-direction","box-orient"];Xg.exports=ml});var ey=v((sI,Zg)=>{l();var YA=pe(),QA=M(),gl=class extends QA{normalize(){return"flex"}prefixed(e,t){let i;return[i,t]=YA(t),i===2009?t+"box-flex":i===2012?t+"flex-positive":super.prefixed(e,t)}};gl.names=["flex-grow","flex-positive"];Zg.exports=gl});var ry=v((aI,ty)=>{l();var JA=pe(),XA=M(),yl=class extends XA{set(e,t){if(JA(t)[0]!==2009)return super.set(e,t)}};yl.names=["flex-wrap"];ty.exports=yl});var ny=v((oI,iy)=>{l();var KA=M(),Kt=mt(),wl=class extends KA{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let a=Kt.parse(e),[s,o]=Kt.translate(a,0,2),[u,c]=Kt.translate(a,1,3);[["grid-row",s],["grid-row-span",o],["grid-column",u],["grid-column-span",c]].forEach(([f,d])=>{Kt.insertDecl(e,f,d)}),Kt.warnTemplateSelectorNotFound(e,n),Kt.warnIfGridRowColumnExists(e,n)}};wl.names=["grid-area"];iy.exports=wl});var ay=v((lI,sy)=>{l();var ZA=M(),oi=mt(),bl=class extends ZA{insert(e,t,i){if(t!=="-ms-")return super.insert(e,t,i);if(e.parent.some(s=>s.prop==="-ms-grid-row-align"))return;let[[n,a]]=oi.parse(e);a?(oi.insertDecl(e,"grid-row-align",n),oi.insertDecl(e,"grid-column-align",a)):(oi.insertDecl(e,"grid-row-align",n),oi.insertDecl(e,"grid-column-align",n))}};bl.names=["place-self"];sy.exports=bl});var ly=v((uI,oy)=>{l();var e5=M(),vl=class extends e5{check(e){let t=e.value;return!t.includes("/")||t.includes("span")}normalize(e){return e.replace("-start","")}prefixed(e,t){let i=super.prefixed(e,t);return t==="-ms-"&&(i=i.replace("-start","")),i}};vl.names=["grid-row-start","grid-column-start"];oy.exports=vl});var cy=v((fI,fy)=>{l();var uy=pe(),t5=M(),Zt=class extends t5{check(e){return e.parent&&!e.parent.some(t=>t.prop&&t.prop.startsWith("grid-"))}prefixed(e,t){let i;return[i,t]=uy(t),i===2012?t+"flex-item-align":super.prefixed(e,t)}normalize(){return"align-self"}set(e,t){let i=uy(t)[0];if(i===2012)return e.value=Zt.oldValues[e.value]||e.value,super.set(e,t);if(i==="final")return super.set(e,t)}};Zt.names=["align-self","flex-item-align"];Zt.oldValues={"flex-end":"end","flex-start":"start"};fy.exports=Zt});var dy=v((cI,py)=>{l();var r5=M(),i5=le(),xl=class extends r5{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=i5.uniq(this.prefixes.map(n=>n==="-ms-"?"-webkit-":n)))}};xl.names=["appearance"];py.exports=xl});var gy=v((pI,my)=>{l();var hy=pe(),n5=M(),kl=class extends n5{normalize(){return"flex-basis"}prefixed(e,t){let i;return[i,t]=hy(t),i===2012?t+"flex-preferred-size":super.prefixed(e,t)}set(e,t){let i;if([i,t]=hy(t),i===2012||i==="final")return super.set(e,t)}};kl.names=["flex-basis","flex-preferred-size"];my.exports=kl});var wy=v((dI,yy)=>{l();var s5=M(),Sl=class extends s5{normalize(){return this.name.replace("box-image","border")}prefixed(e,t){let i=super.prefixed(e,t);return t==="-webkit-"&&(i=i.replace("border","box-image")),i}};Sl.names=["mask-border","mask-border-source","mask-border-slice","mask-border-width","mask-border-outset","mask-border-repeat","mask-box-image","mask-box-image-source","mask-box-image-slice","mask-box-image-width","mask-box-image-outset","mask-box-image-repeat"];yy.exports=Sl});var vy=v((hI,by)=>{l();var a5=M(),Ne=class extends a5{insert(e,t,i){let n=e.prop==="mask-composite",a;n?a=e.value.split(","):a=e.value.match(Ne.regexp)||[],a=a.map(c=>c.trim()).filter(c=>c);let s=a.length,o;if(s&&(o=this.clone(e),o.value=a.map(c=>Ne.oldValues[c]||c).join(", "),a.includes("intersect")&&(o.value+=", xor"),o.prop=t+"mask-composite"),n)return s?(this.needCascade(e)&&(o.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,o)):void 0;let u=this.clone(e);return u.prop=t+u.prop,s&&(u.value=u.value.replace(Ne.regexp,"")),this.needCascade(e)&&(u.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,u),s?(this.needCascade(e)&&(o.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,o)):e}};Ne.names=["mask","mask-composite"];Ne.oldValues={add:"source-over",subtract:"source-out",intersect:"source-in",exclude:"xor"};Ne.regexp=new RegExp(`\\s+(${Object.keys(Ne.oldValues).join("|")})\\b(?!\\))\\s*(?=[,])`,"ig");by.exports=Ne});var Sy=v((mI,ky)=>{l();var xy=pe(),o5=M(),er=class extends o5{prefixed(e,t){let i;return[i,t]=xy(t),i===2009?t+"box-align":i===2012?t+"flex-align":super.prefixed(e,t)}normalize(){return"align-items"}set(e,t){let i=xy(t)[0];return(i===2009||i===2012)&&(e.value=er.oldValues[e.value]||e.value),super.set(e,t)}};er.names=["align-items","flex-align","box-align"];er.oldValues={"flex-end":"end","flex-start":"start"};ky.exports=er});var Ay=v((gI,Cy)=>{l();var l5=M(),Cl=class extends l5{set(e,t){return t==="-ms-"&&e.value==="contain"&&(e.value="element"),super.set(e,t)}insert(e,t,i){if(!(e.value==="all"&&t==="-ms-"))return super.insert(e,t,i)}};Cl.names=["user-select"];Cy.exports=Cl});var Ey=v((yI,Oy)=>{l();var _y=pe(),u5=M(),Al=class extends u5{normalize(){return"flex-shrink"}prefixed(e,t){let i;return[i,t]=_y(t),i===2012?t+"flex-negative":super.prefixed(e,t)}set(e,t){let i;if([i,t]=_y(t),i===2012||i==="final")return super.set(e,t)}};Al.names=["flex-shrink","flex-negative"];Oy.exports=Al});var Py=v((wI,Ty)=>{l();var f5=M(),_l=class extends f5{prefixed(e,t){return`${t}column-${e}`}normalize(e){return e.includes("inside")?"break-inside":e.includes("before")?"break-before":"break-after"}set(e,t){return(e.prop==="break-inside"&&e.value==="avoid-column"||e.value==="avoid-page")&&(e.value="avoid"),super.set(e,t)}insert(e,t,i){if(e.prop!=="break-inside")return super.insert(e,t,i);if(!(/region/i.test(e.value)||/page/i.test(e.value)))return super.insert(e,t,i)}};_l.names=["break-inside","page-break-inside","column-break-inside","break-before","page-break-before","column-break-before","break-after","page-break-after","column-break-after"];Ty.exports=_l});var Iy=v((bI,Dy)=>{l();var c5=M(),Ol=class extends c5{prefixed(e,t){return t+"print-color-adjust"}normalize(){return"color-adjust"}};Ol.names=["color-adjust","print-color-adjust"];Dy.exports=Ol});var Ry=v((vI,qy)=>{l();var p5=M(),tr=class extends p5{insert(e,t,i){if(t==="-ms-"){let n=this.set(this.clone(e),t);this.needCascade(e)&&(n.raws.before=this.calcBefore(i,e,t));let a="ltr";return e.parent.nodes.forEach(s=>{s.prop==="direction"&&(s.value==="rtl"||s.value==="ltr")&&(a=s.value)}),n.value=tr.msValues[a][e.value]||e.value,e.parent.insertBefore(e,n)}return super.insert(e,t,i)}};tr.names=["writing-mode"];tr.msValues={ltr:{"horizontal-tb":"lr-tb","vertical-rl":"tb-rl","vertical-lr":"tb-lr"},rtl:{"horizontal-tb":"rl-tb","vertical-rl":"bt-rl","vertical-lr":"bt-lr"}};qy.exports=tr});var By=v((xI,My)=>{l();var d5=M(),El=class extends d5{set(e,t){return e.value=e.value.replace(/\s+fill(\s)/,"$1"),super.set(e,t)}};El.names=["border-image"];My.exports=El});var Ny=v((kI,Ly)=>{l();var Fy=pe(),h5=M(),rr=class extends h5{prefixed(e,t){let i;return[i,t]=Fy(t),i===2012?t+"flex-line-pack":super.prefixed(e,t)}normalize(){return"align-content"}set(e,t){let i=Fy(t)[0];if(i===2012)return e.value=rr.oldValues[e.value]||e.value,super.set(e,t);if(i==="final")return super.set(e,t)}};rr.names=["align-content","flex-line-pack"];rr.oldValues={"flex-end":"end","flex-start":"start","space-between":"justify","space-around":"distribute"};Ly.exports=rr});var zy=v((SI,$y)=>{l();var m5=M(),Se=class extends m5{prefixed(e,t){return t==="-moz-"?t+(Se.toMozilla[e]||e):super.prefixed(e,t)}normalize(e){return Se.toNormal[e]||e}};Se.names=["border-radius"];Se.toMozilla={};Se.toNormal={};for(let r of["top","bottom"])for(let e of["left","right"]){let t=`border-${r}-${e}-radius`,i=`border-radius-${r}${e}`;Se.names.push(t),Se.names.push(i),Se.toMozilla[t]=i,Se.toNormal[i]=t}$y.exports=Se});var Vy=v((CI,jy)=>{l();var g5=M(),Tl=class extends g5{prefixed(e,t){return e.includes("-start")?t+e.replace("-block-start","-before"):t+e.replace("-block-end","-after")}normalize(e){return e.includes("-before")?e.replace("-before","-block-start"):e.replace("-after","-block-end")}};Tl.names=["border-block-start","border-block-end","margin-block-start","margin-block-end","padding-block-start","padding-block-end","border-before","border-after","margin-before","margin-after","padding-before","padding-after"];jy.exports=Tl});var Wy=v((AI,Uy)=>{l();var y5=M(),{parseTemplate:w5,warnMissedAreas:b5,getGridGap:v5,warnGridGap:x5,inheritGridGap:k5}=mt(),Pl=class extends y5{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);if(e.parent.some(m=>m.prop==="-ms-grid-rows"))return;let a=v5(e),s=k5(e,a),{rows:o,columns:u,areas:c}=w5({decl:e,gap:s||a}),f=Object.keys(c).length>0,d=Boolean(o),p=Boolean(u);return x5({gap:a,hasColumns:p,decl:e,result:n}),b5(c,e,n),(d&&p||f)&&e.cloneBefore({prop:"-ms-grid-rows",value:o,raws:{}}),p&&e.cloneBefore({prop:"-ms-grid-columns",value:u,raws:{}}),e}};Pl.names=["grid-template"];Uy.exports=Pl});var Hy=v((_I,Gy)=>{l();var S5=M(),Dl=class extends S5{prefixed(e,t){return t+e.replace("-inline","")}normalize(e){return e.replace(/(margin|padding|border)-(start|end)/,"$1-inline-$2")}};Dl.names=["border-inline-start","border-inline-end","margin-inline-start","margin-inline-end","padding-inline-start","padding-inline-end","border-start","border-end","margin-start","margin-end","padding-start","padding-end"];Gy.exports=Dl});var Qy=v((OI,Yy)=>{l();var C5=M(),Il=class extends C5{check(e){return!e.value.includes("flex-")&&e.value!=="baseline"}prefixed(e,t){return t+"grid-row-align"}normalize(){return"align-self"}};Il.names=["grid-row-align"];Yy.exports=Il});var Xy=v((EI,Jy)=>{l();var A5=M(),ir=class extends A5{keyframeParents(e){let{parent:t}=e;for(;t;){if(t.type==="atrule"&&t.name==="keyframes")return!0;({parent:t}=t)}return!1}contain3d(e){if(e.prop==="transform-origin")return!1;for(let t of ir.functions3d)if(e.value.includes(`${t}(`))return!0;return!1}set(e,t){return e=super.set(e,t),t==="-ms-"&&(e.value=e.value.replace(/rotatez/gi,"rotate")),e}insert(e,t,i){if(t==="-ms-"){if(!this.contain3d(e)&&!this.keyframeParents(e))return super.insert(e,t,i)}else if(t==="-o-"){if(!this.contain3d(e))return super.insert(e,t,i)}else return super.insert(e,t,i)}};ir.names=["transform","transform-origin"];ir.functions3d=["matrix3d","translate3d","translateZ","scale3d","scaleZ","rotate3d","rotateX","rotateY","perspective"];Jy.exports=ir});var ew=v((TI,Zy)=>{l();var Ky=pe(),_5=M(),ql=class extends _5{normalize(){return"flex-direction"}insert(e,t,i){let n;if([n,t]=Ky(t),n!==2009)return super.insert(e,t,i);if(e.parent.some(f=>f.prop===t+"box-orient"||f.prop===t+"box-direction"))return;let s=e.value,o,u;s==="inherit"||s==="initial"||s==="unset"?(o=s,u=s):(o=s.includes("row")?"horizontal":"vertical",u=s.includes("reverse")?"reverse":"normal");let c=this.clone(e);return c.prop=t+"box-orient",c.value=o,this.needCascade(e)&&(c.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,c),c=this.clone(e),c.prop=t+"box-direction",c.value=u,this.needCascade(e)&&(c.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,c)}old(e,t){let i;return[i,t]=Ky(t),i===2009?[t+"box-orient",t+"box-direction"]:super.old(e,t)}};ql.names=["flex-direction","box-direction","box-orient"];Zy.exports=ql});var rw=v((PI,tw)=>{l();var O5=M(),Rl=class extends O5{check(e){return e.value==="pixelated"}prefixed(e,t){return t==="-ms-"?"-ms-interpolation-mode":super.prefixed(e,t)}set(e,t){return t!=="-ms-"?super.set(e,t):(e.prop="-ms-interpolation-mode",e.value="nearest-neighbor",e)}normalize(){return"image-rendering"}process(e,t){return super.process(e,t)}};Rl.names=["image-rendering","interpolation-mode"];tw.exports=Rl});var nw=v((DI,iw)=>{l();var E5=M(),T5=le(),Ml=class extends E5{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=T5.uniq(this.prefixes.map(n=>n==="-ms-"?"-webkit-":n)))}};Ml.names=["backdrop-filter"];iw.exports=Ml});var aw=v((II,sw)=>{l();var P5=M(),D5=le(),Bl=class extends P5{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=D5.uniq(this.prefixes.map(n=>n==="-ms-"?"-webkit-":n)))}check(e){return e.value.toLowerCase()==="text"}};Bl.names=["background-clip"];sw.exports=Bl});var lw=v((qI,ow)=>{l();var I5=M(),q5=["none","underline","overline","line-through","blink","inherit","initial","unset"],Fl=class extends I5{check(e){return e.value.split(/\s+/).some(t=>!q5.includes(t))}};Fl.names=["text-decoration"];ow.exports=Fl});var cw=v((RI,fw)=>{l();var uw=pe(),R5=M(),nr=class extends R5{prefixed(e,t){let i;return[i,t]=uw(t),i===2009?t+"box-pack":i===2012?t+"flex-pack":super.prefixed(e,t)}normalize(){return"justify-content"}set(e,t){let i=uw(t)[0];if(i===2009||i===2012){let n=nr.oldValues[e.value]||e.value;if(e.value=n,i!==2009||n!=="distribute")return super.set(e,t)}else if(i==="final")return super.set(e,t)}};nr.names=["justify-content","flex-pack","box-pack"];nr.oldValues={"flex-end":"end","flex-start":"start","space-between":"justify","space-around":"distribute"};fw.exports=nr});var dw=v((MI,pw)=>{l();var M5=M(),Ll=class extends M5{set(e,t){let i=e.value.toLowerCase();return t==="-webkit-"&&!i.includes(" ")&&i!=="contain"&&i!=="cover"&&(e.value=e.value+" "+e.value),super.set(e,t)}};Ll.names=["background-size"];pw.exports=Ll});var mw=v((BI,hw)=>{l();var B5=M(),Nl=mt(),$l=class extends B5{insert(e,t,i){if(t!=="-ms-")return super.insert(e,t,i);let n=Nl.parse(e),[a,s]=Nl.translate(n,0,1);n[0]&&n[0].includes("span")&&(s=n[0].join("").replace(/\D/g,"")),[[e.prop,a],[`${e.prop}-span`,s]].forEach(([u,c])=>{Nl.insertDecl(e,u,c)})}};$l.names=["grid-row","grid-column"];hw.exports=$l});var ww=v((FI,yw)=>{l();var F5=M(),{prefixTrackProp:gw,prefixTrackValue:L5,autoplaceGridItems:N5,getGridGap:$5,inheritGridGap:z5}=mt(),j5=sl(),zl=class extends F5{prefixed(e,t){return t==="-ms-"?gw({prop:e,prefix:t}):super.prefixed(e,t)}normalize(e){return e.replace(/^grid-(rows|columns)/,"grid-template-$1")}insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let{parent:a,prop:s,value:o}=e,u=s.includes("rows"),c=s.includes("columns"),f=a.some(k=>k.prop==="grid-template"||k.prop==="grid-template-areas");if(f&&u)return!1;let d=new j5({options:{}}),p=d.gridStatus(a,n),m=$5(e);m=z5(e,m)||m;let b=u?m.row:m.column;(p==="no-autoplace"||p===!0)&&!f&&(b=null);let x=L5({value:o,gap:b});e.cloneBefore({prop:gw({prop:s,prefix:t}),value:x});let y=a.nodes.find(k=>k.prop==="grid-auto-flow"),w="row";if(y&&!d.disabled(y,n)&&(w=y.value.trim()),p==="autoplace"){let k=a.nodes.find(_=>_.prop==="grid-template-rows");if(!k&&f)return;if(!k&&!f){e.warn(n,"Autoplacement does not work without grid-template-rows property");return}!a.nodes.find(_=>_.prop==="grid-template-columns")&&!f&&e.warn(n,"Autoplacement does not work without grid-template-columns property"),c&&!f&&N5(e,n,m,w)}}};zl.names=["grid-template-rows","grid-template-columns","grid-rows","grid-columns"];yw.exports=zl});var vw=v((LI,bw)=>{l();var V5=M(),jl=class extends V5{check(e){return!e.value.includes("flex-")&&e.value!=="baseline"}prefixed(e,t){return t+"grid-column-align"}normalize(){return"justify-self"}};jl.names=["grid-column-align"];bw.exports=jl});var kw=v((NI,xw)=>{l();var U5=M(),Vl=class extends U5{prefixed(e,t){return t+"scroll-chaining"}normalize(){return"overscroll-behavior"}set(e,t){return e.value==="auto"?e.value="chained":(e.value==="none"||e.value==="contain")&&(e.value="none"),super.set(e,t)}};Vl.names=["overscroll-behavior","scroll-chaining"];xw.exports=Vl});var Aw=v(($I,Cw)=>{l();var W5=M(),{parseGridAreas:G5,warnMissedAreas:H5,prefixTrackProp:Y5,prefixTrackValue:Sw,getGridGap:Q5,warnGridGap:J5,inheritGridGap:X5}=mt();function K5(r){return r.trim().slice(1,-1).split(/["']\s*["']?/g)}var Ul=class extends W5{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let a=!1,s=!1,o=e.parent,u=Q5(e);u=X5(e,u)||u,o.walkDecls(/-ms-grid-rows/,d=>d.remove()),o.walkDecls(/grid-template-(rows|columns)/,d=>{if(d.prop==="grid-template-rows"){s=!0;let{prop:p,value:m}=d;d.cloneBefore({prop:Y5({prop:p,prefix:t}),value:Sw({value:m,gap:u.row})})}else a=!0});let c=K5(e.value);a&&!s&&u.row&&c.length>1&&e.cloneBefore({prop:"-ms-grid-rows",value:Sw({value:`repeat(${c.length}, auto)`,gap:u.row}),raws:{}}),J5({gap:u,hasColumns:a,decl:e,result:n});let f=G5({rows:c,gap:u});return H5(f,e,n),e}};Ul.names=["grid-template-areas"];Cw.exports=Ul});var Ow=v((zI,_w)=>{l();var Z5=M(),Wl=class extends Z5{set(e,t){return t==="-webkit-"&&(e.value=e.value.replace(/\s*(right|left)\s*/i,"")),super.set(e,t)}};Wl.names=["text-emphasis-position"];_w.exports=Wl});var Tw=v((jI,Ew)=>{l();var e_=M(),Gl=class extends e_{set(e,t){return e.prop==="text-decoration-skip-ink"&&e.value==="auto"?(e.prop=t+"text-decoration-skip",e.value="ink",e):super.set(e,t)}};Gl.names=["text-decoration-skip-ink","text-decoration-skip"];Ew.exports=Gl});var Mw=v((VI,Rw)=>{l();"use strict";Rw.exports={wrap:Pw,limit:Dw,validate:Iw,test:Hl,curry:t_,name:qw};function Pw(r,e,t){var i=e-r;return((t-r)%i+i)%i+r}function Dw(r,e,t){return Math.max(r,Math.min(e,t))}function Iw(r,e,t,i,n){if(!Hl(r,e,t,i,n))throw new Error(t+" is outside of range ["+r+","+e+")");return t}function Hl(r,e,t,i,n){return!(te||n&&t===e||i&&t===r)}function qw(r,e,t,i){return(t?"(":"[")+r+","+e+(i?")":"]")}function t_(r,e,t,i){var n=qw.bind(null,r,e,t,i);return{wrap:Pw.bind(null,r,e),limit:Dw.bind(null,r,e),validate:function(a){return Iw(r,e,a,t,i)},test:function(a){return Hl(r,e,a,t,i)},toString:n,name:n}}});var Lw=v((UI,Fw)=>{l();var Yl=Kn(),r_=Mw(),i_=Qt(),n_=ke(),s_=le(),Bw=/top|left|right|bottom/gi,Qe=class extends n_{replace(e,t){let i=Yl(e);for(let n of i.nodes)if(n.type==="function"&&n.value===this.name)if(n.nodes=this.newDirection(n.nodes),n.nodes=this.normalize(n.nodes),t==="-webkit- old"){if(!this.oldWebkit(n))return!1}else n.nodes=this.convertDirection(n.nodes),n.value=t+n.value;return i.toString()}replaceFirst(e,...t){return t.map(n=>n===" "?{type:"space",value:n}:{type:"word",value:n}).concat(e.slice(1))}normalizeUnit(e,t){return`${parseFloat(e)/t*360}deg`}normalize(e){if(!e[0])return e;if(/-?\d+(.\d+)?grad/.test(e[0].value))e[0].value=this.normalizeUnit(e[0].value,400);else if(/-?\d+(.\d+)?rad/.test(e[0].value))e[0].value=this.normalizeUnit(e[0].value,2*Math.PI);else if(/-?\d+(.\d+)?turn/.test(e[0].value))e[0].value=this.normalizeUnit(e[0].value,1);else if(e[0].value.includes("deg")){let t=parseFloat(e[0].value);t=r_.wrap(0,360,t),e[0].value=`${t}deg`}return e[0].value==="0deg"?e=this.replaceFirst(e,"to"," ","top"):e[0].value==="90deg"?e=this.replaceFirst(e,"to"," ","right"):e[0].value==="180deg"?e=this.replaceFirst(e,"to"," ","bottom"):e[0].value==="270deg"&&(e=this.replaceFirst(e,"to"," ","left")),e}newDirection(e){if(e[0].value==="to"||(Bw.lastIndex=0,!Bw.test(e[0].value)))return e;e.unshift({type:"word",value:"to"},{type:"space",value:" "});for(let t=2;t0&&(e[0].value==="to"?this.fixDirection(e):e[0].value.includes("deg")?this.fixAngle(e):this.isRadial(e)&&this.fixRadial(e)),e}fixDirection(e){e.splice(0,2);for(let t of e){if(t.type==="div")break;t.type==="word"&&(t.value=this.revertDirection(t.value))}}fixAngle(e){let t=e[0].value;t=parseFloat(t),t=Math.abs(450-t)%360,t=this.roundFloat(t,3),e[0].value=`${t}deg`}fixRadial(e){let t=[],i=[],n,a,s,o,u;for(o=0;o{l();var a_=Qt(),o_=ke();function Nw(r){return new RegExp(`(^|[\\s,(])(${r}($|[\\s),]))`,"gi")}var Ql=class extends o_{regexp(){return this.regexpCache||(this.regexpCache=Nw(this.name)),this.regexpCache}isStretch(){return this.name==="stretch"||this.name==="fill"||this.name==="fill-available"}replace(e,t){return t==="-moz-"&&this.isStretch()?e.replace(this.regexp(),"$1-moz-available$3"):t==="-webkit-"&&this.isStretch()?e.replace(this.regexp(),"$1-webkit-fill-available$3"):super.replace(e,t)}old(e){let t=e+this.name;return this.isStretch()&&(e==="-moz-"?t="-moz-available":e==="-webkit-"&&(t="-webkit-fill-available")),new a_(this.name,t,t,Nw(t))}add(e,t){if(!(e.prop.includes("grid")&&t!=="-webkit-"))return super.add(e,t)}};Ql.names=["max-content","min-content","fit-content","fill","fill-available","stretch"];$w.exports=Ql});var Uw=v((GI,Vw)=>{l();var jw=Qt(),l_=ke(),Jl=class extends l_{replace(e,t){return t==="-webkit-"?e.replace(this.regexp(),"$1-webkit-optimize-contrast"):t==="-moz-"?e.replace(this.regexp(),"$1-moz-crisp-edges"):super.replace(e,t)}old(e){return e==="-webkit-"?new jw(this.name,"-webkit-optimize-contrast"):e==="-moz-"?new jw(this.name,"-moz-crisp-edges"):super.old(e)}};Jl.names=["pixelated"];Vw.exports=Jl});var Gw=v((HI,Ww)=>{l();var u_=ke(),Xl=class extends u_{replace(e,t){let i=super.replace(e,t);return t==="-webkit-"&&(i=i.replace(/("[^"]+"|'[^']+')(\s+\d+\w)/gi,"url($1)$2")),i}};Xl.names=["image-set"];Ww.exports=Xl});var Yw=v((YI,Hw)=>{l();var f_=ge().list,c_=ke(),Kl=class extends c_{replace(e,t){return f_.space(e).map(i=>{if(i.slice(0,+this.name.length+1)!==this.name+"(")return i;let n=i.lastIndexOf(")"),a=i.slice(n+1),s=i.slice(this.name.length+1,n);if(t==="-webkit-"){let o=s.match(/\d*.?\d+%?/);o?(s=s.slice(o[0].length).trim(),s+=`, ${o[0]}`):s+=", 0.5"}return t+this.name+"("+s+")"+a}).join(" ")}};Kl.names=["cross-fade"];Hw.exports=Kl});var Jw=v((QI,Qw)=>{l();var p_=pe(),d_=Qt(),h_=ke(),Zl=class extends h_{constructor(e,t){super(e,t);e==="display-flex"&&(this.name="flex")}check(e){return e.prop==="display"&&e.value===this.name}prefixed(e){let t,i;return[t,e]=p_(e),t===2009?this.name==="flex"?i="box":i="inline-box":t===2012?this.name==="flex"?i="flexbox":i="inline-flexbox":t==="final"&&(i=this.name),e+i}replace(e,t){return this.prefixed(t)}old(e){let t=this.prefixed(e);if(!!t)return new d_(this.name,t)}};Zl.names=["display-flex","inline-flex"];Qw.exports=Zl});var Kw=v((JI,Xw)=>{l();var m_=ke(),eu=class extends m_{constructor(e,t){super(e,t);e==="display-grid"&&(this.name="grid")}check(e){return e.prop==="display"&&e.value===this.name}};eu.names=["display-grid","inline-grid"];Xw.exports=eu});var eb=v((XI,Zw)=>{l();var g_=ke(),tu=class extends g_{constructor(e,t){super(e,t);e==="filter-function"&&(this.name="filter")}};tu.names=["filter","filter-function"];Zw.exports=tu});var nb=v((KI,ib)=>{l();var tb=ai(),B=M(),rb=Fm(),y_=tg(),w_=sl(),b_=vg(),ru=dt(),sr=Jt(),v_=Eg(),$e=ke(),ar=le(),x_=Pg(),k_=Ig(),S_=Rg(),C_=Bg(),A_=zg(),__=Ug(),O_=Gg(),E_=Yg(),T_=Jg(),P_=Kg(),D_=ey(),I_=ry(),q_=ny(),R_=ay(),M_=ly(),B_=cy(),F_=dy(),L_=gy(),N_=wy(),$_=vy(),z_=Sy(),j_=Ay(),V_=Ey(),U_=Py(),W_=Iy(),G_=Ry(),H_=By(),Y_=Ny(),Q_=zy(),J_=Vy(),X_=Wy(),K_=Hy(),Z_=Qy(),eO=Xy(),tO=ew(),rO=rw(),iO=nw(),nO=aw(),sO=lw(),aO=cw(),oO=dw(),lO=mw(),uO=ww(),fO=vw(),cO=kw(),pO=Aw(),dO=Ow(),hO=Tw(),mO=Lw(),gO=zw(),yO=Uw(),wO=Gw(),bO=Yw(),vO=Jw(),xO=Kw(),kO=eb();sr.hack(x_);sr.hack(k_);sr.hack(S_);sr.hack(C_);B.hack(A_);B.hack(__);B.hack(O_);B.hack(E_);B.hack(T_);B.hack(P_);B.hack(D_);B.hack(I_);B.hack(q_);B.hack(R_);B.hack(M_);B.hack(B_);B.hack(F_);B.hack(L_);B.hack(N_);B.hack($_);B.hack(z_);B.hack(j_);B.hack(V_);B.hack(U_);B.hack(W_);B.hack(G_);B.hack(H_);B.hack(Y_);B.hack(Q_);B.hack(J_);B.hack(X_);B.hack(K_);B.hack(Z_);B.hack(eO);B.hack(tO);B.hack(rO);B.hack(iO);B.hack(nO);B.hack(sO);B.hack(aO);B.hack(oO);B.hack(lO);B.hack(uO);B.hack(fO);B.hack(cO);B.hack(pO);B.hack(dO);B.hack(hO);$e.hack(mO);$e.hack(gO);$e.hack(yO);$e.hack(wO);$e.hack(bO);$e.hack(vO);$e.hack(xO);$e.hack(kO);var iu=new Map,li=class{constructor(e,t,i={}){this.data=e,this.browsers=t,this.options=i,[this.add,this.remove]=this.preprocess(this.select(this.data)),this.transition=new y_(this),this.processor=new w_(this)}cleaner(){if(this.cleanerCache)return this.cleanerCache;if(this.browsers.selected.length){let e=new ru(this.browsers.data,[]);this.cleanerCache=new li(this.data,e,this.options)}else return this;return this.cleanerCache}select(e){let t={add:{},remove:{}};for(let i in e){let n=e[i],a=n.browsers.map(u=>{let c=u.split(" ");return{browser:`${c[0]} ${c[1]}`,note:c[2]}}),s=a.filter(u=>u.note).map(u=>`${this.browsers.prefix(u.browser)} ${u.note}`);s=ar.uniq(s),a=a.filter(u=>this.browsers.isSelected(u.browser)).map(u=>{let c=this.browsers.prefix(u.browser);return u.note?`${c} ${u.note}`:c}),a=this.sort(ar.uniq(a)),this.options.flexbox==="no-2009"&&(a=a.filter(u=>!u.includes("2009")));let o=n.browsers.map(u=>this.browsers.prefix(u));n.mistakes&&(o=o.concat(n.mistakes)),o=o.concat(s),o=ar.uniq(o),a.length?(t.add[i]=a,a.length!a.includes(u)))):t.remove[i]=o}return t}sort(e){return e.sort((t,i)=>{let n=ar.removeNote(t).length,a=ar.removeNote(i).length;return n===a?i.length-t.length:a-n})}preprocess(e){let t={selectors:[],"@supports":new b_(li,this)};for(let n in e.add){let a=e.add[n];if(n==="@keyframes"||n==="@viewport")t[n]=new v_(n,a,this);else if(n==="@resolution")t[n]=new rb(n,a,this);else if(this.data[n].selector)t.selectors.push(sr.load(n,a,this));else{let s=this.data[n].props;if(s){let o=$e.load(n,a,this);for(let u of s)t[u]||(t[u]={values:[]}),t[u].values.push(o)}else{let o=t[n]&&t[n].values||[];t[n]=B.load(n,a,this),t[n].values=o}}}let i={selectors:[]};for(let n in e.remove){let a=e.remove[n];if(this.data[n].selector){let s=sr.load(n,a);for(let o of a)i.selectors.push(s.old(o))}else if(n==="@keyframes"||n==="@viewport")for(let s of a){let o=`@${s}${n.slice(1)}`;i[o]={remove:!0}}else if(n==="@resolution")i[n]=new rb(n,a,this);else{let s=this.data[n].props;if(s){let o=$e.load(n,[],this);for(let u of a){let c=o.old(u);if(c)for(let f of s)i[f]||(i[f]={}),i[f].values||(i[f].values=[]),i[f].values.push(c)}}else for(let o of a){let u=this.decl(n).old(n,o);if(n==="align-self"){let c=t[n]&&t[n].prefixes;if(c){if(o==="-webkit- 2009"&&c.includes("-webkit-"))continue;if(o==="-webkit-"&&c.includes("-webkit- 2009"))continue}}for(let c of u)i[c]||(i[c]={}),i[c].remove=!0}}}return[t,i]}decl(e){return iu.has(e)||iu.set(e,B.load(e)),iu.get(e)}unprefixed(e){let t=this.normalize(tb.unprefixed(e));return t==="flex-direction"&&(t="flex-flow"),t}normalize(e){return this.decl(e).normalize(e)}prefixed(e,t){return e=tb.unprefixed(e),this.decl(e).prefixed(e,t)}values(e,t){let i=this[e],n=i["*"]&&i["*"].values,a=i[t]&&i[t].values;return n&&a?ar.uniq(n.concat(a)):n||a||[]}group(e){let t=e.parent,i=t.index(e),{length:n}=t.nodes,a=this.unprefixed(e.prop),s=(o,u)=>{for(i+=o;i>=0&&i{l();sb.exports={"backdrop-filter":{feature:"css-backdrop-filter",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},element:{props:["background","background-image","border-image","mask","list-style","list-style-image","content","mask-image"],feature:"css-element-function",browsers:["firefox 114"]},"user-select":{mistakes:["-khtml-"],feature:"user-select-none",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},"background-clip":{feature:"background-clip-text",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},hyphens:{feature:"css-hyphens",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},fill:{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"fill-available":{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},stretch:{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["firefox 114"]},"fit-content":{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["firefox 114"]},"text-decoration-style":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-color":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-line":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-skip":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-skip-ink":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-size-adjust":{feature:"text-size-adjust",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"mask-clip":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-composite":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-image":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-origin":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-repeat":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-repeat":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-source":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},mask:{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-position":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-size":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-outset":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-width":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-slice":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"clip-path":{feature:"css-clip-path",browsers:["samsung 21"]},"box-decoration-break":{feature:"css-boxdecorationbreak",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","opera 99","safari 16.5","samsung 21"]},appearance:{feature:"css-appearance",browsers:["samsung 21"]},"image-set":{props:["background","background-image","border-image","cursor","mask","mask-image","list-style","list-style-image","content"],feature:"css-image-set",browsers:["and_uc 15.5","chrome 109","samsung 21"]},"cross-fade":{props:["background","background-image","border-image","mask","list-style","list-style-image","content","mask-image"],feature:"css-cross-fade",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},isolate:{props:["unicode-bidi"],feature:"css-unicode-bidi",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},"color-adjust":{feature:"css-color-adjust",browsers:["chrome 109","chrome 113","chrome 114","edge 114","opera 99"]}}});var lb=v((eq,ob)=>{l();ob.exports={}});var pb=v((tq,cb)=>{l();var SO=Ho(),{agents:CO}=(Gn(),Wn),nu=Sm(),AO=dt(),_O=nb(),OO=ab(),EO=lb(),ub={browsers:CO,prefixes:OO},fb=` + Replace Autoprefixer \`browsers\` option to Browserslist config. + Use \`browserslist\` key in \`package.json\` or \`.browserslistrc\` file. + + Using \`browsers\` option can cause errors. Browserslist config can + be used for Babel, Autoprefixer, postcss-normalize and other tools. + + If you really need to use option, rename it to \`overrideBrowserslist\`. + + Learn more at: + https://github.com/browserslist/browserslist#readme + https://twitter.com/browserslist + +`;function TO(r){return Object.prototype.toString.apply(r)==="[object Object]"}var su=new Map;function PO(r,e){e.browsers.selected.length!==0&&(e.add.selectors.length>0||Object.keys(e.add).length>2||r.warn(`Autoprefixer target browsers do not need any prefixes.You do not need Autoprefixer anymore. +Check your Browserslist config to be sure that your targets are set up correctly. + + Learn more at: + https://github.com/postcss/autoprefixer#readme + https://github.com/browserslist/browserslist#readme + +`))}cb.exports=or;function or(...r){let e;if(r.length===1&&TO(r[0])?(e=r[0],r=void 0):r.length===0||r.length===1&&!r[0]?r=void 0:r.length<=2&&(Array.isArray(r[0])||!r[0])?(e=r[1],r=r[0]):typeof r[r.length-1]=="object"&&(e=r.pop()),e||(e={}),e.browser)throw new Error("Change `browser` option to `overrideBrowserslist` in Autoprefixer");if(e.browserslist)throw new Error("Change `browserslist` option to `overrideBrowserslist` in Autoprefixer");e.overrideBrowserslist?r=e.overrideBrowserslist:e.browsers&&(typeof console!="undefined"&&console.warn&&(nu.red?console.warn(nu.red(fb.replace(/`[^`]+`/g,n=>nu.yellow(n.slice(1,-1))))):console.warn(fb)),r=e.browsers);let t={ignoreUnknownVersions:e.ignoreUnknownVersions,stats:e.stats,env:e.env};function i(n){let a=ub,s=new AO(a.browsers,r,n,t),o=s.selected.join(", ")+JSON.stringify(e);return su.has(o)||su.set(o,new _O(a.prefixes,s,e)),su.get(o)}return{postcssPlugin:"autoprefixer",prepare(n){let a=i({from:n.opts.from,env:e.env});return{OnceExit(s){PO(n,a),e.remove!==!1&&a.processor.remove(s,n),e.add!==!1&&a.processor.add(s,n)}}},info(n){return n=n||{},n.from=n.from||h.cwd(),EO(i(n))},options:e,browsers:r}}or.postcss=!0;or.data=ub;or.defaults=SO.defaults;or.info=()=>or().info()});var hb=v((rq,db)=>{l();db.exports={aqua:/#00ffff(ff)?(?!\w)|#0ff(f)?(?!\w)/gi,azure:/#f0ffff(ff)?(?!\w)/gi,beige:/#f5f5dc(ff)?(?!\w)/gi,bisque:/#ffe4c4(ff)?(?!\w)/gi,black:/#000000(ff)?(?!\w)|#000(f)?(?!\w)/gi,blue:/#0000ff(ff)?(?!\w)|#00f(f)?(?!\w)/gi,brown:/#a52a2a(ff)?(?!\w)/gi,coral:/#ff7f50(ff)?(?!\w)/gi,cornsilk:/#fff8dc(ff)?(?!\w)/gi,crimson:/#dc143c(ff)?(?!\w)/gi,cyan:/#00ffff(ff)?(?!\w)|#0ff(f)?(?!\w)/gi,darkblue:/#00008b(ff)?(?!\w)/gi,darkcyan:/#008b8b(ff)?(?!\w)/gi,darkgrey:/#a9a9a9(ff)?(?!\w)/gi,darkred:/#8b0000(ff)?(?!\w)/gi,deeppink:/#ff1493(ff)?(?!\w)/gi,dimgrey:/#696969(ff)?(?!\w)/gi,gold:/#ffd700(ff)?(?!\w)/gi,green:/#008000(ff)?(?!\w)/gi,grey:/#808080(ff)?(?!\w)/gi,honeydew:/#f0fff0(ff)?(?!\w)/gi,hotpink:/#ff69b4(ff)?(?!\w)/gi,indigo:/#4b0082(ff)?(?!\w)/gi,ivory:/#fffff0(ff)?(?!\w)/gi,khaki:/#f0e68c(ff)?(?!\w)/gi,lavender:/#e6e6fa(ff)?(?!\w)/gi,lime:/#00ff00(ff)?(?!\w)|#0f0(f)?(?!\w)/gi,linen:/#faf0e6(ff)?(?!\w)/gi,maroon:/#800000(ff)?(?!\w)/gi,moccasin:/#ffe4b5(ff)?(?!\w)/gi,navy:/#000080(ff)?(?!\w)/gi,oldlace:/#fdf5e6(ff)?(?!\w)/gi,olive:/#808000(ff)?(?!\w)/gi,orange:/#ffa500(ff)?(?!\w)/gi,orchid:/#da70d6(ff)?(?!\w)/gi,peru:/#cd853f(ff)?(?!\w)/gi,pink:/#ffc0cb(ff)?(?!\w)/gi,plum:/#dda0dd(ff)?(?!\w)/gi,purple:/#800080(ff)?(?!\w)/gi,red:/#ff0000(ff)?(?!\w)|#f00(f)?(?!\w)/gi,salmon:/#fa8072(ff)?(?!\w)/gi,seagreen:/#2e8b57(ff)?(?!\w)/gi,seashell:/#fff5ee(ff)?(?!\w)/gi,sienna:/#a0522d(ff)?(?!\w)/gi,silver:/#c0c0c0(ff)?(?!\w)/gi,skyblue:/#87ceeb(ff)?(?!\w)/gi,snow:/#fffafa(ff)?(?!\w)/gi,tan:/#d2b48c(ff)?(?!\w)/gi,teal:/#008080(ff)?(?!\w)/gi,thistle:/#d8bfd8(ff)?(?!\w)/gi,tomato:/#ff6347(ff)?(?!\w)/gi,violet:/#ee82ee(ff)?(?!\w)/gi,wheat:/#f5deb3(ff)?(?!\w)/gi,white:/#ffffff(ff)?(?!\w)|#fff(f)?(?!\w)/gi}});var gb=v((iq,mb)=>{l();var au=hb(),ou={whitespace:/\s+/g,urlHexPairs:/%[\dA-F]{2}/g,quotes:/"/g};function DO(r){return r.trim().replace(ou.whitespace," ")}function IO(r){return encodeURIComponent(r).replace(ou.urlHexPairs,RO)}function qO(r){return Object.keys(au).forEach(function(e){au[e].test(r)&&(r=r.replace(au[e],e))}),r}function RO(r){switch(r){case"%20":return" ";case"%3D":return"=";case"%3A":return":";case"%2F":return"/";default:return r.toLowerCase()}}function lu(r){if(typeof r!="string")throw new TypeError("Expected a string, but received "+typeof r);r.charCodeAt(0)===65279&&(r=r.slice(1));var e=qO(DO(r)).replace(ou.quotes,"'");return"data:image/svg+xml,"+IO(e)}lu.toSrcset=function(e){return lu(e).replace(/ /g,"%20")};mb.exports=lu});var uu={};Ae(uu,{default:()=>MO});var yb,MO,fu=C(()=>{l();wi();yb=X(Si()),MO=et(yb.default.theme)});var kb=v((sq,xb)=>{l();var Zn=gb(),BO=(qn(),In).default,wb=(fu(),uu).default,gt=(mi(),as).default,[FO,{lineHeight:LO}]=wb.fontSize.base,{spacing:Je,borderWidth:bb,borderRadius:vb}=wb;function yt(r,e){return r.replace("",`var(${e}, 1)`)}var NO=BO.withOptions(function(r={strategy:void 0}){return function({addBase:e,addComponents:t,theme:i}){let n=r.strategy===void 0?["base","class"]:[r.strategy],a=[{base:["[type='text']","input:where(:not([type]))","[type='email']","[type='url']","[type='password']","[type='number']","[type='date']","[type='datetime-local']","[type='month']","[type='search']","[type='tel']","[type='time']","[type='week']","[multiple]","textarea","select"],class:[".form-input",".form-textarea",".form-select",".form-multiselect"],styles:{appearance:"none","background-color":"#fff","border-color":yt(i("colors.gray.500",gt.gray[500]),"--tw-border-opacity"),"border-width":bb.DEFAULT,"border-radius":vb.none,"padding-top":Je[2],"padding-right":Je[3],"padding-bottom":Je[2],"padding-left":Je[3],"font-size":FO,"line-height":LO,"--tw-shadow":"0 0 #0000","&:focus":{outline:"2px solid transparent","outline-offset":"2px","--tw-ring-inset":"var(--tw-empty,/*!*/ /*!*/)","--tw-ring-offset-width":"0px","--tw-ring-offset-color":"#fff","--tw-ring-color":yt(i("colors.blue.600",gt.blue[600]),"--tw-ring-opacity"),"--tw-ring-offset-shadow":"var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)","--tw-ring-shadow":"var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)","box-shadow":"var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)","border-color":yt(i("colors.blue.600",gt.blue[600]),"--tw-border-opacity")}}},{base:["input::placeholder","textarea::placeholder"],class:[".form-input::placeholder",".form-textarea::placeholder"],styles:{color:yt(i("colors.gray.500",gt.gray[500]),"--tw-text-opacity"),opacity:"1"}},{base:["::-webkit-datetime-edit-fields-wrapper"],class:[".form-input::-webkit-datetime-edit-fields-wrapper"],styles:{padding:"0"}},{base:["::-webkit-date-and-time-value"],class:[".form-input::-webkit-date-and-time-value"],styles:{"min-height":"1.5em"}},{base:["::-webkit-date-and-time-value"],class:[".form-input::-webkit-date-and-time-value"],styles:{"text-align":"inherit"}},{base:["::-webkit-datetime-edit"],class:[".form-input::-webkit-datetime-edit"],styles:{display:"inline-flex"}},{base:["::-webkit-datetime-edit","::-webkit-datetime-edit-year-field","::-webkit-datetime-edit-month-field","::-webkit-datetime-edit-day-field","::-webkit-datetime-edit-hour-field","::-webkit-datetime-edit-minute-field","::-webkit-datetime-edit-second-field","::-webkit-datetime-edit-millisecond-field","::-webkit-datetime-edit-meridiem-field"],class:[".form-input::-webkit-datetime-edit",".form-input::-webkit-datetime-edit-year-field",".form-input::-webkit-datetime-edit-month-field",".form-input::-webkit-datetime-edit-day-field",".form-input::-webkit-datetime-edit-hour-field",".form-input::-webkit-datetime-edit-minute-field",".form-input::-webkit-datetime-edit-second-field",".form-input::-webkit-datetime-edit-millisecond-field",".form-input::-webkit-datetime-edit-meridiem-field"],styles:{"padding-top":0,"padding-bottom":0}},{base:["select"],class:[".form-select"],styles:{"background-image":`url("${Zn(``)}")`,"background-position":`right ${Je[2]} center`,"background-repeat":"no-repeat","background-size":"1.5em 1.5em","padding-right":Je[10],"print-color-adjust":"exact"}},{base:["[multiple]",'[size]:where(select:not([size="1"]))'],class:['.form-select:where([size]:not([size="1"]))'],styles:{"background-image":"initial","background-position":"initial","background-repeat":"unset","background-size":"initial","padding-right":Je[3],"print-color-adjust":"unset"}},{base:["[type='checkbox']","[type='radio']"],class:[".form-checkbox",".form-radio"],styles:{appearance:"none",padding:"0","print-color-adjust":"exact",display:"inline-block","vertical-align":"middle","background-origin":"border-box","user-select":"none","flex-shrink":"0",height:Je[4],width:Je[4],color:yt(i("colors.blue.600",gt.blue[600]),"--tw-text-opacity"),"background-color":"#fff","border-color":yt(i("colors.gray.500",gt.gray[500]),"--tw-border-opacity"),"border-width":bb.DEFAULT,"--tw-shadow":"0 0 #0000"}},{base:["[type='checkbox']"],class:[".form-checkbox"],styles:{"border-radius":vb.none}},{base:["[type='radio']"],class:[".form-radio"],styles:{"border-radius":"100%"}},{base:["[type='checkbox']:focus","[type='radio']:focus"],class:[".form-checkbox:focus",".form-radio:focus"],styles:{outline:"2px solid transparent","outline-offset":"2px","--tw-ring-inset":"var(--tw-empty,/*!*/ /*!*/)","--tw-ring-offset-width":"2px","--tw-ring-offset-color":"#fff","--tw-ring-color":yt(i("colors.blue.600",gt.blue[600]),"--tw-ring-opacity"),"--tw-ring-offset-shadow":"var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)","--tw-ring-shadow":"var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)","box-shadow":"var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)"}},{base:["[type='checkbox']:checked","[type='radio']:checked"],class:[".form-checkbox:checked",".form-radio:checked"],styles:{"border-color":"transparent","background-color":"currentColor","background-size":"100% 100%","background-position":"center","background-repeat":"no-repeat"}},{base:["[type='checkbox']:checked"],class:[".form-checkbox:checked"],styles:{"background-image":`url("${Zn('')}")`,"@media (forced-colors: active) ":{appearance:"auto"}}},{base:["[type='radio']:checked"],class:[".form-radio:checked"],styles:{"background-image":`url("${Zn('')}")`,"@media (forced-colors: active) ":{appearance:"auto"}}},{base:["[type='checkbox']:checked:hover","[type='checkbox']:checked:focus","[type='radio']:checked:hover","[type='radio']:checked:focus"],class:[".form-checkbox:checked:hover",".form-checkbox:checked:focus",".form-radio:checked:hover",".form-radio:checked:focus"],styles:{"border-color":"transparent","background-color":"currentColor"}},{base:["[type='checkbox']:indeterminate"],class:[".form-checkbox:indeterminate"],styles:{"background-image":`url("${Zn('')}")`,"border-color":"transparent","background-color":"currentColor","background-size":"100% 100%","background-position":"center","background-repeat":"no-repeat","@media (forced-colors: active) ":{appearance:"auto"}}},{base:["[type='checkbox']:indeterminate:hover","[type='checkbox']:indeterminate:focus"],class:[".form-checkbox:indeterminate:hover",".form-checkbox:indeterminate:focus"],styles:{"border-color":"transparent","background-color":"currentColor"}},{base:["[type='file']"],class:null,styles:{background:"unset","border-color":"inherit","border-width":"0","border-radius":"0",padding:"0","font-size":"unset","line-height":"inherit"}},{base:["[type='file']:focus"],class:null,styles:{outline:["1px solid ButtonText","1px auto -webkit-focus-ring-color"]}}],s=o=>a.map(u=>u[o]===null?null:{[u[o]]:u.styles}).filter(Boolean);n.includes("base")&&e(s("base")),n.includes("class")&&t(s("class"))}});xb.exports=NO});var Sb={};Ae(Sb,{default:()=>$O});var $O,Cb=C(()=>{l();$O=[kb()]});var _b={};Ae(_b,{default:()=>zO});var Ab,zO,Ob=C(()=>{l();wi();Ab=X(Si()),zO=et(Ab.default)});l();"use strict";var jO=Xe(xm()),VO=Xe(ge()),UO=Xe(pb()),WO=Xe((Cb(),Sb)),GO=Xe((fu(),uu)),HO=Xe((Ob(),_b)),YO=Xe((mi(),as)),QO=Xe((qn(),In)),JO=Xe((xs(),lf));function Xe(r){return r&&r.__esModule?r:{default:r}}console.warn("cdn.tailwindcss.com should not be used in production. To use Tailwind CSS in production, install it as a PostCSS plugin or use the Tailwind CLI: https://tailwindcss.com/docs/installation");var es="tailwind",cu="text/tailwindcss",Eb="/template.html",Ct,Tb=!0,Pb=0,pu=new Set,du,Db="",Ib=(r=!1)=>({get(e,t){return(!r||t==="config")&&typeof e[t]=="object"&&e[t]!==null?new Proxy(e[t],Ib()):e[t]},set(e,t,i){return e[t]=i,(!r||t==="config")&&hu(!0),!0}});window[es]=new Proxy({config:{},defaultTheme:GO.default,defaultConfig:HO.default,colors:YO.default,plugin:QO.default,resolveConfig:JO.default},Ib(!0));function qb(r){du.observe(r,{attributes:!0,attributeFilter:["type"],characterData:!0,subtree:!0,childList:!0})}new MutationObserver(async r=>{let e=!1;if(!du){du=new MutationObserver(async()=>await hu(!0));for(let t of document.querySelectorAll(`style[type="${cu}"]`))qb(t)}for(let t of r)for(let i of t.addedNodes)i.nodeType===1&&i.tagName==="STYLE"&&i.getAttribute("type")===cu&&(qb(i),e=!0);await hu(e)}).observe(document.documentElement,{attributes:!0,attributeFilter:["class"],childList:!0,subtree:!0});async function hu(r=!1){r&&(Pb++,pu.clear());let e="";for(let i of document.querySelectorAll(`style[type="${cu}"]`))e+=i.textContent;let t=new Set;for(let i of document.querySelectorAll("[class]"))for(let n of i.classList)pu.has(n)||t.add(n);if(document.body&&(Tb||t.size>0||e!==Db||!Ct||!Ct.isConnected)){for(let n of t)pu.add(n);Tb=!1,Db=e,self[Eb]=Array.from(t).join(" ");let{css:i}=await(0,VO.default)([(0,jO.default)({...window[es].config,_hash:Pb,content:[Eb],plugins:[...WO.default,...Array.isArray(window[es].config.plugins)?window[es].config.plugins:[]]}),(0,UO.default)({remove:!1})]).process(`@tailwind base;@tailwind components;@tailwind utilities;${e}`);(!Ct||!Ct.isConnected)&&(Ct=document.createElement("style"),document.head.append(Ct)),Ct.textContent=i}}})(); +/*! https://mths.be/cssesc v3.0.0 by @mathias */ diff --git a/electron/build/icon.png b/electron/build/icon.png new file mode 100644 index 0000000..aea1c67 Binary files /dev/null and b/electron/build/icon.png differ diff --git a/electron/loading.html b/electron/loading.html new file mode 100644 index 0000000..e49f966 --- /dev/null +++ b/electron/loading.html @@ -0,0 +1,161 @@ + + + + + + + MeshChatX + + + + +
+
+
+
+ +
+
+
+
+ +
+
+ MeshChatX logo +
+
+

MeshChatX

+
MeshChatX
+
Custom fork by Sudo-Ivan
+
+
+ +
+
+ + Preparing your node +
+
+ + Starting services +
+
+ +
+
+ + + +
+
+
Loading services
+
Waiting for the MeshChatX API to come online.
+
+
+ +
+
+
Version
+
v0.0.0
+
+
+
Status
+
Booting
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/electron/main.js b/electron/main.js new file mode 100644 index 0000000..83f818f --- /dev/null +++ b/electron/main.js @@ -0,0 +1,346 @@ +const { app, BrowserWindow, dialog, ipcMain, shell, systemPreferences } = require("electron"); +const electronPrompt = require("electron-prompt"); +const { spawn } = require("child_process"); +const fs = require("fs"); +const path = require("node:path"); + +// remember main window +var mainWindow = null; + +// remember child process for exe so we can kill it when app exits +var exeChildProcess = null; + +// allow fetching app version via ipc +ipcMain.handle("app-version", () => { + return app.getVersion(); +}); + +// add support for showing an alert window via ipc +ipcMain.handle("alert", async (event, message) => { + return await dialog.showMessageBox(mainWindow, { + message: message, + }); +}); + +// add support for showing a confirm window via ipc +ipcMain.handle("confirm", async (event, message) => { + // show confirm dialog + const result = await dialog.showMessageBox(mainWindow, { + type: "question", + title: "Confirm", + message: message, + cancelId: 0, // esc key should press cancel button + defaultId: 1, // enter key should press ok button + buttons: [ + "Cancel", // 0 + "OK", // 1 + ], + }); + + // check if user clicked OK + return result.response === 1; +}); + +// add support for showing a prompt window via ipc +ipcMain.handle("prompt", async (event, message) => { + return await electronPrompt({ + title: message, + label: "", + value: "", + type: "input", + inputAttrs: { + type: "text", + }, + }); +}); + +// allow relaunching app via ipc +ipcMain.handle("relaunch", () => { + app.relaunch(); + app.exit(); +}); + +// allow showing a file path in os file manager +ipcMain.handle("showPathInFolder", (event, path) => { + shell.showItemInFolder(path); +}); + +function log(message) { + // log to stdout of this process + console.log(message); + + // make sure main window exists + if (!mainWindow) { + return; + } + + // make sure window is not destroyed + if (mainWindow.isDestroyed()) { + return; + } + + // log to web console + mainWindow.webContents.send("log", message); +} + +function getDefaultStorageDir() { + // if we are running a windows portable exe, we want to use .reticulum-meshchat in the portable exe dir + // e.g if we launch "E:\Some\Path\MeshChat.exe" we want to use "E:\Some\Path\.reticulum-meshchat" + const portableExecutableDir = process.env.PORTABLE_EXECUTABLE_DIR; + if (process.platform === "win32" && portableExecutableDir != null) { + return path.join(portableExecutableDir, ".reticulum-meshchat"); + } + + // otherwise, we will fall back to putting the storage dir in the users home directory + // e.g: ~/.reticulum-meshchat + return path.join(app.getPath("home"), ".reticulum-meshchat"); +} + +function getDefaultReticulumConfigDir() { + // if we are running a windows portable exe, we want to use .reticulum in the portable exe dir + // e.g if we launch "E:\Some\Path\MeshChat.exe" we want to use "E:\Some\Path\.reticulum" + const portableExecutableDir = process.env.PORTABLE_EXECUTABLE_DIR; + if (process.platform === "win32" && portableExecutableDir != null) { + return path.join(portableExecutableDir, ".reticulum"); + } + + // otherwise, we will fall back to using the .reticulum folder in the users home directory + // e.g: ~/.reticulum + return path.join(app.getPath("home"), ".reticulum"); +} + +app.whenReady().then(async () => { + // get arguments passed to application, and remove the provided application path + const ignoredArguments = ["--no-sandbox", "--ozone-platform-hint=auto"]; + const userProvidedArguments = process.argv.slice(1).filter((arg) => !ignoredArguments.includes(arg)); + const shouldLaunchHeadless = userProvidedArguments.includes("--headless"); + + if (!shouldLaunchHeadless) { + // create browser window + mainWindow = new BrowserWindow({ + width: 1500, + height: 800, + webPreferences: { + // used to inject logging over ipc + preload: path.join(__dirname, "preload.js"), + // Security: disable node integration in renderer + nodeIntegration: false, + // Security: enable context isolation (default in Electron 12+) + contextIsolation: true, + // Security: enable sandbox for additional protection + sandbox: true, + // Security: disable remote module (deprecated but explicit) + enableRemoteModule: false, + }, + }); + + // open external links in default web browser instead of electron + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + var shouldShowInNewElectronWindow = false; + + // we want to open call.html in a new electron window + // but all other target="_blank" links should open in the system web browser + // we don't want /rnode-flasher/index.html to open in electron, otherwise user can't select usb devices... + if ( + (url.startsWith("http://localhost") || url.startsWith("https://localhost")) && + url.includes("/call.html") + ) { + shouldShowInNewElectronWindow = true; + } + + // we want to open blob urls in a new electron window + else if (url.startsWith("blob:")) { + shouldShowInNewElectronWindow = true; + } + + // open in new electron window + if (shouldShowInNewElectronWindow) { + return { + action: "allow", + }; + } + + // fallback to opening any other url in external browser + shell.openExternal(url); + return { + action: "deny", + }; + }); + + // navigate to loading page + await mainWindow.loadFile(path.join(__dirname, "loading.html")); + + // ask mac users for microphone access for audio calls to work + if (process.platform === "darwin") { + await systemPreferences.askForMediaAccess("microphone"); + } + } + + // find path to python/cxfreeze reticulum meshchat executable + // Note: setup.py creates ReticulumMeshChatX (with X), not ReticulumMeshChat + const exeName = process.platform === "win32" ? "ReticulumMeshChatX.exe" : "ReticulumMeshChatX"; + + // get app path (handles both development and packaged app) + const appPath = app.getAppPath(); + // get resources path (where extraFiles are placed) + const resourcesPath = process.resourcesPath || path.join(appPath, "..", ".."); + var exe = null; + + // when packaged, extraFiles are placed at resources/app/electron/build/exe + // when packaged with asar, unpacked files are in app.asar.unpacked/ directory + // app.getAppPath() returns the path to app.asar, so unpacked is at the same level + const possiblePaths = [ + // packaged app - extraFiles location (resources/app/electron/build/exe) + path.join(resourcesPath, "app", "electron", "build", "exe", exeName), + // packaged app with asar (unpacked files from asarUnpack) + path.join(appPath, "..", "app.asar.unpacked", "build", "exe", exeName), + // packaged app without asar (relative to app path) + path.join(appPath, "build", "exe", exeName), + // development mode (relative to electron directory) + path.join(__dirname, "build", "exe", exeName), + // development mode (relative to project root) + path.join(__dirname, "..", "build", "exe", exeName), + ]; + + // find the first path that exists + for (const possibleExe of possiblePaths) { + if (fs.existsSync(possibleExe)) { + exe = possibleExe; + break; + } + } + + // verify executable exists + if (!exe || !fs.existsSync(exe)) { + const errorMsg = `Could not find executable: ${exeName}\nChecked paths:\n${possiblePaths.join("\n")}\n\nApp path: ${appPath}\nResources path: ${resourcesPath}`; + log(errorMsg); + if (mainWindow) { + await dialog.showMessageBox(mainWindow, { + message: errorMsg, + }); + } + app.quit(); + return; + } + + log(`Found executable at: ${exe}`); + + try { + // arguments we always want to pass in + const requiredArguments = [ + "--headless", // reticulum meshchat usually launches default web browser, we don't want this when using electron + "--port", + "9337", // FIXME: let system pick a random unused port? + // '--test-exception-message', 'Test Exception Message', // uncomment to test the crash dialog + ]; + + // if user didn't provide reticulum config dir, we should provide it + if (!userProvidedArguments.includes("--reticulum-config-dir")) { + requiredArguments.push("--reticulum-config-dir", getDefaultReticulumConfigDir()); + } + + // if user didn't provide storage dir, we should provide it + if (!userProvidedArguments.includes("--storage-dir")) { + requiredArguments.push("--storage-dir", getDefaultStorageDir()); + } + + // spawn executable + exeChildProcess = await spawn(exe, [ + ...requiredArguments, // always provide required arguments + ...userProvidedArguments, // also include any user provided arguments + ]); + + // log stdout + var stdoutLines = []; + exeChildProcess.stdout.setEncoding("utf8"); + exeChildProcess.stdout.on("data", function (data) { + // log + log(data.toString()); + + // keep track of last 10 stdout lines + stdoutLines.push(data.toString()); + if (stdoutLines.length > 10) { + stdoutLines.shift(); + } + }); + + // log stderr + var stderrLines = []; + exeChildProcess.stderr.setEncoding("utf8"); + exeChildProcess.stderr.on("data", function (data) { + // log + log(data.toString()); + + // keep track of last 10 stderr lines + stderrLines.push(data.toString()); + if (stderrLines.length > 10) { + stderrLines.shift(); + } + }); + + // log errors + exeChildProcess.on("error", function (error) { + log(error); + }); + + // quit electron app if exe dies + exeChildProcess.on("exit", async function (code) { + // if no exit code provided, we wanted exit to happen, so do nothing + if (code == null) { + return; + } + + // tell user that Visual C++ redistributable needs to be installed on Windows + if (code === 3221225781 && process.platform === "win32") { + await dialog.showMessageBox(mainWindow, { + message: "Microsoft Visual C++ redistributable must be installed to run this application.", + }); + app.quit(); + return; + } + + // show crash log + const stdout = stdoutLines.join(""); + const stderr = stderrLines.join(""); + await dialog.showMessageBox(mainWindow, { + message: [ + "MeshChat Crashed!", + "", + `Exit Code: ${code}`, + "", + `----- stdout -----`, + "", + stdout, + `----- stderr -----`, + "", + stderr, + ].join("\n"), + }); + + // quit after dismissing error dialog + app.quit(); + }); + } catch (e) { + log(e); + } +}); + +function quit() { + // kill python process + if (exeChildProcess) { + exeChildProcess.kill("SIGKILL"); + } + + // quit electron app + app.quit(); +} + +// quit electron if all windows are closed +app.on("window-all-closed", () => { + quit(); +}); + +// make sure child process is killed if app is quiting +app.on("quit", () => { + quit(); +}); diff --git a/electron/preload.js b/electron/preload.js new file mode 100644 index 0000000..d677747 --- /dev/null +++ b/electron/preload.js @@ -0,0 +1,36 @@ +const { ipcRenderer, contextBridge } = require("electron"); + +// forward logs received from exe to web console +ipcRenderer.on("log", (event, message) => console.log(message)); + +contextBridge.exposeInMainWorld("electron", { + // allow fetching app version in electron browser window + appVersion: async function () { + return await ipcRenderer.invoke("app-version"); + }, + + // show an alert dialog in electron browser window, this fixes a bug where alert breaks input fields on windows + alert: async function (message) { + return await ipcRenderer.invoke("alert", message); + }, + + // show a confirm dialog in electron browser window, this fixes a bug where confirm breaks input fields on windows + confirm: async function (message) { + return await ipcRenderer.invoke("confirm", message); + }, + + // add support for using "prompt" in electron browser window + prompt: async function (message) { + return await ipcRenderer.invoke("prompt", message); + }, + + // allow relaunching app in electron browser window + relaunch: async function () { + return await ipcRenderer.invoke("relaunch"); + }, + + // allow showing a file path in os file manager + showPathInFolder: async function (path) { + return await ipcRenderer.invoke("showPathInFolder", path); + }, +}); diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..4938630 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,53 @@ +import js from "@eslint/js"; +import pluginVue from "eslint-plugin-vue"; +import pluginPrettier from "eslint-plugin-prettier/recommended"; +import globals from "globals"; + +export default [ + { + ignores: [ + "**/node_modules/**", + "**/dist/**", + "**/build/**", + "**/electron/assets/**", + "**/meshchatx/public/**", + "**/meshchatx/src/frontend/public/**", + "**/storage/**", + "**/__pycache__/**", + "**/.venv/**", + "**/*.min.js", + "**/pnpm-lock.yaml", + "**/poetry.lock", + "**/linux-unpacked/**", + "**/win-unpacked/**", + "**/mac-unpacked/**", + "**/*.asar", + "**/*.asar.unpacked/**", + "**/*.wasm", + "**/*.proto", + ], + }, + { + files: ["**/*.{js,mjs,cjs,vue}"], + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + axios: "readonly", + Codec2Lib: "readonly", + Codec2MicrophoneRecorder: "readonly", + }, + }, + }, + js.configs.recommended, + ...pluginVue.configs["flat/recommended"], + pluginPrettier, + { + files: ["**/*.{js,mjs,cjs,vue}"], + rules: { + "vue/multi-word-component-names": "off", + "no-unused-vars": "warn", + "no-console": "off", + }, + }, +]; diff --git a/logo/icon.ico b/logo/icon.ico new file mode 100644 index 0000000..f00e77e Binary files /dev/null and b/logo/icon.ico differ diff --git a/logo/logo-chat-bubble.png b/logo/logo-chat-bubble.png new file mode 100644 index 0000000..aea1c67 Binary files /dev/null and b/logo/logo-chat-bubble.png differ diff --git a/logo/logo.afdesign b/logo/logo.afdesign new file mode 100644 index 0000000..c2a6696 Binary files /dev/null and b/logo/logo.afdesign differ diff --git a/logo/logo.png b/logo/logo.png new file mode 100644 index 0000000..ab9ae1f Binary files /dev/null and b/logo/logo.png differ diff --git a/meshchatx/__init__.py b/meshchatx/__init__.py new file mode 100644 index 0000000..4ccf170 --- /dev/null +++ b/meshchatx/__init__.py @@ -0,0 +1,3 @@ +"""Reticulum MeshChatX - A mesh network communications app.""" + +__version__ = "2.50.0" diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py new file mode 100644 index 0000000..32d8ba4 --- /dev/null +++ b/meshchatx/meshchat.py @@ -0,0 +1,7025 @@ +#!/usr/bin/env python + +import argparse +import asyncio +import base64 +import copy +import io +import ipaddress +import json +import os +import platform +import secrets +import shutil +import ssl +import sys +import tempfile +import threading +import time +import webbrowser +import zipfile +from collections.abc import Callable +from datetime import UTC, datetime, timedelta + +import bcrypt +import LXMF +import LXST +import psutil +import RNS +import RNS.vendor.umsgpack as msgpack +from aiohttp import WSCloseCode, WSMessage, WSMsgType, web +from aiohttp_session import get_session +from aiohttp_session import setup as setup_session +from aiohttp_session.cookie_storage import EncryptedCookieStorage +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID +from LXMF import LXMRouter +from serial.tools import list_ports + +from meshchatx.src.backend.announce_handler import AnnounceHandler +from meshchatx.src.backend.announce_manager import AnnounceManager +from meshchatx.src.backend.archiver_manager import ArchiverManager +from meshchatx.src.backend.async_utils import AsyncUtils +from meshchatx.src.backend.colour_utils import ColourUtils +from meshchatx.src.backend.config_manager import ConfigManager +from meshchatx.src.backend.database import Database +from meshchatx.src.backend.forwarding_manager import ForwardingManager +from meshchatx.src.backend.interface_config_parser import InterfaceConfigParser +from meshchatx.src.backend.interface_editor import InterfaceEditor +from meshchatx.src.backend.lxmf_message_fields import ( + LxmfAudioField, + LxmfFileAttachment, + LxmfFileAttachmentsField, + LxmfImageField, +) +from meshchatx.src.backend.map_manager import MapManager +from meshchatx.src.backend.message_handler import MessageHandler +from meshchatx.src.backend.rncp_handler import RNCPHandler +from meshchatx.src.backend.rnprobe_handler import RNProbeHandler +from meshchatx.src.backend.rnstatus_handler import RNStatusHandler +from meshchatx.src.backend.sideband_commands import SidebandCommands +from meshchatx.src.backend.telephone_manager import TelephoneManager +from meshchatx.src.backend.translator_handler import TranslatorHandler +from meshchatx.src.version import __version__ as app_version + + +# NOTE: this is required to be able to pack our app with cxfreeze as an exe, otherwise it can't access bundled assets +# this returns a file path based on if we are running meshchat.py directly, or if we have packed it as an exe with cxfreeze +# https://cx-freeze.readthedocs.io/en/latest/faq.html#using-data-files +# bearer:disable python_lang_path_traversal +def get_file_path(filename): + if getattr(sys, "frozen", False): + datadir = os.path.dirname(sys.executable) + return os.path.join(datadir, filename) + + # Assets live inside the meshchatx package when installed from a wheel + package_dir = os.path.dirname(__file__) + package_path = os.path.join(package_dir, filename) + if os.path.exists(package_path): + return package_path + + # When running from the repository, fall back to the project root + repo_root = os.path.dirname(package_dir) + repo_path = os.path.join(repo_root, filename) + if os.path.exists(repo_path): + return repo_path + + return package_path + + +def generate_ssl_certificate(cert_path: str, key_path: str): + """Generate a self-signed SSL certificate for local HTTPS. + + Args: + cert_path: Path where the certificate will be saved + key_path: Path where the private key will be saved + + """ + if os.path.exists(cert_path) and os.path.exists(key_path): + return + + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend(), + ) + + subject = issuer = x509.Name( + [ + x509.NameAttribute(NameOID.COUNTRY_NAME, "US"), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Local"), + x509.NameAttribute(NameOID.LOCALITY_NAME, "Local"), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Reticulum MeshChatX"), + x509.NameAttribute(NameOID.COMMON_NAME, "localhost"), + ], + ) + + cert = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(private_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.now(UTC)) + .not_valid_after(datetime.now(UTC) + timedelta(days=365)) + .add_extension( + x509.SubjectAlternativeName( + [ + x509.DNSName("localhost"), + x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")), + x509.IPAddress(ipaddress.IPv6Address("::1")), + ], + ), + critical=False, + ) + .sign(private_key, hashes.SHA256(), default_backend()) + ) + + cert_dir = os.path.dirname(cert_path) + if cert_dir: + os.makedirs(cert_dir, exist_ok=True) + + with open(cert_path, "wb") as f: + f.write(cert.public_bytes(serialization.Encoding.PEM)) + + with open(key_path, "wb") as f: + f.write( + private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ), + ) + + +class ReticulumMeshChat: + def __init__( + self, + identity: RNS.Identity, + storage_dir, + reticulum_config_dir, + auto_recover: bool = False, + identity_file_path: str | None = None, + auth_enabled: bool = False, + ): + # when providing a custom storage_dir, files will be saved as + # /identities// + # /identities//database.db + + # if storage_dir is not provided, we will use ./storage instead + # ./storage/identities// + # ./storage/identities//database.db + + # ensure a storage path exists for the loaded identity + self.storage_dir = storage_dir or os.path.join("storage") + self.storage_path = os.path.join( + self.storage_dir, + "identities", + identity.hash.hex(), + ) + self.identity_file_path = identity_file_path + print(f"Using Storage Path: {self.storage_path}") + os.makedirs(self.storage_path, exist_ok=True) + + # define path to files based on storage path + self.database_path = os.path.join(self.storage_path, "database.db") + lxmf_router_path = os.path.join(self.storage_path, "lxmf_router") + + # remember preference for automatic recovery + self.auto_recover = auto_recover + + # init database + self.database = Database(self.database_path) + self.db = self.database # keep for compatibility with parts I haven't changed yet + + try: + self.database.initialize() + # Try to auto-migrate from legacy database if this is a fresh start + self.database.migrate_from_legacy(reticulum_config_dir, identity.hash.hex()) + self._tune_sqlite_pragmas() + except Exception as exc: + if not self.auto_recover: + raise + + print(f"Database initialization failed, attempting auto recovery: {exc}") + self._run_startup_auto_recovery() + # retry once after recovery + self.database.initialize() + self._tune_sqlite_pragmas() + + # init config + self.config = ConfigManager(self.database) + + # init managers + self.message_handler = MessageHandler(self.database) + self.announce_manager = AnnounceManager(self.database) + self.archiver_manager = ArchiverManager(self.database) + self.map_manager = MapManager(self.config, self.storage_dir) + self.forwarding_manager = None # will init after lxmf router + + # remember if authentication is enabled + self.auth_enabled = auth_enabled or self.config.auth_enabled.get() + + # migrate database + # The new initialize() handles migrations automatically, but we still update the config if needed + self.config.database_version.set(self.database.schema.LATEST_VERSION) + + # vacuum database on start to shrink its file size + self.database.provider.vacuum() + + # lxmf messages in outbound or sending state should be marked as failed when app starts as they are no longer being processed + self.database.messages.mark_stuck_messages_as_failed() + + # init reticulum + self.reticulum = RNS.Reticulum(reticulum_config_dir) + self.identity = identity + + # init lxmf router + # get propagation node stamp cost from config (only used if running a propagation node) + propagation_stamp_cost = self.config.lxmf_propagation_node_stamp_cost.get() + self.message_router = LXMF.LXMRouter( + identity=self.identity, + storagepath=lxmf_router_path, + propagation_cost=propagation_stamp_cost, + ) + self.message_router.PROCESSING_INTERVAL = 1 + + # increase limit for incoming lxmf messages (received over a resource), to allow receiving larger attachments + # the lxmf router expects delivery_per_transfer_limit to be provided in kilobytes, so we will do that... + self.message_router.delivery_per_transfer_limit = ( + self.config.lxmf_delivery_transfer_limit_in_bytes.get() / 1000 + ) + + # register lxmf identity + inbound_stamp_cost = self.config.lxmf_inbound_stamp_cost.get() + self.local_lxmf_destination = self.message_router.register_delivery_identity( + identity=self.identity, + display_name=self.config.display_name.get(), + stamp_cost=inbound_stamp_cost, + ) + + # load and register all forwarding alias identities + self.forwarding_manager = ForwardingManager(self.database, self.message_router) + self.forwarding_manager.load_aliases() + + # set a callback for when an lxmf message is received + self.message_router.register_delivery_callback(self.on_lxmf_delivery) + + # update active propagation node + self.set_active_propagation_node( + self.config.lxmf_preferred_propagation_node_destination_hash.get(), + ) + + # enable propagation node (we don't call with false if disabled, as no need to announce disabled state every launch) + if self.config.lxmf_local_propagation_node_enabled.get(): + self.enable_local_propagation_node() + + # handle received announces based on aspect + RNS.Transport.register_announce_handler( + AnnounceHandler("lxst.telephony", self.on_telephone_announce_received), + ) + RNS.Transport.register_announce_handler( + AnnounceHandler("lxmf.delivery", self.on_lxmf_announce_received), + ) + RNS.Transport.register_announce_handler( + AnnounceHandler( + "lxmf.propagation", + self.on_lxmf_propagation_announce_received, + ), + ) + RNS.Transport.register_announce_handler( + AnnounceHandler( + "nomadnetwork.node", + self.on_nomadnet_node_announce_received, + ), + ) + + # remember websocket clients + self.websocket_clients: list[web.WebSocketResponse] = [] + + # track announce timestamps for rate calculation + self.announce_timestamps = [] + + # track download speeds for nomadnetwork files (list of tuples: (file_size_bytes, duration_seconds)) + self.download_speeds = [] + + # track active downloads (download_id -> downloader instance) + self.active_downloads = {} + self.download_id_counter = 0 + + # register audio call identity + # init telephone manager + self.telephone_manager = TelephoneManager( + identity=self.identity, + config_manager=self.config, + ) + self.telephone_manager.register_ringing_callback( + self.on_incoming_telephone_call, + ) + self.telephone_manager.register_established_callback( + self.on_telephone_call_established, + ) + self.telephone_manager.register_ended_callback( + self.on_telephone_call_ended, + ) + self.telephone_manager.init_telephone() + + # init RNCP handler + self.rncp_handler = RNCPHandler( + reticulum_instance=self.reticulum, + identity=self.identity, + storage_dir=self.storage_dir, + ) + + # init RNStatus handler + self.rnstatus_handler = RNStatusHandler(reticulum_instance=self.reticulum) + + # init RNProbe handler + self.rnprobe_handler = RNProbeHandler( + reticulum_instance=self.reticulum, + identity=self.identity, + ) + + # init Translator handler + libretranslate_url = self.config.get("libretranslate_url", None) + self.translator_handler = TranslatorHandler(libretranslate_url=libretranslate_url) + + # start background thread for auto announce loop + thread = threading.Thread(target=asyncio.run, args=(self.announce_loop(),)) + thread.daemon = True + thread.start() + + # start background thread for auto syncing propagation nodes + thread = threading.Thread( + target=asyncio.run, + args=(self.announce_sync_propagation_nodes(),), + ) + thread.daemon = True + thread.start() + + # start background thread for crawler loop + thread = threading.Thread( + target=asyncio.run, + args=(self.crawler_loop(),), + ) + thread.daemon = True + thread.start() + + def _tune_sqlite_pragmas(self): + try: + self.db.execute_sql("PRAGMA wal_autocheckpoint=1000") + self.db.execute_sql("PRAGMA temp_store=MEMORY") + self.db.execute_sql("PRAGMA journal_mode=WAL") + except Exception as exc: + print(f"SQLite pragma setup failed: {exc}") + + def _get_pragma_value(self, pragma: str, default=None): + try: + cursor = self.db.execute_sql(f"PRAGMA {pragma}") + row = cursor.fetchone() + if row is None: + return default + return row[0] + except Exception: + return default + + def _get_database_file_stats(self): + def size_for(path): + try: + return os.path.getsize(path) + except OSError: + return 0 + + wal_path = f"{self.database_path}-wal" + shm_path = f"{self.database_path}-shm" + + main_bytes = size_for(self.database_path) + wal_bytes = size_for(wal_path) + shm_bytes = size_for(shm_path) + + return { + "main_bytes": main_bytes, + "wal_bytes": wal_bytes, + "shm_bytes": shm_bytes, + "total_bytes": main_bytes + wal_bytes + shm_bytes, + } + + def _database_paths(self): + return { + "main": self.database_path, + "wal": f"{self.database_path}-wal", + "shm": f"{self.database_path}-shm", + } + + def get_database_health_snapshot(self): + page_size = self._get_pragma_value("page_size", 0) or 0 + page_count = self._get_pragma_value("page_count", 0) or 0 + freelist_pages = self._get_pragma_value("freelist_count", 0) or 0 + free_bytes = ( + page_size * freelist_pages if page_size > 0 and freelist_pages > 0 else 0 + ) + + return { + "quick_check": self._get_pragma_value("quick_check", "unknown"), + "journal_mode": self._get_pragma_value("journal_mode", "unknown"), + "synchronous": self._get_pragma_value("synchronous", None), + "wal_autocheckpoint": self._get_pragma_value("wal_autocheckpoint", None), + "auto_vacuum": self._get_pragma_value("auto_vacuum", None), + "page_size": page_size, + "page_count": page_count, + "freelist_pages": freelist_pages, + "estimated_free_bytes": free_bytes, + "files": self._get_database_file_stats(), + } + + def _checkpoint_wal(self, mode: str = "TRUNCATE"): + return self.db.execute_sql(f"PRAGMA wal_checkpoint({mode})").fetchall() + + def run_database_vacuum(self): + checkpoint = self._checkpoint_wal() + self.db.execute_sql("VACUUM") + self._tune_sqlite_pragmas() + + return { + "checkpoint": checkpoint, + "health": self.get_database_health_snapshot(), + } + + def run_database_recovery(self): + actions = [] + + actions.append( + { + "step": "quick_check_before", + "result": self._get_pragma_value("quick_check", "unknown"), + }, + ) + + actions.append({"step": "wal_checkpoint", "result": self._checkpoint_wal()}) + + integrity_rows = self.database.provider.integrity_check() + integrity = [row[0] for row in integrity_rows] if integrity_rows else [] + actions.append({"step": "integrity_check", "result": integrity}) + + self.database.provider.vacuum() + self._tune_sqlite_pragmas() + + actions.append( + { + "step": "quick_check_after", + "result": self._get_pragma_value("quick_check", "unknown"), + }, + ) + + return { + "actions": actions, + "health": self.get_database_health_snapshot(), + } + + def _checkpoint_and_close(self): + try: + self._checkpoint_wal() + except Exception as e: + print(f"Failed to checkpoint WAL: {e}") + try: + self.database.close() + except Exception as e: + print(f"Failed to close database: {e}") + + def _backup_to_zip(self, backup_path: str): + paths = self._database_paths() + os.makedirs(os.path.dirname(backup_path), exist_ok=True) + # ensure WAL is checkpointed to get a consistent snapshot + self._checkpoint_wal() + + with zipfile.ZipFile(backup_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: + zf.write(paths["main"], arcname="database.db") + if os.path.exists(paths["wal"]): + zf.write(paths["wal"], arcname="database.db-wal") + if os.path.exists(paths["shm"]): + zf.write(paths["shm"], arcname="database.db-shm") + + return { + "path": backup_path, + "size": os.path.getsize(backup_path), + } + + def backup_database(self, backup_path: str | None = None): + default_dir = os.path.join(self.storage_path, "database-backups") + os.makedirs(default_dir, exist_ok=True) + if backup_path is None: + timestamp = datetime.now(UTC).strftime("%Y%m%d-%H%M%S") + backup_path = os.path.join(default_dir, f"backup-{timestamp}.zip") + + return self._backup_to_zip(backup_path) + + def restore_database(self, backup_path: str): + if not os.path.exists(backup_path): + msg = f"Backup not found at {backup_path}" + raise FileNotFoundError(msg) + + paths = self._database_paths() + self._checkpoint_and_close() + + # clean existing files + for p in paths.values(): + if os.path.exists(p): + os.remove(p) + + if zipfile.is_zipfile(backup_path): + with zipfile.ZipFile(backup_path, "r") as zf: + zf.extractall(os.path.dirname(paths["main"])) + else: + shutil.copy2(backup_path, paths["main"]) + + # reopen and retune + self.database.initialize() + self._tune_sqlite_pragmas() + integrity = self.database.provider.integrity_check() + + return { + "restored_from": backup_path, + "integrity_check": integrity, + "health": self.get_database_health_snapshot(), + } + + def _get_identity_bytes(self) -> bytes: + return self.identity.get_private_key() + + def backup_identity(self): + identity_bytes = self._get_identity_bytes() + target_path = self.identity_file_path or os.path.join( + self.storage_dir, "identity", + ) + os.makedirs(os.path.dirname(target_path), exist_ok=True) + with open(target_path, "wb") as f: + f.write(identity_bytes) + return { + "path": target_path, + "size": os.path.getsize(target_path), + } + + def backup_identity_base32(self) -> str: + return base64.b32encode(self._get_identity_bytes()).decode("utf-8") + + def restore_identity_from_bytes(self, identity_bytes: bytes): + target_path = self.identity_file_path or os.path.join( + self.storage_dir, "identity", + ) + os.makedirs(os.path.dirname(target_path), exist_ok=True) + with open(target_path, "wb") as f: + f.write(identity_bytes) + return {"path": target_path, "size": os.path.getsize(target_path)} + + def restore_identity_from_base32(self, base32_value: str): + try: + identity_bytes = base64.b32decode(base32_value, casefold=True) + except Exception as exc: + msg = f"Invalid base32 identity: {exc}" + raise ValueError(msg) from exc + + return self.restore_identity_from_bytes(identity_bytes) + + def _run_startup_auto_recovery(self): + try: + self.database.initialize() + print("Attempting SQLite auto recovery on startup...") + actions = [] + actions.append( + { + "step": "wal_checkpoint", + "result": self.database.provider.checkpoint(), + }, + ) + actions.append( + { + "step": "integrity_check", + "result": self.database.provider.integrity_check(), + }, + ) + self.database.provider.vacuum() + self._tune_sqlite_pragmas() + actions.append( + { + "step": "quick_check_after", + "result": self.database.provider.quick_check(), + }, + ) + print(f"Auto recovery completed: {actions}") + finally: + try: + self.database.close() + except Exception as e: + print(f"Failed to close database during recovery: {e}") + + # gets app version from the synchronized Python version helper + @staticmethod + def get_app_version() -> str: + return app_version + + # automatically announces based on user config + async def announce_loop(self): + while True: + should_announce = False + + # check if auto announce is enabled + if self.config.auto_announce_enabled.get(): + # check if we have announced recently + last_announced_at = self.config.last_announced_at.get() + if last_announced_at is not None: + # determine when next announce should be sent + auto_announce_interval_seconds = ( + self.config.auto_announce_interval_seconds.get() + ) + next_announce_at = ( + last_announced_at + auto_announce_interval_seconds + ) + + # we should announce if current time has passed next announce at timestamp + if time.time() > next_announce_at: + should_announce = True + + else: + # last announced at is null, so we have never announced, lets do it now + should_announce = True + + # announce + if should_announce: + await self.announce() + + # wait 1 second before next loop + await asyncio.sleep(1) + + # automatically syncs propagation nodes based on user config + async def announce_sync_propagation_nodes(self): + while True: + should_sync = False + + # check if auto sync is enabled + auto_sync_interval_seconds = self.config.lxmf_preferred_propagation_node_auto_sync_interval_seconds.get() + if auto_sync_interval_seconds > 0: + # check if we have synced recently + last_synced_at = ( + self.config.lxmf_preferred_propagation_node_last_synced_at.get() + ) + if last_synced_at is not None: + # determine when next sync should happen + next_sync_at = last_synced_at + auto_sync_interval_seconds + + # we should sync if current time has passed next sync at timestamp + if time.time() > next_sync_at: + should_sync = True + + else: + # last synced at is null, so we have never synced, lets do it now + should_sync = True + + # sync + if should_sync: + await self.sync_propagation_nodes() + + # wait 1 second before next loop + await asyncio.sleep(1) + + async def crawler_loop(self): + while True: + try: + if self.config.crawler_enabled.get(): + # Proactively queue any known nodes from the database that haven't been queued yet + # get known propagation nodes from database + known_nodes = self.database.announces.get_announces(aspect="nomadnetwork.node") + for node in known_nodes: + self.queue_crawler_task(node["destination_hash"], "/page/index.mu") + + # process pending or failed tasks + # ensure we handle potential string comparison issues in SQLite + tasks = self.database.misc.get_pending_or_failed_crawl_tasks( + max_retries=self.config.crawler_max_retries.get(), + max_concurrent=self.config.crawler_max_concurrent.get(), + ) + + # process tasks concurrently up to the limit + await asyncio.gather(*[self.process_crawler_task(task) for task in tasks]) + + except Exception as e: + print(f"Error in crawler loop: {e}") + + # wait 30 seconds before checking again + await asyncio.sleep(30) + + async def process_crawler_task(self, task): + # mark as crawling + task_id = task["id"] + self.database.misc.update_crawl_task(task_id, status="crawling", last_retry_at=datetime.now(UTC)) + + destination_hash = task["destination_hash"] + page_path = task["page_path"] + + print(f"Crawler: Archiving {destination_hash}:{page_path} (Attempt {task['retry_count'] + 1})") + + # completion event + done_event = asyncio.Event() + success = [False] + content_received = [None] + failure_reason = ["timeout"] + + def on_success(content): + success[0] = True + content_received[0] = content + done_event.set() + + def on_failure(reason): + failure_reason[0] = reason + done_event.set() + + def on_progress(progress): + pass + + # start downloader + downloader = NomadnetPageDownloader( + destination_hash=bytes.fromhex(destination_hash), + page_path=page_path, + data=None, + on_page_download_success=on_success, + on_page_download_failure=on_failure, + on_progress_update=on_progress, + timeout=120, + ) + + try: + # use a dedicated task for the download so we can wait for it + download_task = asyncio.create_task(downloader.download()) + + # wait for completion event + try: + await asyncio.wait_for(done_event.wait(), timeout=180) + except TimeoutError: + failure_reason[0] = "timeout" + downloader.cancel() + + await download_task + except Exception as e: + print(f"Crawler: Error during download for {destination_hash}:{page_path}: {e}") + failure_reason[0] = str(e) + done_event.set() + + if success[0]: + print(f"Crawler: Successfully archived {destination_hash}:{page_path}") + self.archive_page(destination_hash, page_path, content_received[0], is_manual=False) + task.status = "completed" + task.save() + else: + print(f"Crawler: Failed to archive {destination_hash}:{page_path} - {failure_reason[0]}") + task.retry_count += 1 + task.status = "failed" + + # calculate next retry time + retry_delay = self.config.crawler_retry_delay_seconds.get() + # simple backoff + backoff_delay = retry_delay * (2 ** (task.retry_count - 1)) + task.next_retry_at = datetime.now(UTC) + timedelta(seconds=backoff_delay) + task.save() + + # uses the provided destination hash as the active propagation node + def set_active_propagation_node(self, destination_hash: str | None): + # set outbound propagation node + if destination_hash is not None and destination_hash != "": + try: + self.message_router.set_outbound_propagation_node( + bytes.fromhex(destination_hash), + ) + except Exception: + # failed to set propagation node, clear it to ensure we don't use an old one by mistake + self.remove_active_propagation_node() + + # stop using propagation node + else: + self.remove_active_propagation_node() + + # stops the in progress propagation node sync + def stop_propagation_node_sync(self): + self.message_router.cancel_propagation_node_requests() + + # stops and removes the active propagation node + def remove_active_propagation_node(self): + # fixme: it's possible for internal transfer state to get stuck if we change propagation node during a sync + # this still happens even if we cancel the propagation node requests + # for now, the user can just manually cancel syncing in the ui if they think it's stuck... + self.stop_propagation_node_sync() + self.message_router.outbound_propagation_node = None + + # enables or disables the local lxmf propagation node + def enable_local_propagation_node(self, enabled: bool = True): + try: + if enabled: + self.message_router.enable_propagation() + else: + self.message_router.disable_propagation() + except Exception: + print("failed to enable or disable propagation node") + + def _get_reticulum_section(self): + try: + reticulum_config = self.reticulum.config["reticulum"] + except Exception: + reticulum_config = None + + if not isinstance(reticulum_config, dict): + reticulum_config = {} + self.reticulum.config["reticulum"] = reticulum_config + + return reticulum_config + + def _get_interfaces_section(self): + try: + interfaces = self.reticulum.config["interfaces"] + except Exception: + interfaces = None + + if not isinstance(interfaces, dict): + interfaces = {} + self.reticulum.config["interfaces"] = interfaces + + return interfaces + + def _get_interfaces_snapshot(self): + snapshot = {} + interfaces = self._get_interfaces_section() + for name, interface in interfaces.items(): + try: + snapshot[name] = copy.deepcopy(dict(interface)) + except Exception: + try: + snapshot[name] = copy.deepcopy(interface) + except Exception: + snapshot[name] = {} + return snapshot + + def _write_reticulum_config(self): + try: + self.reticulum.config.write() + return True + except Exception as e: + print(f"Failed to write Reticulum config: {e}") + return False + + def build_user_guidance_messages(self): + guidance = [] + + interfaces = self._get_interfaces_section() + if len(interfaces) == 0: + guidance.append( + { + "id": "no_interfaces", + "title": "No Reticulum interfaces configured", + "description": "Add at least one Reticulum interface so MeshChat can talk to your radio or transport.", + "action_route": "/interfaces/add", + "action_label": "Add Interface", + "severity": "warning", + }, + ) + + if not self.reticulum.transport_enabled(): + guidance.append( + { + "id": "transport_disabled", + "title": "Transport mode is disabled", + "description": "Enable transport to allow MeshChat to relay traffic over your configured interfaces.", + "action_route": "/settings", + "action_label": "Open Settings", + "severity": "info", + }, + ) + + if not self.config.auto_announce_enabled.get(): + guidance.append( + { + "id": "announce_disabled", + "title": "Auto announcements are turned off", + "description": "Automatic announces make it easier for other peers to discover you. Enable them if you want to stay visible.", + "action_route": "/settings", + "action_label": "Manage Announce Settings", + "severity": "info", + }, + ) + + return guidance + + # returns the latest message for the provided destination hash + def get_conversation_latest_message(self, destination_hash: str): + local_hash = self.identity.hexhash + messages = self.message_handler.get_conversation_messages(local_hash, destination_hash, limit=1) + return messages[0] if messages else None + + # returns true if the conversation with the provided destination hash has any attachments + def conversation_has_attachments(self, destination_hash: str): + local_hash = self.identity.hexhash + messages = self.message_handler.get_conversation_messages(local_hash, destination_hash) + for message in messages: + if self.message_fields_have_attachments(message["fields"]): + return True + return False + + @staticmethod + def message_fields_have_attachments(fields_json: str | None): + if not fields_json: + return False + try: + fields = json.loads(fields_json) + except Exception: + return False + if "image" in fields or "audio" in fields: + return True + if "file_attachments" in fields and isinstance( + fields["file_attachments"], + list, + ): + return len(fields["file_attachments"]) > 0 + return False + + def search_destination_hashes_by_message(self, search_term: str): + if search_term is None or search_term.strip() == "": + return set() + + local_hash = self.local_lxmf_destination.hexhash + search_term = search_term.strip() + matches = set() + + query_results = self.message_handler.search_messages(local_hash, search_term) + + for message in query_results: + if message["source_hash"] == local_hash: + matches.add(message["destination_hash"]) + else: + matches.add(message["source_hash"]) + + # also check custom display names + custom_names = self.database.announces.get_announces() # Or more specific if needed + for announce in custom_names: + custom_name = self.database.announces.get_custom_display_name(announce["destination_hash"]) + if custom_name and search_term.lower() in custom_name.lower(): + matches.add(announce["destination_hash"]) + + return matches + + @staticmethod + def parse_bool_query_param(value: str | None) -> bool: + if value is None: + return False + value = value.lower() + return value in {"1", "true", "yes", "on"} + + # handle receiving a new audio call + def on_incoming_telephone_call(self, caller_identity: RNS.Identity): + print(f"on_incoming_telephone_call: {caller_identity.hash.hex()}") + AsyncUtils.run_async( + self.websocket_broadcast( + json.dumps( + { + "type": "telephone_ringing", + }, + ), + ), + ) + + def on_telephone_call_established(self, caller_identity: RNS.Identity): + print(f"on_telephone_call_established: {caller_identity.hash.hex()}") + AsyncUtils.run_async( + self.websocket_broadcast( + json.dumps( + { + "type": "telephone_call_established", + }, + ), + ), + ) + + def on_telephone_call_ended(self, caller_identity: RNS.Identity): + print(f"on_telephone_call_ended: {caller_identity.hash.hex() if caller_identity else 'Unknown'}") + + # Record call history + if caller_identity: + remote_identity_hash = caller_identity.hash.hex() + remote_identity_name = self.get_name_for_identity_hash(remote_identity_hash) + + is_incoming = self.telephone_manager.call_is_incoming + status_code = self.telephone_manager.call_status_at_end + + status_map = { + 0: "Busy", + 1: "Rejected", + 2: "Calling", + 3: "Available", + 4: "Ringing", + 5: "Connecting", + 6: "Completed", + } + status_text = status_map.get(status_code, f"Status {status_code}") + + duration = 0 + if self.telephone_manager.call_start_time: + duration = int(time.time() - self.telephone_manager.call_start_time) + + self.database.telephone.add_call_history( + remote_identity_hash=remote_identity_hash, + remote_identity_name=remote_identity_name, + is_incoming=is_incoming, + status=status_text, + duration_seconds=duration, + timestamp=time.time(), + ) + + AsyncUtils.run_async( + self.websocket_broadcast( + json.dumps( + { + "type": "telephone_call_ended", + }, + ), + ), + ) + + # web server has shutdown, likely ctrl+c, but if we don't do the following, the script never exits + async def shutdown(self, app): + # force close websocket clients + for websocket_client in self.websocket_clients: + await websocket_client.close(code=WSCloseCode.GOING_AWAY) + + # stop reticulum + RNS.Transport.detach_interfaces() + self.reticulum.exit_handler() + RNS.exit() + + def run(self, host, port, launch_browser: bool, enable_https: bool = True): + # create route table + routes = web.RouteTableDef() + + ssl_context = None + use_https = enable_https + if enable_https: + cert_dir = os.path.join(self.storage_path, "ssl") + cert_path = os.path.join(cert_dir, "cert.pem") + key_path = os.path.join(cert_dir, "key.pem") + + try: + generate_ssl_certificate(cert_path, key_path) + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.load_cert_chain(cert_path, key_path) + print(f"HTTPS enabled with certificate at {cert_path}") + except Exception as e: + print(f"Failed to generate SSL certificate: {e}") + print("Falling back to HTTP") + use_https = False + + # session secret for encrypted cookies (generate once and store in config) + session_secret_key = self.config.auth_session_secret.get() + if not session_secret_key: + session_secret_key = secrets.token_urlsafe(32) + self.config.auth_session_secret.set(session_secret_key) + + # authentication middleware + @web.middleware + async def auth_middleware(request, handler): + if not self.auth_enabled: + return await handler(request) + + path = request.path + + # allow access to auth endpoints and setup page + public_paths = [ + "/api/v1/auth/setup", + "/api/v1/auth/login", + "/api/v1/auth/status", + "/api/v1/auth/logout", + "/manifest.json", + "/service-worker.js", + ] + + # check if path is public + is_public = any(path.startswith(public) for public in public_paths) + + # check if requesting setup page (index.html will show setup if needed) + if ( + path == "/" + or path.startswith("/assets/") + or path.startswith("/favicons/") + ): + is_public = True + + if is_public: + return await handler(request) + + # check authentication + session = await get_session(request) + if not session.get("authenticated", False): + if path.startswith("/api/"): + return web.json_response( + {"error": "Authentication required"}, + status=401, + ) + return web.Response( + text="Authentication required", + status=401, + headers={"Content-Type": "text/html"}, + ) + + return await handler(request) + + # serve index.html + @routes.get("/") + async def index(request): + return web.FileResponse( + path=get_file_path("public/index.html"), + headers={ + # don't allow browser to store page in cache, otherwise new app versions may get stale ui + "Cache-Control": "no-cache, no-store", + }, + ) + + # serve ping + @routes.get("/api/v1/status") + async def status(request): + return web.json_response( + { + "status": "ok", + }, + ) + + # auth status + @routes.get("/api/v1/auth/status") + async def auth_status(request): + session = await get_session(request) + return web.json_response( + { + "auth_enabled": self.auth_enabled, + "password_set": self.config.auth_password_hash.get() is not None, + "authenticated": session.get("authenticated", False), + }, + ) + + # auth setup + @routes.post("/api/v1/auth/setup") + async def auth_setup(request): + # check if password already set + if self.config.auth_password_hash.get() is not None: + return web.json_response( + {"error": "Initial setup already completed"}, + status=403, + ) + + data = await request.json() + password = data.get("password") + + if not password or len(password) < 8: + return web.json_response( + {"error": "Password must be at least 8 characters long"}, + status=400, + ) + + # hash password + password_hash = bcrypt.hashpw( + password.encode("utf-8"), + bcrypt.gensalt(), + ).decode("utf-8") + + # save to config + self.config.auth_password_hash.set(password_hash) + + # set authenticated in session + session = await get_session(request) + session["authenticated"] = True + + return web.json_response({"message": "Setup completed successfully"}) + + # auth login + @routes.post("/api/v1/auth/login") + async def auth_login(request): + data = await request.json() + password = data.get("password") + + password_hash = self.config.auth_password_hash.get() + if password_hash is None: + return web.json_response( + {"error": "Auth not setup"}, + status=403, + ) + + if not password: + return web.json_response( + {"error": "Password required"}, + status=400, + ) + + # verify password + if bcrypt.checkpw( + password.encode("utf-8"), + password_hash.encode("utf-8"), + ): + # set authenticated in session + session = await get_session(request) + session["authenticated"] = True + return web.json_response({"message": "Login successful"}) + + return web.json_response( + {"error": "Invalid password"}, + status=401, + ) + + # auth logout + @routes.post("/api/v1/auth/logout") + async def auth_logout(request): + session = await get_session(request) + session["authenticated"] = False + return web.json_response({"message": "Logged out successfully"}) + + # fetch com ports + @routes.get("/api/v1/comports") + async def comports(request): + comports = [ + { + "device": comport.device, + "product": comport.product, + "serial_number": comport.serial_number, + } + for comport in list_ports.comports() + ] + + return web.json_response( + { + "comports": comports, + }, + ) + + # fetch reticulum interfaces + @routes.get("/api/v1/reticulum/interfaces") + async def reticulum_interfaces(request): + interfaces = self._get_interfaces_snapshot() + + processed_interfaces = {} + for interface_name, interface in interfaces.items(): + interface_data = copy.deepcopy(interface) + + # handle sub-interfaces for RNodeMultiInterface + if interface_data.get("type") == "RNodeMultiInterface": + sub_interfaces = [] + for sub_name, sub_config in interface_data.items(): + if sub_name not in { + "type", + "port", + "interface_enabled", + "selected_interface_mode", + "configured_bitrate", + }: + if isinstance(sub_config, dict): + sub_config["name"] = sub_name + sub_interfaces.append(sub_config) + + # add sub-interfaces to the main interface data + interface_data["sub_interfaces"] = sub_interfaces + + for sub in sub_interfaces: + del interface_data[sub["name"]] + + processed_interfaces[interface_name] = interface_data + + return web.json_response( + { + "interfaces": processed_interfaces, + }, + ) + + # enable reticulum interface + @routes.post("/api/v1/reticulum/interfaces/enable") + async def reticulum_interfaces_enable(request): + # get request data + data = await request.json() + interface_name = data.get("name") + + if interface_name is None or interface_name == "": + return web.json_response( + { + "message": "Interface name is required", + }, + status=422, + ) + + # enable interface + interfaces = self._get_interfaces_section() + if interface_name not in interfaces: + return web.json_response( + { + "message": "Interface not found", + }, + status=404, + ) + interface = interfaces[interface_name] + if "enabled" in interface: + interface["enabled"] = "true" + if "interface_enabled" in interface: + interface["interface_enabled"] = "true" + + keys_to_remove = [] + for key, value in interface.items(): + if value is None: + keys_to_remove.append(key) + for key in keys_to_remove: + del interface[key] + + # save config + if not self._write_reticulum_config(): + return web.json_response( + { + "message": "Failed to write Reticulum config", + }, + status=500, + ) + + return web.json_response( + { + "message": "Interface is now enabled", + }, + ) + + # disable reticulum interface + @routes.post("/api/v1/reticulum/interfaces/disable") + async def reticulum_interfaces_disable(request): + # get request data + data = await request.json() + interface_name = data.get("name") + + if interface_name is None or interface_name == "": + return web.json_response( + { + "message": "Interface name is required", + }, + status=422, + ) + + # disable interface + interfaces = self._get_interfaces_section() + if interface_name not in interfaces: + return web.json_response( + { + "message": "Interface not found", + }, + status=404, + ) + interface = interfaces[interface_name] + if "enabled" in interface: + interface["enabled"] = "false" + if "interface_enabled" in interface: + interface["interface_enabled"] = "false" + + keys_to_remove = [] + for key, value in interface.items(): + if value is None: + keys_to_remove.append(key) + for key in keys_to_remove: + del interface[key] + + # save config + if not self._write_reticulum_config(): + return web.json_response( + { + "message": "Failed to write Reticulum config", + }, + status=500, + ) + + return web.json_response( + { + "message": "Interface is now disabled", + }, + ) + + # delete reticulum interface + @routes.post("/api/v1/reticulum/interfaces/delete") + async def reticulum_interfaces_delete(request): + # get request data + data = await request.json() + interface_name = data.get("name") + + if interface_name is None or interface_name == "": + return web.json_response( + { + "message": "Interface name is required", + }, + status=422, + ) + + interfaces = self._get_interfaces_section() + if interface_name not in interfaces: + return web.json_response( + { + "message": "Interface not found", + }, + status=404, + ) + + # delete interface + del interfaces[interface_name] + + # save config + if not self._write_reticulum_config(): + return web.json_response( + { + "message": "Failed to write Reticulum config", + }, + status=500, + ) + + return web.json_response( + { + "message": "Interface has been deleted", + }, + ) + + # add reticulum interface + @routes.post("/api/v1/reticulum/interfaces/add") + async def reticulum_interfaces_add(request): + # get request data + data = await request.json() + interface_name = data.get("name") + interface_type = data.get("type") + allow_overwriting_interface = data.get("allow_overwriting_interface", False) + + # ensure name is provided + if interface_name is None or interface_name == "": + return web.json_response( + { + "message": "Name is required", + }, + status=422, + ) + + # ensure type name provided + if interface_type is None or interface_type == "": + return web.json_response( + { + "message": "Type is required", + }, + status=422, + ) + + # get existing interfaces + interfaces = self._get_interfaces_section() + + # ensure name is not for an existing interface, to prevent overwriting + if allow_overwriting_interface is False and interface_name in interfaces: + return web.json_response( + { + "message": "Name is already in use by another interface", + }, + status=422, + ) + + # get existing interface details if available + interface_details = {} + if interface_name in interfaces: + interface_details = interfaces[interface_name] + + # update interface details + interface_details["type"] = interface_type + + # if interface doesn't have enabled or interface_enabled setting already, enable it by default + if ( + "enabled" not in interface_details + and "interface_enabled" not in interface_details + ): + interface_details["interface_enabled"] = "true" + + # handle AutoInterface + if interface_type == "AutoInterface": + # set optional AutoInterface options + InterfaceEditor.update_value(interface_details, data, "group_id") + InterfaceEditor.update_value( + interface_details, + data, + "multicast_address_type", + ) + InterfaceEditor.update_value(interface_details, data, "devices") + InterfaceEditor.update_value(interface_details, data, "ignored_devices") + InterfaceEditor.update_value(interface_details, data, "discovery_scope") + InterfaceEditor.update_value(interface_details, data, "discovery_port") + InterfaceEditor.update_value(interface_details, data, "data_port") + + # handle TCPClientInterface + if interface_type == "TCPClientInterface": + # ensure target host provided + interface_target_host = data.get("target_host") + if interface_target_host is None or interface_target_host == "": + return web.json_response( + { + "message": "Target Host is required", + }, + status=422, + ) + + # ensure target port provided + interface_target_port = data.get("target_port") + if interface_target_port is None or interface_target_port == "": + return web.json_response( + { + "message": "Target Port is required", + }, + status=422, + ) + + # set required TCPClientInterface options + interface_details["target_host"] = interface_target_host + interface_details["target_port"] = interface_target_port + + # set optional TCPClientInterface options + InterfaceEditor.update_value(interface_details, data, "kiss_framing") + InterfaceEditor.update_value(interface_details, data, "i2p_tunneled") + + # handle I2P interface + if interface_type == "I2PInterface": + interface_details["connectable"] = "True" + InterfaceEditor.update_value(interface_details, data, "peers") + + # handle tcp server interface + if interface_type == "TCPServerInterface": + # ensure listen ip provided + interface_listen_ip = data.get("listen_ip") + if interface_listen_ip is None or interface_listen_ip == "": + return web.json_response( + { + "message": "Listen IP is required", + }, + status=422, + ) + + # ensure listen port provided + interface_listen_port = data.get("listen_port") + if interface_listen_port is None or interface_listen_port == "": + return web.json_response( + { + "message": "Listen Port is required", + }, + status=422, + ) + + # set required TCPServerInterface options + interface_details["listen_ip"] = interface_listen_ip + interface_details["listen_port"] = interface_listen_port + + # set optional TCPServerInterface options + InterfaceEditor.update_value(interface_details, data, "device") + InterfaceEditor.update_value(interface_details, data, "prefer_ipv6") + + # handle udp interface + if interface_type == "UDPInterface": + # ensure listen ip provided + interface_listen_ip = data.get("listen_ip") + if interface_listen_ip is None or interface_listen_ip == "": + return web.json_response( + { + "message": "Listen IP is required", + }, + status=422, + ) + + # ensure listen port provided + interface_listen_port = data.get("listen_port") + if interface_listen_port is None or interface_listen_port == "": + return web.json_response( + { + "message": "Listen Port is required", + }, + status=422, + ) + + # ensure forward ip provided + interface_forward_ip = data.get("forward_ip") + if interface_forward_ip is None or interface_forward_ip == "": + return web.json_response( + { + "message": "Forward IP is required", + }, + status=422, + ) + + # ensure forward port provided + interface_forward_port = data.get("forward_port") + if interface_forward_port is None or interface_forward_port == "": + return web.json_response( + { + "message": "Forward Port is required", + }, + status=422, + ) + + # set required UDPInterface options + interface_details["listen_ip"] = interface_listen_ip + interface_details["listen_port"] = interface_listen_port + interface_details["forward_ip"] = interface_forward_ip + interface_details["forward_port"] = interface_forward_port + + # set optional UDPInterface options + InterfaceEditor.update_value(interface_details, data, "device") + + # handle RNodeInterface + if interface_type == "RNodeInterface": + # ensure port provided + interface_port = data.get("port") + if interface_port is None or interface_port == "": + return web.json_response( + { + "message": "Port is required", + }, + status=422, + ) + + # ensure frequency provided + interface_frequency = data.get("frequency") + if interface_frequency is None or interface_frequency == "": + return web.json_response( + { + "message": "Frequency is required", + }, + status=422, + ) + + # ensure bandwidth provided + interface_bandwidth = data.get("bandwidth") + if interface_bandwidth is None or interface_bandwidth == "": + return web.json_response( + { + "message": "Bandwidth is required", + }, + status=422, + ) + + # ensure txpower provided + interface_txpower = data.get("txpower") + if interface_txpower is None or interface_txpower == "": + return web.json_response( + { + "message": "TX power is required", + }, + status=422, + ) + + # ensure spreading factor provided + interface_spreadingfactor = data.get("spreadingfactor") + if interface_spreadingfactor is None or interface_spreadingfactor == "": + return web.json_response( + { + "message": "Spreading Factor is required", + }, + status=422, + ) + + # ensure coding rate provided + interface_codingrate = data.get("codingrate") + if interface_codingrate is None or interface_codingrate == "": + return web.json_response( + { + "message": "Coding Rate is required", + }, + status=422, + ) + + # set required RNodeInterface options + interface_details["port"] = interface_port + interface_details["frequency"] = interface_frequency + interface_details["bandwidth"] = interface_bandwidth + interface_details["txpower"] = interface_txpower + interface_details["spreadingfactor"] = interface_spreadingfactor + interface_details["codingrate"] = interface_codingrate + + # set optional RNodeInterface options + InterfaceEditor.update_value(interface_details, data, "callsign") + InterfaceEditor.update_value(interface_details, data, "id_interval") + InterfaceEditor.update_value( + interface_details, + data, + "airtime_limit_long", + ) + InterfaceEditor.update_value( + interface_details, + data, + "airtime_limit_short", + ) + + # handle RNodeMultiInterface + if interface_type == "RNodeMultiInterface": + # required settings + interface_port = data.get("port") + sub_interfaces = data.get("sub_interfaces", []) + + # ensure port provided + if interface_port is None or interface_port == "": + return web.json_response( + { + "message": "Port is required", + }, + status=422, + ) + + # ensure sub interfaces provided + if not isinstance(sub_interfaces, list) or not sub_interfaces: + return web.json_response( + { + "message": "At least one sub-interface is required", + }, + status=422, + ) + + # set required RNodeMultiInterface options + interface_details["port"] = interface_port + + # remove any existing sub interfaces, which can be found by finding keys that contain a dict value + # this allows us to replace all sub interfaces with the ones we are about to add, while also ensuring + # that we do not remove any existing config values from the main interface config + for key in list(interface_details.keys()): + value = interface_details[key] + if isinstance(value, dict): + del interface_details[key] + + # process each provided sub interface + required_subinterface_fields = [ + "name", + "frequency", + "bandwidth", + "txpower", + "spreadingfactor", + "codingrate", + "vport", + ] + for idx, sub_interface in enumerate(sub_interfaces): + # ensure required fields for sub-interface provided + missing_fields = [ + field + for field in required_subinterface_fields + if ( + field not in sub_interface + or sub_interface.get(field) is None + or sub_interface.get(field) == "" + ) + ] + if missing_fields: + return web.json_response( + { + "message": f"Sub-interface {idx + 1} is missing required field(s): {', '.join(missing_fields)}", + }, + status=422, + ) + + sub_interface_name = sub_interface.get("name") + interface_details[sub_interface_name] = { + "interface_enabled": "true", + "frequency": int(sub_interface["frequency"]), + "bandwidth": int(sub_interface["bandwidth"]), + "txpower": int(sub_interface["txpower"]), + "spreadingfactor": int(sub_interface["spreadingfactor"]), + "codingrate": int(sub_interface["codingrate"]), + "vport": int(sub_interface["vport"]), + } + + interfaces[interface_name] = interface_details + + # handle SerialInterface, KISSInterface, and AX25KISSInterface + if interface_type in ( + "SerialInterface", + "KISSInterface", + "AX25KISSInterface", + ): + # ensure port provided + interface_port = data.get("port") + if interface_port is None or interface_port == "": + return web.json_response( + { + "message": "Port is required", + }, + status=422, + ) + + # set required options + interface_details["port"] = interface_port + + # set optional options + InterfaceEditor.update_value(interface_details, data, "speed") + InterfaceEditor.update_value(interface_details, data, "databits") + InterfaceEditor.update_value(interface_details, data, "parity") + InterfaceEditor.update_value(interface_details, data, "stopbits") + + # Handle KISS and AX25KISS specific options + if interface_type in ("KISSInterface", "AX25KISSInterface"): + # set optional options + InterfaceEditor.update_value(interface_details, data, "preamble") + InterfaceEditor.update_value(interface_details, data, "txtail") + InterfaceEditor.update_value(interface_details, data, "persistence") + InterfaceEditor.update_value(interface_details, data, "slottime") + InterfaceEditor.update_value(interface_details, data, "callsign") + InterfaceEditor.update_value(interface_details, data, "ssid") + + # FIXME: move to own sections + # RNode Airtime limits and station ID + InterfaceEditor.update_value(interface_details, data, "callsign") + InterfaceEditor.update_value(interface_details, data, "id_interval") + InterfaceEditor.update_value(interface_details, data, "airtime_limit_long") + InterfaceEditor.update_value(interface_details, data, "airtime_limit_short") + + # handle Pipe Interface + if interface_type == "PipeInterface": + # ensure command provided + interface_command = data.get("command") + if interface_command is None or interface_command == "": + return web.json_response( + { + "message": "Command is required", + }, + status=422, + ) + + # ensure command provided + interface_respawn_delay = data.get("respawn_delay") + if interface_respawn_delay is None or interface_respawn_delay == "": + return web.json_response( + { + "message": "Respawn delay is required", + }, + status=422, + ) + + # set required options + interface_details["command"] = interface_command + interface_details["respawn_delay"] = interface_respawn_delay + + # set common interface options + InterfaceEditor.update_value(interface_details, data, "bitrate") + InterfaceEditor.update_value(interface_details, data, "mode") + InterfaceEditor.update_value(interface_details, data, "network_name") + InterfaceEditor.update_value(interface_details, data, "passphrase") + InterfaceEditor.update_value(interface_details, data, "ifac_size") + + # merge new interface into existing interfaces + interfaces[interface_name] = interface_details + # save config + if not self._write_reticulum_config(): + return web.json_response( + { + "message": "Failed to write Reticulum config", + }, + status=500, + ) + + if allow_overwriting_interface: + return web.json_response( + { + "message": "Interface has been saved. Please restart MeshChat for these changes to take effect.", + }, + ) + return web.json_response( + { + "message": "Interface has been added. Please restart MeshChat for these changes to take effect.", + }, + ) + + # export interfaces + @routes.post("/api/v1/reticulum/interfaces/export") + async def export_interfaces(request): + try: + # get request data + selected_interface_names = None + try: + data = await request.json() + selected_interface_names = data.get("selected_interface_names") + except Exception as e: + # request data was not json, but we don't care + print(f"Request data was not JSON: {e}") + + # format interfaces for export + output = [] + interfaces = self._get_interfaces_snapshot() + for interface_name, interface in interfaces.items(): + # skip interface if not selected + if ( + selected_interface_names is not None + and selected_interface_names != "" + ): + if interface_name not in selected_interface_names: + continue + + # add interface to output + output.append(f"[[{interface_name}]]") + for key, value in interface.items(): + if not isinstance(value, dict): + output.append(f" {key} = {value}") + output.append("") + + # Handle sub-interfaces for RNodeMultiInterface + if interface.get("type") == "RNodeMultiInterface": + for sub_name, sub_config in interface.items(): + if sub_name in {"type", "port", "interface_enabled"}: + continue + if isinstance(sub_config, dict): + output.append(f" [[[{sub_name}]]]") + for sub_key, sub_value in sub_config.items(): + output.append(f" {sub_key} = {sub_value}") + output.append("") + + return web.Response( + text="\n".join(output), + content_type="text/plain", + headers={ + "Content-Disposition": "attachment; filename=meshchat_interfaces", + }, + ) + + except Exception as e: + return web.json_response( + { + "message": f"Failed to export interfaces: {e!s}", + }, + status=500, + ) + + # preview importable interfaces + @routes.post("/api/v1/reticulum/interfaces/import-preview") + async def import_interfaces_preview(request): + try: + # get request data + data = await request.json() + config = data.get("config") + + # parse interfaces from config + interfaces = InterfaceConfigParser.parse(config) + + return web.json_response( + { + "interfaces": interfaces, + }, + ) + + except Exception as e: + return web.json_response( + { + "message": f"Failed to parse config file: {e!s}", + }, + status=500, + ) + + # import interfaces from config + @routes.post("/api/v1/reticulum/interfaces/import") + async def import_interfaces(request): + try: + # get request data + data = await request.json() + config = data.get("config") + selected_interface_names = data.get("selected_interface_names") + + # parse interfaces from config + interfaces = InterfaceConfigParser.parse(config) + + # find selected interfaces + selected_interfaces = [ + interface + for interface in interfaces + if interface["name"] in selected_interface_names + ] + + # convert interfaces to object + interface_config = {} + for interface in selected_interfaces: + # add interface and keys/values + interface_name = interface["name"] + interface_config[interface_name] = {} + for key, value in interface.items(): + interface_config[interface_name][key] = value + + # unset name which isn't part of the config + del interface_config[interface_name]["name"] + + # force imported interface to be enabled by default + interface_config[interface_name]["interface_enabled"] = "true" + + # remove enabled config value in favour of interface_enabled + if "enabled" in interface_config[interface_name]: + del interface_config[interface_name]["enabled"] + + # update reticulum config with new interfaces + interfaces = self._get_interfaces_section() + interfaces.update(interface_config) + if not self._write_reticulum_config(): + return web.json_response( + { + "message": "Failed to write Reticulum config", + }, + status=500, + ) + + return web.json_response( + { + "message": "Interfaces imported successfully", + }, + ) + + except Exception as e: + return web.json_response( + { + "message": f"Failed to import interfaces: {e!s}", + }, + status=500, + ) + + # handle websocket clients + @routes.get("/ws") + async def ws(request): + # prepare websocket response + websocket_response = web.WebSocketResponse( + # set max message size accepted by server to 50 megabytes + max_msg_size=50 * 1024 * 1024, + ) + await websocket_response.prepare(request) + + # add client to connected clients list + self.websocket_clients.append(websocket_response) + + # send config to all clients + await self.send_config_to_websocket_clients() + + # handle websocket messages until disconnected + async for msg in websocket_response: + msg: WSMessage = msg + if msg.type == WSMsgType.TEXT: + try: + data = json.loads(msg.data) + await self.on_websocket_data_received(websocket_response, data) + except Exception as e: + # ignore errors while handling message + print("failed to process client message") + print(e) + elif msg.type == WSMsgType.ERROR: + # ignore errors while handling message + print(f"ws connection error {websocket_response.exception()}") + + # websocket closed + self.websocket_clients.remove(websocket_response) + + return websocket_response + + # get app info + @routes.get("/api/v1/app/info") + async def app_info(request): + # Get memory usage for current process + process = psutil.Process() + memory_info = process.memory_info() + + # Get network I/O statistics + net_io = psutil.net_io_counters() + + # Get total paths + path_table = self.reticulum.get_path_table() + total_paths = len(path_table) + + # Calculate announce rates + current_time = time.time() + announces_per_second = len( + [t for t in self.announce_timestamps if current_time - t <= 1.0], + ) + announces_per_minute = len( + [t for t in self.announce_timestamps if current_time - t <= 60.0], + ) + announces_per_hour = len( + [t for t in self.announce_timestamps if current_time - t <= 3600.0], + ) + + # Clean up old announce timestamps (older than 1 hour) + self.announce_timestamps = [ + t for t in self.announce_timestamps if current_time - t <= 3600.0 + ] + + # Calculate average download speed + avg_download_speed_bps = None + if self.download_speeds: + total_bytes = sum(size for size, _ in self.download_speeds) + total_duration = sum(duration for _, duration in self.download_speeds) + if total_duration > 0: + avg_download_speed_bps = total_bytes / total_duration + + db_files = self._get_database_file_stats() + + return web.json_response( + { + "app_info": { + "version": self.get_app_version(), + "lxmf_version": LXMF.__version__, + "rns_version": RNS.__version__, + "python_version": platform.python_version(), + "storage_path": self.storage_path, + "database_path": self.database_path, + "database_file_size": db_files["main_bytes"], + "database_files": db_files, + "sqlite": { + "journal_mode": self._get_pragma_value( + "journal_mode", + "unknown", + ), + "synchronous": self._get_pragma_value( + "synchronous", + None, + ), + "wal_autocheckpoint": self._get_pragma_value( + "wal_autocheckpoint", + None, + ), + "busy_timeout": self._get_pragma_value( + "busy_timeout", + None, + ), + }, + "reticulum_config_path": self.reticulum.configpath, + "is_connected_to_shared_instance": self.reticulum.is_connected_to_shared_instance, + "is_transport_enabled": self.reticulum.transport_enabled(), + "memory_usage": { + "rss": memory_info.rss, # Resident Set Size (bytes) + "vms": memory_info.vms, # Virtual Memory Size (bytes) + }, + "network_stats": { + "bytes_sent": net_io.bytes_sent, + "bytes_recv": net_io.bytes_recv, + "packets_sent": net_io.packets_sent, + "packets_recv": net_io.packets_recv, + }, + "reticulum_stats": { + "total_paths": total_paths, + "announces_per_second": announces_per_second, + "announces_per_minute": announces_per_minute, + "announces_per_hour": announces_per_hour, + }, + "download_stats": { + "avg_download_speed_bps": avg_download_speed_bps, + }, + "user_guidance": self.build_user_guidance_messages(), + }, + }, + ) + + @routes.get("/api/v1/database/health") + async def database_health(request): + try: + return web.json_response( + { + "database": self.get_database_health_snapshot(), + }, + ) + except Exception as e: + return web.json_response( + { + "message": f"Failed to fetch database health: {e!s}", + }, + status=500, + ) + + @routes.post("/api/v1/database/vacuum") + async def database_vacuum(request): + try: + result = self.run_database_vacuum() + return web.json_response( + { + "message": "Database vacuum completed", + "database": result, + }, + ) + except Exception as e: + return web.json_response( + { + "message": f"Failed to vacuum database: {e!s}", + }, + status=500, + ) + + @routes.post("/api/v1/database/recover") + async def database_recover(request): + try: + result = self.run_database_recovery() + return web.json_response( + { + "message": "Database recovery routine completed", + "database": result, + }, + ) + except Exception as e: + return web.json_response( + { + "message": f"Failed to recover database: {e!s}", + }, + status=500, + ) + + @routes.post("/api/v1/database/backup") + async def database_backup(request): + try: + result = self.backup_database() + return web.json_response( + { + "message": "Database backup created", + "backup": result, + }, + ) + except Exception as e: + return web.json_response( + { + "message": f"Failed to create database backup: {e!s}", + }, + status=500, + ) + + @routes.get("/api/v1/database/backup/download") + async def database_backup_download(request): + try: + backup_info = self.backup_database() + file_path = backup_info["path"] + with open(file_path, "rb") as f: + data = f.read() + return web.Response( + body=data, + headers={ + "Content-Type": "application/zip", + "Content-Disposition": f'attachment; filename="{os.path.basename(file_path)}"', + }, + ) + except Exception as e: + return web.json_response( + { + "message": f"Failed to create database backup: {e!s}", + }, + status=500, + ) + + @routes.get("/api/v1/identity/backup/download") + async def identity_backup_download(request): + try: + info = self.backup_identity() + with open(info["path"], "rb") as f: + data = f.read() + return web.Response( + body=data, + headers={ + "Content-Type": "application/octet-stream", + "Content-Disposition": 'attachment; filename="identity"', + }, + ) + except Exception as e: + return web.json_response( + { + "message": f"Failed to create identity backup: {e!s}", + }, + status=500, + ) + + @routes.get("/api/v1/identity/backup/base32") + async def identity_backup_base32(request): + try: + return web.json_response( + { + "identity_base32": self.backup_identity_base32(), + }, + ) + except Exception as e: + return web.json_response( + { + "message": f"Failed to export identity: {e!s}", + }, + status=500, + ) + + @routes.post("/api/v1/database/restore") + async def database_restore(request): + try: + reader = await request.multipart() + field = await reader.next() + if field is None or field.name != "file": + return web.json_response( + { + "message": "Restore file is required", + }, + status=400, + ) + + with tempfile.NamedTemporaryFile(delete=False) as tmp: + while True: + chunk = await field.read_chunk() + if not chunk: + break + tmp.write(chunk) + temp_path = tmp.name + + result = self.restore_database(temp_path) + os.remove(temp_path) + + return web.json_response( + { + "message": "Database restored successfully", + "database": result, + }, + ) + except Exception as e: + return web.json_response( + { + "message": f"Failed to restore database: {e!s}", + }, + status=500, + ) + + @routes.post("/api/v1/identity/restore") + async def identity_restore(request): + try: + content_type = request.headers.get("Content-Type", "") + # multipart file upload + if "multipart/form-data" in content_type: + reader = await request.multipart() + field = await reader.next() + if field is None or field.name != "file": + return web.json_response( + {"message": "Identity file is required"}, + status=400, + ) + with tempfile.NamedTemporaryFile(delete=False) as tmp: + while True: + chunk = await field.read_chunk() + if not chunk: + break + tmp.write(chunk) + temp_path = tmp.name + with open(temp_path, "rb") as f: + identity_bytes = f.read() + os.remove(temp_path) + result = self.restore_identity_from_bytes(identity_bytes) + else: + data = await request.json() + base32_value = data.get("base32") + if not base32_value: + return web.json_response( + {"message": "base32 value is required"}, + status=400, + ) + result = self.restore_identity_from_base32(base32_value) + + return web.json_response( + { + "message": "Identity restored. Restart app to use the new identity.", + "identity": result, + }, + ) + except Exception as e: + return web.json_response( + { + "message": f"Failed to restore identity: {e!s}", + }, + status=500, + ) + + # get config + @routes.get("/api/v1/config") + async def config_get(request): + return web.json_response( + { + "config": self.get_config_dict(), + }, + ) + + # update config + @routes.patch("/api/v1/config") + async def config_update(request): + # get request body as json + data = await request.json() + + # update config + await self.update_config(data) + + return web.json_response( + { + "config": self.get_config_dict(), + }, + ) + + # enable transport mode + @routes.post("/api/v1/reticulum/enable-transport") + async def reticulum_enable_transport(request): + # enable transport mode + reticulum_config = self._get_reticulum_section() + reticulum_config["enable_transport"] = True + if not self._write_reticulum_config(): + return web.json_response( + { + "message": "Failed to write Reticulum config", + }, + status=500, + ) + + return web.json_response( + { + "message": "Transport has been enabled. MeshChat must be restarted for this change to take effect.", + }, + ) + + # disable transport mode + @routes.post("/api/v1/reticulum/disable-transport") + async def reticulum_disable_transport(request): + # disable transport mode + reticulum_config = self._get_reticulum_section() + reticulum_config["enable_transport"] = False + if not self._write_reticulum_config(): + return web.json_response( + { + "message": "Failed to write Reticulum config", + }, + status=500, + ) + + return web.json_response( + { + "message": "Transport has been disabled. MeshChat must be restarted for this change to take effect.", + }, + ) + + # serve telephone status + @routes.get("/api/v1/telephone/status") + async def telephone_status(request): + # make sure telephone is enabled + if self.telephone_manager.telephone is None: + return web.json_response( + { + "enabled": False, + "message": "Telephone is disabled", + }, + ) + + # get active call info + active_call = None + telephone_active_call = self.telephone_manager.telephone.active_call + if telephone_active_call is not None: + # get remote identity hash + remote_identity_hash = None + remote_identity_name = None + remote_identity = telephone_active_call.get_remote_identity() + if remote_identity is not None: + remote_identity_hash = remote_identity.hash.hex() + remote_identity_name = self.get_name_for_identity_hash( + remote_identity_hash, + ) + + active_call = { + "hash": telephone_active_call.hash.hex(), + "status": self.telephone_manager.telephone.call_status, + "is_incoming": telephone_active_call.is_incoming, + "is_outgoing": telephone_active_call.is_outgoing, + "remote_identity_hash": remote_identity_hash, + "remote_identity_name": remote_identity_name, + "audio_profile_id": self.telephone_manager.telephone.transmit_codec.profile + if hasattr(self.telephone_manager.telephone.transmit_codec, "profile") + else None, + "tx_packets": getattr(telephone_active_call, "tx", 0), + "rx_packets": getattr(telephone_active_call, "rx", 0), + "tx_bytes": getattr(telephone_active_call, "txbytes", 0), + "rx_bytes": getattr(telephone_active_call, "rxbytes", 0), + "is_mic_muted": self.telephone_manager.telephone.transmit_muted, + "is_speaker_muted": self.telephone_manager.telephone.receive_muted, + } + + return web.json_response( + { + "enabled": True, + "is_busy": self.telephone_manager.telephone.busy, + "call_status": self.telephone_manager.telephone.call_status, + "active_call": active_call, + "is_mic_muted": self.telephone_manager.telephone.transmit_muted, + "is_speaker_muted": self.telephone_manager.telephone.receive_muted, + }, + ) + + # answer incoming telephone call + @routes.get("/api/v1/telephone/answer") + async def telephone_answer(request): + # get incoming caller identity + active_call = self.telephone_manager.telephone.active_call + if not active_call: + return web.json_response({"message": "No active call"}, status=404) + + caller_identity = active_call.get_remote_identity() + + # answer call + await asyncio.to_thread(self.telephone_manager.telephone.answer, caller_identity) + + return web.json_response( + { + "message": "Answering call...", + }, + ) + + # hangup active telephone call + @routes.get("/api/v1/telephone/hangup") + async def telephone_hangup(request): + await asyncio.to_thread(self.telephone_manager.telephone.hangup) + return web.json_response( + { + "message": "Hanging up call...", + }, + ) + + # mute/unmute transmit + @routes.get("/api/v1/telephone/mute-transmit") + async def telephone_mute_transmit(request): + await asyncio.to_thread(self.telephone_manager.telephone.mute_transmit) + return web.json_response({"message": "Microphone muted"}) + + @routes.get("/api/v1/telephone/unmute-transmit") + async def telephone_unmute_transmit(request): + await asyncio.to_thread(self.telephone_manager.telephone.unmute_transmit) + return web.json_response({"message": "Microphone unmuted"}) + + # mute/unmute receive + @routes.get("/api/v1/telephone/mute-receive") + async def telephone_mute_receive(request): + await asyncio.to_thread(self.telephone_manager.telephone.mute_receive) + return web.json_response({"message": "Speaker muted"}) + + @routes.get("/api/v1/telephone/unmute-receive") + async def telephone_unmute_receive(request): + await asyncio.to_thread(self.telephone_manager.telephone.unmute_receive) + return web.json_response({"message": "Speaker unmuted"}) + + # get call history + @routes.get("/api/v1/telephone/history") + async def telephone_history(request): + limit = int(request.query.get("limit", 10)) + history = self.database.telephone.get_call_history(limit=limit) + return web.json_response( + { + "call_history": [dict(row) for row in history], + }, + ) + + # switch audio profile + @routes.get("/api/v1/telephone/switch-audio-profile/{profile_id}") + async def telephone_switch_audio_profile(request): + profile_id = request.match_info.get("profile_id") + try: + await asyncio.to_thread( + self.telephone_manager.telephone.switch_profile, int(profile_id) + ) + return web.json_response({"message": f"Switched to profile {profile_id}"}) + except Exception as e: + return web.json_response({"message": str(e)}, status=500) + + # initiate a telephone call + @routes.get("/api/v1/telephone/call/{identity_hash}") + async def telephone_call(request): + # make sure telephone enabled + if self.telephone_manager.telephone is None: + return web.json_response( + { + "message": "Telephone has been disabled.", + }, + status=503, + ) + + # get path params + identity_hash_hex = request.match_info.get("identity_hash", "") + timeout_seconds = int(request.query.get("timeout", 15)) + + # convert hash to bytes + identity_hash_bytes = bytes.fromhex(identity_hash_hex) + + # try to find identity + destination_identity = self.recall_identity(identity_hash_hex) + + # if identity not found, try to resolve it by requesting a path + if destination_identity is None: + # determine identity hash + # if the provided hash is a known destination, get its identity hash + announce = self.database.announces.get_announce_by_hash( + identity_hash_hex, + ) + if announce: + identity_hash_bytes = bytes.fromhex(announce["identity_hash"]) + + # calculate telephony destination hash + telephony_destination_hash = RNS.Destination.hash_from_name_and_identity( + f"{LXST.APP_NAME}.telephony", + identity_hash_bytes, + ) + + # request path to telephony destination + if not RNS.Transport.has_path(telephony_destination_hash): + print( + f"Requesting path to telephony destination: {telephony_destination_hash.hex()}", + ) + RNS.Transport.request_path(telephony_destination_hash) + + # wait for path + timeout_after_seconds = time.time() + timeout_seconds + while ( + not RNS.Transport.has_path(telephony_destination_hash) + and time.time() < timeout_after_seconds + ): + await asyncio.sleep(0.1) + + # try to recall again now that we might have a path/announce + destination_identity = self.recall_identity(identity_hash_hex) + if destination_identity is None: + # try recalling by telephony destination hash + destination_identity = self.recall_identity( + telephony_destination_hash.hex(), + ) + + # ensure identity was found + if destination_identity is None: + return web.json_response( + { + "message": "Call Failed: Destination identity not found.", + }, + status=503, + ) + + # initiate call + await asyncio.to_thread( + self.telephone_manager.telephone.call, + destination_identity, + ) + + return web.json_response( + { + "message": "Calling...", + }, + ) + + # serve list of available audio profiles + @routes.get("/api/v1/telephone/audio-profiles") + async def telephone_audio_profiles(request): + from LXST.Primitives.Telephony import Profiles + + # get audio profiles + audio_profiles = [] + for available_profile in Profiles.available_profiles(): + audio_profiles.append( + { + "id": available_profile, + "name": Profiles.profile_name(available_profile), + }, + ) + + return web.json_response( + { + "default_audio_profile_id": Profiles.DEFAULT_PROFILE, + "audio_profiles": audio_profiles, + }, + ) + + # announce + @routes.get("/api/v1/announce") + async def announce_trigger(request): + await self.announce() + + return web.json_response( + { + "message": "announcing", + }, + ) + + # serve announces + @routes.get("/api/v1/announces") + async def announces_get(request): + # get query params + aspect = request.query.get("aspect", None) + identity_hash = request.query.get("identity_hash", None) + destination_hash = request.query.get("destination_hash", None) + search_query = request.query.get("search", None) + limit = request.query.get("limit", None) + offset = request.query.get("offset", None) + include_blocked = request.query.get("include_blocked", "false").lower() == "true" + + blocked_identity_hashes = None + if not include_blocked: + blocked = self.database.misc.get_blocked_destinations() + blocked_identity_hashes = [b["destination_hash"] for b in blocked] + + # fetch announces from database + results = self.announce_manager.get_filtered_announces( + aspect=aspect, + identity_hash=identity_hash, + destination_hash=destination_hash, + query=search_query, + blocked_identity_hashes=blocked_identity_hashes, + ) + + # apply pagination + total_count = len(results) + if offset is not None or limit is not None: + start = int(offset) if offset else 0 + end = start + int(limit) if limit else total_count + paginated_results = results[start:end] + else: + paginated_results = results + + # process announces + announces = [ + self.convert_db_announce_to_dict(announce) for announce in paginated_results + ] + + return web.json_response( + { + "announces": announces, + "total_count": total_count, + }, + ) + + # serve favourites + @routes.get("/api/v1/favourites") + async def favourites_get(request): + # get query params + aspect = request.query.get("aspect", None) + + # get favourites from database + results = self.database.announces.get_favourites(aspect=aspect) + + # process favourites + favourites = [ + self.convert_db_favourite_to_dict(favourite) + for favourite in results + ] + + return web.json_response( + { + "favourites": favourites, + }, + ) + + # add favourite + @routes.post("/api/v1/favourites/add") + async def favourites_add(request): + # get request data + data = await request.json() + destination_hash = data.get("destination_hash", None) + display_name = data.get("display_name", None) + aspect = data.get("aspect", None) + + # destination hash is required + if destination_hash is None: + return web.json_response( + { + "message": "destination_hash is required", + }, + status=422, + ) + + # display name is required + if display_name is None: + return web.json_response( + { + "message": "display_name is required", + }, + status=422, + ) + + # aspect is required + if aspect is None: + return web.json_response( + { + "message": "aspect is required", + }, + status=422, + ) + + # upsert favourite + self.database.announces.upsert_favourite(destination_hash, display_name, aspect) + return web.json_response( + { + "message": "Favourite has been added!", + }, + ) + + # rename favourite + @routes.post("/api/v1/favourites/{destination_hash}/rename") + async def favourites_rename(request): + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + # get request data + data = await request.json() + display_name = data.get("display_name") + + # update display name if provided + if len(display_name) > 0: + self.database.announces.upsert_custom_display_name(destination_hash, display_name) + + return web.json_response( + { + "message": "Favourite has been renamed", + }, + ) + + # delete favourite + @routes.delete("/api/v1/favourites/{destination_hash}") + async def favourites_delete(request): + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + # delete favourite + self.database.announces.delete_favourite(destination_hash) + return web.json_response( + { + "message": "Favourite has been deleted!", + }, + ) + + # serve archived pages + @routes.get("/api/v1/nomadnet/archives") + async def get_all_archived_pages(request): + # get search query and pagination from request + query = request.query.get("q", "").strip() + page = int(request.query.get("page", 1)) + limit = int(request.query.get("limit", 15)) + offset = (page - 1) * limit + + # fetch archived pages from database + all_archives = self.database.misc.get_archived_pages_paginated( + query=query, + ) + total_count = len(all_archives) + total_pages = (total_count + limit - 1) // limit + + # apply pagination + archives_results = all_archives[offset : offset + limit] + + # return results + archives = [] + for archive in archives_results: + # find node name from announces or custom display names + node_name = self.get_custom_destination_display_name(archive["destination_hash"]) + if not node_name: + db_announce = self.database.announces.get_announce_by_hash(archive["destination_hash"]) + if db_announce and db_announce["aspect"] == "nomadnetwork.node": + node_name = ReticulumMeshChat.parse_nomadnetwork_node_display_name(db_announce["app_data"]) + + archives.append({ + "id": archive["id"], + "destination_hash": archive["destination_hash"], + "node_name": node_name or "Unknown Node", + "page_path": archive["page_path"], + "content": archive["content"], + "hash": archive["hash"], + "created_at": archive["created_at"], + }) + + return web.json_response({ + "archives": archives, + "pagination": { + "page": page, + "limit": limit, + "total_count": total_count, + "total_pages": total_pages, + }, + }) + + @routes.get("/api/v1/lxmf/propagation-node/status") + async def propagation_node_status(request): + return web.json_response( + { + "propagation_node_status": { + "state": self.convert_propagation_node_state_to_string( + self.message_router.propagation_transfer_state, + ), + "progress": self.message_router.propagation_transfer_progress + * 100, # convert to percentage + "messages_received": self.message_router.propagation_transfer_last_result, + }, + }, + ) + + # sync propagation node + @routes.get("/api/v1/lxmf/propagation-node/sync") + async def propagation_node_sync(request): + # ensure propagation node is configured before attempting to sync + if self.message_router.get_outbound_propagation_node() is None: + return web.json_response( + { + "message": "A propagation node must be configured to sync messages.", + }, + status=400, + ) + + # request messages from propagation node + await self.sync_propagation_nodes() + + return web.json_response( + { + "message": "Sync is starting", + }, + ) + + # stop syncing propagation node + @routes.get("/api/v1/lxmf/propagation-node/stop-sync") + async def propagation_node_stop_sync(request): + self.stop_propagation_node_sync() + + return web.json_response( + { + "message": "Sync is stopping", + }, + ) + + # serve propagation nodes + @routes.get("/api/v1/lxmf/propagation-nodes") + async def propagation_nodes_get(request): + # get query params + limit = request.query.get("limit", None) + + # get lxmf.propagation announces + results = self.database.announces.get_announces(aspect="lxmf.propagation") + + # limit results + if limit is not None: + results = results[:int(limit)] + + # process announces + lxmf_propagation_nodes = [] + for announce in results: + # find an lxmf.delivery announce for the same identity hash, so we can use that as an "operater by" name + lxmf_delivery_results = self.database.announces.get_filtered_announces( + aspect="lxmf.delivery", + identity_hash=announce["identity_hash"], + ) + lxmf_delivery_announce = lxmf_delivery_results[0] if lxmf_delivery_results else None + + # find a nomadnetwork.node announce for the same identity hash, so we can use that as an "operated by" name + nomadnetwork_node_results = self.database.announces.get_filtered_announces( + aspect="nomadnetwork.node", + identity_hash=announce["identity_hash"], + ) + nomadnetwork_node_announce = nomadnetwork_node_results[0] if nomadnetwork_node_results else None + + # get a display name from other announces belonging to the propagation nodes identity + operator_display_name = None + if ( + lxmf_delivery_announce is not None + and lxmf_delivery_announce["app_data"] is not None + ): + operator_display_name = self.parse_lxmf_display_name( + lxmf_delivery_announce["app_data"], + None, + ) + elif ( + nomadnetwork_node_announce is not None + and nomadnetwork_node_announce["app_data"] is not None + ): + operator_display_name = ReticulumMeshChat.parse_nomadnetwork_node_display_name( + nomadnetwork_node_announce["app_data"], + None, + ) + + # parse app_data so we can see if propagation is enabled or disabled for this node + is_propagation_enabled = None + per_transfer_limit = None + propagation_node_data = ( + ReticulumMeshChat.parse_lxmf_propagation_node_app_data( + announce["app_data"], + ) + ) + if propagation_node_data is not None: + is_propagation_enabled = propagation_node_data["enabled"] + per_transfer_limit = propagation_node_data["per_transfer_limit"] + + lxmf_propagation_nodes.append( + { + "destination_hash": announce["destination_hash"], + "identity_hash": announce["identity_hash"], + "operator_display_name": operator_display_name, + "is_propagation_enabled": is_propagation_enabled, + "per_transfer_limit": per_transfer_limit, + "created_at": announce["created_at"], + "updated_at": announce["updated_at"], + }, + ) + + return web.json_response( + { + "lxmf_propagation_nodes": lxmf_propagation_nodes, + }, + ) + + # get path to destination + @routes.get("/api/v1/destination/{destination_hash}/path") + async def destination_path(request): + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + # convert destination hash to bytes + destination_hash = bytes.fromhex(destination_hash) + + # check if user wants to request the path from the network right now + request_query_param = request.query.get("request", "false") + should_request_now = request_query_param in ("true", "1") + if should_request_now: + # determine how long we should wait for a path response + timeout_seconds = int(request.query.get("timeout", 15)) + timeout_after_seconds = time.time() + timeout_seconds + + # request path if we don't have it + if not RNS.Transport.has_path(destination_hash): + RNS.Transport.request_path(destination_hash) + + # wait until we have a path, or give up after the configured timeout + while ( + not RNS.Transport.has_path(destination_hash) + and time.time() < timeout_after_seconds + ): + await asyncio.sleep(0.1) + + # ensure path is known + if not RNS.Transport.has_path(destination_hash): + return web.json_response( + { + "path": None, + }, + ) + + # determine next hop and hop count + hops = RNS.Transport.hops_to(destination_hash) + next_hop_bytes = self.reticulum.get_next_hop(destination_hash) + + # ensure next hop provided + if next_hop_bytes is None: + return web.json_response( + { + "path": None, + }, + ) + + next_hop = next_hop_bytes.hex() + next_hop_interface = self.reticulum.get_next_hop_if_name(destination_hash) + + return web.json_response( + { + "path": { + "hops": hops, + "next_hop": next_hop, + "next_hop_interface": next_hop_interface, + }, + }, + ) + + # drop path to destination + @routes.post("/api/v1/destination/{destination_hash}/drop-path") + async def destination_drop_path(request): + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + # convert destination hash to bytes + destination_hash = bytes.fromhex(destination_hash) + + # drop path + self.reticulum.drop_path(destination_hash) + + return web.json_response( + { + "message": "Path has been dropped", + }, + ) + + # get signal metrics for a destination by checking the latest announce or lxmf message received from them + @routes.get("/api/v1/destination/{destination_hash}/signal-metrics") + async def destination_signal_metrics(request): + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + # signal metrics to return + snr = None + rssi = None + quality = None + updated_at = None + + # get latest announce from database for the provided destination hash + latest_announce = self.database.announces.get_announce_by_hash(destination_hash) + + # get latest lxmf message from database sent to us from the provided destination hash + local_hash = self.local_lxmf_destination.hexhash + messages = self.message_handler.get_conversation_messages(local_hash, destination_hash, limit=1) + # Filter for incoming messages only + latest_lxmf_message = next((m for m in messages if m["source_hash"] == destination_hash), None) + + # determine when latest announce was received + latest_announce_at = None + if latest_announce is not None: + latest_announce_at = datetime.fromisoformat(latest_announce["updated_at"]) + if latest_announce_at.tzinfo is not None: + latest_announce_at = latest_announce_at.replace(tzinfo=None) + + # determine when latest lxmf message was received + latest_lxmf_message_at = None + if latest_lxmf_message is not None: + latest_lxmf_message_at = datetime.fromisoformat( + latest_lxmf_message["created_at"], + ) + if latest_lxmf_message_at.tzinfo is not None: + latest_lxmf_message_at = latest_lxmf_message_at.replace(tzinfo=None) + + # get signal metrics from latest announce + if latest_announce is not None: + snr = latest_announce["snr"] + rssi = latest_announce["rssi"] + quality = latest_announce["quality"] + # using updated_at from announce because this is when the latest announce was received + updated_at = latest_announce["updated_at"] + + # get signal metrics from latest lxmf message if it's more recent than the announce + if latest_lxmf_message is not None and ( + latest_announce_at is None + or latest_lxmf_message_at > latest_announce_at + ): + snr = latest_lxmf_message["snr"] + rssi = latest_lxmf_message["rssi"] + quality = latest_lxmf_message["quality"] + # using created_at from lxmf message because this is when the message was received + updated_at = latest_lxmf_message["created_at"] + + return web.json_response( + { + "signal_metrics": { + "snr": snr, + "rssi": rssi, + "quality": quality, + "updated_at": updated_at, + }, + }, + ) + + # pings an lxmf.delivery destination by sending empty data and waiting for the recipient to send a proof back + # the lxmf router proves all received packets, then drops them if they can't be decoded as lxmf messages + # this allows us to ping/probe any active lxmf.delivery destination and get rtt/snr/rssi data on demand + # https://github.com/markqvist/LXMF/blob/9ff76c0473e9d4107e079f266dd08144bb74c7c8/LXMF/LXMRouter.py#L234 + # https://github.com/markqvist/LXMF/blob/9ff76c0473e9d4107e079f266dd08144bb74c7c8/LXMF/LXMRouter.py#L1374 + @routes.get("/api/v1/ping/{destination_hash}/lxmf.delivery") + async def ping_lxmf_delivery(request): + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + # convert destination hash to bytes + destination_hash = bytes.fromhex(destination_hash) + + # determine how long until we should time out + timeout_seconds = int(request.query.get("timeout", 15)) + timeout_after_seconds = time.time() + timeout_seconds + + # request path if we don't have it + if not RNS.Transport.has_path(destination_hash): + RNS.Transport.request_path(destination_hash) + + # wait until we have a path, or give up after the configured timeout + while ( + not RNS.Transport.has_path(destination_hash) + and time.time() < timeout_after_seconds + ): + await asyncio.sleep(0.1) + + # find destination identity + destination_identity = self.recall_identity(destination_hash) + if destination_identity is None: + return web.json_response( + { + "message": "Ping failed. Could not find path to destination.", + }, + status=503, + ) + + # create outbound destination + request_destination = RNS.Destination( + destination_identity, + RNS.Destination.OUT, + RNS.Destination.SINGLE, + "lxmf", + "delivery", + ) + + # send empty packet to destination + packet = RNS.Packet(request_destination, b"") + receipt = packet.send() + + # wait until delivered, or give up after time out + while ( + receipt.status != RNS.PacketReceipt.DELIVERED + and time.time() < timeout_after_seconds + ): + await asyncio.sleep(0.1) + + # ping failed if not delivered + if receipt.status != RNS.PacketReceipt.DELIVERED: + return web.json_response( + { + "message": f"Ping failed. Timed out after {timeout_seconds} seconds.", + }, + status=503, + ) + + # get number of hops to destination and back from destination + hops_there = RNS.Transport.hops_to(destination_hash) + hops_back = receipt.proof_packet.hops + + # get rssi + rssi = receipt.proof_packet.rssi + if rssi is None: + rssi = self.reticulum.get_packet_rssi(receipt.proof_packet.packet_hash) + + # get snr + snr = receipt.proof_packet.snr + if snr is None: + snr = self.reticulum.get_packet_snr(receipt.proof_packet.packet_hash) + + # get signal quality + quality = receipt.proof_packet.q + if quality is None: + quality = self.reticulum.get_packet_q(receipt.proof_packet.packet_hash) + + # get and format round trip time + rtt = receipt.get_rtt() + rtt_milliseconds = round(rtt * 1000, 3) + rtt_duration_string = f"{rtt_milliseconds} ms" + + return web.json_response( + { + "message": f"Valid reply from {receipt.destination.hash.hex()}\nDuration: {rtt_duration_string}\nHops There: {hops_there}\nHops Back: {hops_back}", + "ping_result": { + "rtt": rtt, + "hops_there": hops_there, + "hops_back": hops_back, + "rssi": rssi, + "snr": snr, + "quality": quality, + "receiving_interface": str( + receipt.proof_packet.receiving_interface, + ), + }, + }, + ) + + @routes.post("/api/v1/rncp/send") + async def rncp_send(request): + data = await request.json() + destination_hash_str = data.get("destination_hash", "") + file_path = data.get("file_path", "") + timeout = float(data.get("timeout", RNS.Transport.PATH_REQUEST_TIMEOUT)) + no_compress = bool(data.get("no_compress", False)) + + try: + destination_hash = bytes.fromhex(destination_hash_str) + except Exception as e: + return web.json_response( + {"message": f"Invalid destination hash: {e}"}, + status=400, + ) + + transfer_id = None + + def on_progress(progress): + if transfer_id: + AsyncUtils.run_async( + self._broadcast_websocket_message( + { + "type": "rncp.transfer.progress", + "transfer_id": transfer_id, + "progress": progress, + }, + ), + ) + + try: + result = await self.rncp_handler.send_file( + destination_hash=destination_hash, + file_path=file_path, + timeout=timeout, + on_progress=on_progress, + no_compress=no_compress, + ) + transfer_id = result["transfer_id"] + return web.json_response(result) + except Exception as e: + return web.json_response( + {"message": str(e)}, + status=500, + ) + + @routes.post("/api/v1/rncp/fetch") + async def rncp_fetch(request): + data = await request.json() + destination_hash_str = data.get("destination_hash", "") + file_path = data.get("file_path", "") + timeout = float(data.get("timeout", RNS.Transport.PATH_REQUEST_TIMEOUT)) + save_path = data.get("save_path") + allow_overwrite = bool(data.get("allow_overwrite", False)) + + try: + destination_hash = bytes.fromhex(destination_hash_str) + except Exception as e: + return web.json_response( + {"message": f"Invalid destination hash: {e}"}, + status=400, + ) + + transfer_id = None + + def on_progress(progress): + if transfer_id: + AsyncUtils.run_async( + self._broadcast_websocket_message( + { + "type": "rncp.transfer.progress", + "transfer_id": transfer_id, + "progress": progress, + }, + ), + ) + + try: + result = await self.rncp_handler.fetch_file( + destination_hash=destination_hash, + file_path=file_path, + timeout=timeout, + on_progress=on_progress, + save_path=save_path, + allow_overwrite=allow_overwrite, + ) + return web.json_response(result) + except Exception as e: + return web.json_response( + {"message": str(e)}, + status=500, + ) + + @routes.get("/api/v1/rncp/transfer/{transfer_id}") + async def rncp_transfer_status(request): + transfer_id = request.match_info.get("transfer_id", "") + status = self.rncp_handler.get_transfer_status(transfer_id) + if status: + return web.json_response(status) + return web.json_response( + {"message": "Transfer not found"}, + status=404, + ) + + @routes.post("/api/v1/rncp/listen") + async def rncp_listen(request): + data = await request.json() + allowed_hashes = data.get("allowed_hashes", []) + fetch_allowed = bool(data.get("fetch_allowed", False)) + fetch_jail = data.get("fetch_jail") + allow_overwrite = bool(data.get("allow_overwrite", False)) + + try: + destination_hash = self.rncp_handler.setup_receive_destination( + allowed_hashes=allowed_hashes, + fetch_allowed=fetch_allowed, + fetch_jail=fetch_jail, + allow_overwrite=allow_overwrite, + ) + return web.json_response( + { + "destination_hash": destination_hash, + "message": "RNCP listener started", + }, + ) + except Exception as e: + return web.json_response( + {"message": str(e)}, + status=500, + ) + + @routes.get("/api/v1/rnstatus") + async def rnstatus(request): + include_link_stats = request.query.get("include_link_stats", "false") in ("true", "1") + sorting = request.query.get("sorting") + sort_reverse = request.query.get("sort_reverse", "false") in ("true", "1") + + try: + status = self.rnstatus_handler.get_status( + include_link_stats=include_link_stats, + sorting=sorting, + sort_reverse=sort_reverse, + ) + return web.json_response(status) + except Exception as e: + return web.json_response( + {"message": str(e)}, + status=500, + ) + + @routes.post("/api/v1/rnprobe") + async def rnprobe(request): + data = await request.json() + destination_hash_str = data.get("destination_hash", "") + full_name = data.get("full_name", "") + size = int(data.get("size", RNProbeHandler.DEFAULT_PROBE_SIZE)) + timeout = float(data.get("timeout", 0)) or None + wait = float(data.get("wait", 0)) + probes = int(data.get("probes", 1)) + + try: + destination_hash = bytes.fromhex(destination_hash_str) + except Exception as e: + return web.json_response( + {"message": f"Invalid destination hash: {e}"}, + status=400, + ) + + if not full_name: + return web.json_response( + {"message": "full_name is required"}, + status=400, + ) + + try: + result = await self.rnprobe_handler.probe_destination( + destination_hash=destination_hash, + full_name=full_name, + size=size, + timeout=timeout, + wait=wait, + probes=probes, + ) + return web.json_response(result) + except Exception as e: + return web.json_response( + {"message": str(e)}, + status=500, + ) + + @routes.get("/api/v1/translator/languages") + async def translator_languages(request): + try: + libretranslate_url = request.query.get("libretranslate_url") + languages = self.translator_handler.get_supported_languages(libretranslate_url=libretranslate_url) + return web.json_response({ + "languages": languages, + "has_argos": self.translator_handler.has_argos, + "has_argos_lib": self.translator_handler.has_argos_lib, + "has_argos_cli": self.translator_handler.has_argos_cli, + }) + except Exception as e: + return web.json_response( + {"message": str(e)}, + status=500, + ) + + @routes.post("/api/v1/translator/translate") + async def translator_translate(request): + data = await request.json() + text = data.get("text", "") + source_lang = data.get("source_lang", "auto") + target_lang = data.get("target_lang", "") + use_argos = bool(data.get("use_argos", False)) + libretranslate_url = data.get("libretranslate_url") + + if not text: + return web.json_response( + {"message": "Text cannot be empty"}, + status=400, + ) + + if not target_lang: + return web.json_response( + {"message": "Target language is required"}, + status=400, + ) + + try: + result = self.translator_handler.translate_text( + text=text, + source_lang=source_lang, + target_lang=target_lang, + use_argos=use_argos, + libretranslate_url=libretranslate_url, + ) + return web.json_response(result) + except Exception as e: + return web.json_response( + {"message": str(e)}, + status=500, + ) + + @routes.post("/api/v1/translator/install-languages") + async def translator_install_languages(request): + data = await request.json() + package_name = data.get("package", "translate") + + try: + result = self.translator_handler.install_language_package(package_name) + return web.json_response(result) + except Exception as e: + return web.json_response( + {"message": str(e)}, + status=500, + ) + + # get custom destination display name + @routes.get("/api/v1/destination/{destination_hash}/custom-display-name") + async def destination_custom_display_name_get(request): + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + return web.json_response( + { + "custom_display_name": self.get_custom_destination_display_name( + destination_hash, + ), + }, + ) + + # set custom destination display name + @routes.post( + "/api/v1/destination/{destination_hash}/custom-display-name/update", + ) + async def destination_custom_display_name_update(request): + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + # get request data + data = await request.json() + display_name = data.get("display_name") + + # update display name if provided + if len(display_name) > 0: + self.database.announces.upsert_custom_display_name( + destination_hash, + display_name, + ) + return web.json_response( + { + "message": "Custom display name has been updated", + }, + ) + + # otherwise remove display name + self.database.announces.delete_custom_display_name(destination_hash) + return web.json_response( + { + "message": "Custom display name has been removed", + }, + ) + + # get lxmf stamp cost for the provided lxmf.delivery destination hash + @routes.get("/api/v1/destination/{destination_hash}/lxmf-stamp-info") + async def destination_lxmf_stamp_info(request): + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + # convert destination hash to bytes + destination_hash_bytes = bytes.fromhex(destination_hash) + + # get lxmf stamp cost from announce in database + lxmf_stamp_cost = None + announce = self.database.announces.get_announce_by_hash(destination_hash) + if announce is not None: + lxmf_stamp_cost = ReticulumMeshChat.parse_lxmf_stamp_cost(announce["app_data"]) + + # get outbound ticket expiry for this lxmf destination + lxmf_outbound_ticket_expiry = ( + self.message_router.get_outbound_ticket_expiry(destination_hash_bytes) + ) + + return web.json_response( + { + "lxmf_stamp_info": { + "stamp_cost": lxmf_stamp_cost, + "outbound_ticket_expiry": lxmf_outbound_ticket_expiry, + }, + }, + ) + + # get interface stats + @routes.get("/api/v1/interface-stats") + async def interface_stats(request): + # get interface stats + interface_stats = self.reticulum.get_interface_stats() + + # ensure transport_id is hex as json_response can't serialize bytes + if "transport_id" in interface_stats: + interface_stats["transport_id"] = interface_stats["transport_id"].hex() + + # ensure probe_responder is hex as json_response can't serialize bytes + if ( + "probe_responder" in interface_stats + and interface_stats["probe_responder"] is not None + ): + interface_stats["probe_responder"] = interface_stats[ + "probe_responder" + ].hex() + + # ensure ifac_signature is hex as json_response can't serialize bytes + for interface in interface_stats["interfaces"]: + if "short_name" in interface: + interface["interface_name"] = interface["short_name"] + + if ( + "parent_interface_name" in interface + and interface["parent_interface_name"] is not None + ): + interface["parent_interface_hash"] = interface[ + "parent_interface_hash" + ].hex() + + if interface.get("ifac_signature"): + interface["ifac_signature"] = interface["ifac_signature"].hex() + + if interface.get("hash"): + interface["hash"] = interface["hash"].hex() + + return web.json_response( + { + "interface_stats": interface_stats, + }, + ) + + # get path table + @routes.get("/api/v1/path-table") + async def path_table(request): + limit = request.query.get("limit", None) + offset = request.query.get("offset", None) + + # get path table, making sure hash and via are in hex as json_response can't serialize bytes + all_paths = self.reticulum.get_path_table() + total_count = len(all_paths) + + # apply pagination if requested + if limit is not None or offset is not None: + try: + start = int(offset) if offset else 0 + end = (start + int(limit)) if limit else total_count + paginated_paths = all_paths[start:end] + except (ValueError, TypeError): + paginated_paths = all_paths + else: + paginated_paths = all_paths + + path_table = [] + for path in paginated_paths: + path["hash"] = path["hash"].hex() + path["via"] = path["via"].hex() + path_table.append(path) + + return web.json_response( + { + "path_table": path_table, + "total_count": total_count, + }, + ) + + # send lxmf message + @routes.post("/api/v1/lxmf-messages/send") + async def lxmf_messages_send(request): + # get request body as json + data = await request.json() + + # get delivery method + delivery_method = None + if "delivery_method" in data: + delivery_method = data["delivery_method"] + + # get data from json + destination_hash = data["lxmf_message"]["destination_hash"] + content = data["lxmf_message"]["content"] + fields = {} + if "fields" in data["lxmf_message"]: + fields = data["lxmf_message"]["fields"] + + # parse image field + image_field = None + if "image" in fields: + image_type = data["lxmf_message"]["fields"]["image"]["image_type"] + image_bytes = base64.b64decode( + data["lxmf_message"]["fields"]["image"]["image_bytes"], + ) + image_field = LxmfImageField(image_type, image_bytes) + + # parse audio field + audio_field = None + if "audio" in fields: + audio_mode = data["lxmf_message"]["fields"]["audio"]["audio_mode"] + audio_bytes = base64.b64decode( + data["lxmf_message"]["fields"]["audio"]["audio_bytes"], + ) + audio_field = LxmfAudioField(audio_mode, audio_bytes) + + # parse file attachments field + file_attachments_field = None + if "file_attachments" in fields: + file_attachments = [] + for file_attachment in data["lxmf_message"]["fields"][ + "file_attachments" + ]: + file_name = file_attachment["file_name"] + file_bytes = base64.b64decode(file_attachment["file_bytes"]) + file_attachments.append(LxmfFileAttachment(file_name, file_bytes)) + + file_attachments_field = LxmfFileAttachmentsField(file_attachments) + + try: + # send lxmf message to destination + lxmf_message = await self.send_message( + destination_hash=destination_hash, + content=content, + image_field=image_field, + audio_field=audio_field, + file_attachments_field=file_attachments_field, + delivery_method=delivery_method, + ) + + return web.json_response( + { + "lxmf_message": self.convert_lxmf_message_to_dict( + lxmf_message, + include_attachments=False, + ), + }, + ) + + except Exception as e: + return web.json_response( + { + "message": f"Sending Failed: {e!s}", + }, + status=503, + ) + + # cancel sending lxmf message + @routes.post("/api/v1/lxmf-messages/{hash}/cancel") + async def lxmf_messages_cancel(request): + # get path params + message_hash = request.match_info.get("hash", None) + + # convert hash to bytes + hash_as_bytes = bytes.fromhex(message_hash) + + # cancel outbound message by lxmf message hash + self.message_router.cancel_outbound(hash_as_bytes) + + # get lxmf message from database + lxmf_message = None + db_lxmf_message = self.database.messages.get_lxmf_message_by_hash(message_hash) + if db_lxmf_message is not None: + lxmf_message = self.convert_db_lxmf_message_to_dict(db_lxmf_message) + + return web.json_response( + { + "message": "ok", + "lxmf_message": lxmf_message, + }, + ) + + # identify self on existing nomadnetwork link + @routes.post("/api/v1/nomadnetwork/{destination_hash}/identify") + async def nomadnetwork_identify(request): + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + # convert destination hash to bytes + destination_hash = bytes.fromhex(destination_hash) + + # identify to existing active link + if destination_hash in nomadnet_cached_links: + link = nomadnet_cached_links[destination_hash] + if link.status is RNS.Link.ACTIVE: + link.identify(self.identity) + return web.json_response( + { + "message": "Identity has been sent!", + }, + ) + + # failed to identify + return web.json_response( + { + "message": "Failed to identify. No active link to destination.", + }, + status=500, + ) + + # delete lxmf message + @routes.delete("/api/v1/lxmf-messages/{hash}") + async def lxmf_messages_delete(request): + # get path params + message_hash = request.match_info.get("hash", None) + + # hash is required + if message_hash is None: + return web.json_response( + { + "message": "hash is required", + }, + status=422, + ) + + # delete lxmf messages from db where hash matches + self.database.messages.delete_lxmf_message_by_hash(message_hash) + + return web.json_response( + { + "message": "ok", + }, + ) + + # serve lxmf messages for conversation + @routes.get("/api/v1/lxmf-messages/conversation/{destination_hash}") + async def lxmf_messages_conversation(request): + # get path params + destination_hash = request.match_info.get("destination_hash", "") + order = request.query.get("order", "asc") + count = request.query.get("count") + after_id = request.query.get("after_id") + + # get source hash from local lxmf destination + local_hash = self.local_lxmf_destination.hash.hex() + + # fetch messages from database + results = self.message_handler.get_conversation_messages( + local_hash, + destination_hash, + limit=int(count) if count else 100, + after_id=after_id if order == "asc" else None, + before_id=after_id if order == "desc" else None, + ) + + # convert to response json + lxmf_messages = [ + self.convert_db_lxmf_message_to_dict(db_lxmf_message) + for db_lxmf_message in results + ] + + return web.json_response( + { + "lxmf_messages": lxmf_messages, + }, + ) + + # fetch lxmf message attachment + @routes.get("/api/v1/lxmf-messages/attachment/{message_hash}/{attachment_type}") + async def lxmf_message_attachment(request): + message_hash = request.match_info.get("message_hash") + attachment_type = request.match_info.get("attachment_type") + file_index = request.query.get("file_index") + + # find message from database + db_lxmf_message = self.database.messages.get_lxmf_message_by_hash(message_hash) + if db_lxmf_message is None: + return web.json_response({"message": "Message not found"}, status=404) + + # parse fields + fields = json.loads(db_lxmf_message["fields"]) + + # handle image + if attachment_type == "image" and "image" in fields: + image_data = base64.b64decode(fields["image"]["image_bytes"]) + image_type = fields["image"]["image_type"] + return web.Response(body=image_data, content_type=f"image/{image_type}") + + # handle audio + if attachment_type == "audio" and "audio" in fields: + audio_data = base64.b64decode(fields["audio"]["audio_bytes"]) + return web.Response( + body=audio_data, + content_type="application/octet-stream", + ) + + # handle file attachments + if attachment_type == "file" and "file_attachments" in fields: + if file_index is not None: + try: + index = int(file_index) + file_attachment = fields["file_attachments"][index] + file_data = base64.b64decode(file_attachment["file_bytes"]) + return web.Response( + body=file_data, + content_type="application/octet-stream", + headers={ + "Content-Disposition": f'attachment; filename="{file_attachment["file_name"]}"', + }, + ) + except (ValueError, IndexError): + pass + + return web.json_response({"message": "Attachment not found"}, status=404) + + # delete lxmf messages for conversation + @routes.delete("/api/v1/lxmf-messages/conversation/{destination_hash}") + async def lxmf_messages_conversation_delete(request): + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + # get source hash from local lxmf destination + local_hash = self.local_lxmf_destination.hash.hex() + + # delete lxmf messages from db where "source to destination" or "destination to source" + self.message_handler.delete_conversation(local_hash, destination_hash) + + return web.json_response( + { + "message": "ok", + }, + ) + + # get lxmf conversations + @routes.get("/api/v1/lxmf/conversations") + async def lxmf_conversations_get(request): + # get query params + search_query = request.query.get("q", None) + filter_unread = ReticulumMeshChat.parse_bool_query_param( + request.query.get("unread", "false"), + ) + filter_failed = ReticulumMeshChat.parse_bool_query_param( + request.query.get("failed", "false"), + ) + filter_has_attachments = ReticulumMeshChat.parse_bool_query_param( + request.query.get("has_attachments", "false"), + ) + + local_hash = self.local_lxmf_destination.hexhash + search_destination_hashes = set() + if search_query is not None and search_query != "": + search_destination_hashes = self.search_destination_hashes_by_message( + search_query, + ) + + # fetch conversations from database + db_conversations = self.message_handler.get_conversations(local_hash) + + conversations = [] + for db_message in db_conversations: + # determine other user hash + if db_message["source_hash"] == local_hash: + other_user_hash = db_message["destination_hash"] + else: + other_user_hash = db_message["source_hash"] + + # determine latest message data + latest_message_title = db_message["title"] + latest_message_preview = db_message["content"] + latest_message_timestamp = db_message["timestamp"] + latest_message_has_attachments = ( + self.message_fields_have_attachments(db_message["fields"]) + ) + + # using timestamp (sent time) for updated_at as it is more reliable across restarts + # and represents the actual time the message was created by the sender. + # we convert it to ISO format for the frontend. + updated_at = datetime.fromtimestamp(latest_message_timestamp, UTC).isoformat() + + # check if conversation has attachments + has_attachments = self.conversation_has_attachments(other_user_hash) + + # find user icon from database + lxmf_user_icon = None + db_lxmf_user_icon = self.database.misc.get_user_icon(other_user_hash) + if db_lxmf_user_icon: + lxmf_user_icon = { + "icon_name": db_lxmf_user_icon["icon_name"], + "foreground_colour": db_lxmf_user_icon["foreground_colour"], + "background_colour": db_lxmf_user_icon["background_colour"], + } + + # add to conversations + conversations.append( + { + "display_name": self.get_lxmf_conversation_name( + other_user_hash, + ), + "custom_display_name": self.get_custom_destination_display_name( + other_user_hash, + ), + "destination_hash": other_user_hash, + "is_unread": self.database.messages.is_conversation_unread( + other_user_hash, + ), + "failed_messages_count": self.lxmf_conversation_failed_messages_count( + other_user_hash, + ), + "has_attachments": has_attachments, + "latest_message_title": latest_message_title, + "latest_message_preview": latest_message_preview, + "latest_message_created_at": latest_message_timestamp, + "latest_message_has_attachments": latest_message_has_attachments, + "lxmf_user_icon": lxmf_user_icon, + "updated_at": updated_at, + }, + ) + + if search_query is not None and search_query != "": + lowered_query = search_query.lower() + filtered = [] + for conversation in conversations: + matches_display = ( + conversation["display_name"] + and lowered_query in conversation["display_name"].lower() + ) + matches_custom = ( + conversation["custom_display_name"] + and lowered_query in conversation["custom_display_name"].lower() + ) + matches_destination = ( + conversation["destination_hash"] + and lowered_query in conversation["destination_hash"].lower() + ) + matches_latest_title = ( + conversation["latest_message_title"] + and lowered_query + in conversation["latest_message_title"].lower() + ) + matches_latest_preview = ( + conversation["latest_message_preview"] + and lowered_query + in conversation["latest_message_preview"].lower() + ) + matches_history = ( + conversation["destination_hash"] in search_destination_hashes + ) + if ( + matches_display + or matches_custom + or matches_destination + or matches_latest_title + or matches_latest_preview + or matches_history + ): + filtered.append(conversation) + conversations = filtered + + if filter_unread: + conversations = [c for c in conversations if c["is_unread"]] + + if filter_failed: + conversations = [ + c for c in conversations if c["failed_messages_count"] > 0 + ] + + if filter_has_attachments: + conversations = [c for c in conversations if c["has_attachments"]] + + return web.json_response( + { + "conversations": conversations, + }, + ) + + # mark lxmf conversation as read + @routes.get("/api/v1/lxmf/conversations/{destination_hash}/mark-as-read") + async def lxmf_conversations_mark_read(request): + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + # mark lxmf conversation as read + self.database.messages.mark_conversation_as_read(destination_hash) + + return web.json_response( + { + "message": "ok", + }, + ) + + # get blocked destinations + @routes.get("/api/v1/blocked-destinations") + async def blocked_destinations_get(request): + blocked = self.database.misc.get_blocked_destinations() + blocked_list = [ + { + "destination_hash": b["destination_hash"], + "created_at": b["created_at"], + } + for b in blocked + ] + return web.json_response( + { + "blocked_destinations": blocked_list, + }, + ) + + # add blocked destination + @routes.post("/api/v1/blocked-destinations") + async def blocked_destinations_add(request): + data = await request.json() + destination_hash = data.get("destination_hash", "") + if not destination_hash or len(destination_hash) != 32: + return web.json_response( + {"error": "Invalid destination hash"}, + status=400, + ) + + try: + self.database.misc.add_blocked_destination(destination_hash) + # drop any existing paths to this destination + try: + self.reticulum.drop_path(bytes.fromhex(destination_hash)) + except Exception as e: + print(f"Failed to drop path for blocked destination: {e}") + return web.json_response({"message": "ok"}) + except Exception: + return web.json_response( + {"error": "Destination already blocked"}, + status=400, + ) + + # remove blocked destination + @routes.delete("/api/v1/blocked-destinations/{destination_hash}") + async def blocked_destinations_delete(request): + destination_hash = request.match_info.get("destination_hash", "") + if not destination_hash or len(destination_hash) != 32: + return web.json_response( + {"error": "Invalid destination hash"}, + status=400, + ) + + try: + self.database.misc.delete_blocked_destination(destination_hash) + return web.json_response({"message": "ok"}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + # get spam keywords + @routes.get("/api/v1/spam-keywords") + async def spam_keywords_get(request): + keywords = self.database.misc.get_spam_keywords() + keyword_list = [ + { + "id": k["id"], + "keyword": k["keyword"], + "created_at": k["created_at"], + } + for k in keywords + ] + return web.json_response( + { + "spam_keywords": keyword_list, + }, + ) + + # add spam keyword + @routes.post("/api/v1/spam-keywords") + async def spam_keywords_add(request): + data = await request.json() + keyword = data.get("keyword", "").strip() + if not keyword: + return web.json_response({"error": "Keyword is required"}, status=400) + + try: + self.database.misc.add_spam_keyword(keyword) + return web.json_response({"message": "ok"}) + except Exception: + return web.json_response( + {"error": "Keyword already exists"}, + status=400, + ) + + # remove spam keyword + @routes.delete("/api/v1/spam-keywords/{keyword_id}") + async def spam_keywords_delete(request): + keyword_id = request.match_info.get("keyword_id", "") + try: + keyword_id = int(keyword_id) + except (ValueError, TypeError): + return web.json_response({"error": "Invalid keyword ID"}, status=400) + + try: + self.database.misc.delete_spam_keyword(keyword_id) + return web.json_response({"message": "ok"}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + # mark message as spam or not spam + @routes.post("/api/v1/lxmf-messages/{hash}/spam") + async def lxmf_messages_spam(request): + message_hash = request.match_info.get("hash", "") + data = await request.json() + is_spam = data.get("is_spam", False) + + try: + message = self.database.messages.get_lxmf_message_by_hash(message_hash) + if message: + message_data = dict(message) + message_data["is_spam"] = 1 if is_spam else 0 + self.database.messages.upsert_lxmf_message(message_data) + return web.json_response({"message": "ok"}) + return web.json_response({"error": "Message not found"}, status=404) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + # get offline map metadata + @routes.get("/api/v1/map/offline") + async def get_map_offline_metadata(request): + metadata = self.map_manager.get_metadata() + if metadata: + return web.json_response(metadata) + return web.json_response({"error": "No offline map loaded"}, status=404) + + # get map tile + @routes.get("/api/v1/map/tiles/{z}/{x}/{y}") + async def get_map_tile(request): + try: + z = int(request.match_info.get("z")) + x = int(request.match_info.get("x")) + y_str = request.match_info.get("y") + # remove .png if present + y_str = y_str.removesuffix(".png") + y = int(y_str) + + tile_data = self.map_manager.get_tile(z, x, y) + if tile_data: + return web.Response(body=tile_data, content_type="image/png") + return web.Response(status=404) + except Exception: + return web.Response(status=400) + + # list available MBTiles files + @routes.get("/api/v1/map/mbtiles") + async def list_mbtiles(request): + return web.json_response(self.map_manager.list_mbtiles()) + + # delete an MBTiles file + @routes.delete("/api/v1/map/mbtiles/{filename}") + async def delete_mbtiles(request): + filename = request.match_info.get("filename") + if self.map_manager.delete_mbtiles(filename): + return web.json_response({"message": "File deleted"}) + return web.json_response({"error": "File not found"}, status=404) + + # set active MBTiles file + @routes.post("/api/v1/map/mbtiles/active") + async def set_active_mbtiles(request): + data = await request.json() + filename = data.get("filename") + if not filename: + self.config.map_offline_path.set(None) + self.config.map_offline_enabled.set(False) + return web.json_response({"message": "Offline map disabled"}) + + mbtiles_dir = self.map_manager.get_mbtiles_dir() + file_path = os.path.join(mbtiles_dir, filename) + if os.path.exists(file_path): + self.map_manager.close() + self.config.map_offline_path.set(file_path) + self.config.map_offline_enabled.set(True) + return web.json_response({"message": "Active map updated", "metadata": self.map_manager.get_metadata()}) + return web.json_response({"error": "File not found"}, status=404) + + # upload offline map + @routes.post("/api/v1/map/offline") + async def upload_map_offline(request): + try: + reader = await request.multipart() + field = await reader.next() + if field.name != "file": + return web.json_response({"error": "No file field"}, status=400) + + filename = field.filename + if not filename.endswith(".mbtiles"): + return web.json_response({"error": "Invalid file format, must be .mbtiles"}, status=400) + + # save to mbtiles dir + mbtiles_dir = self.map_manager.get_mbtiles_dir() + if not os.path.exists(mbtiles_dir): + os.makedirs(mbtiles_dir) + + dest_path = os.path.join(mbtiles_dir, filename) + + size = 0 + with open(dest_path, "wb") as f: + while True: + chunk = await field.read_chunk() + if not chunk: + break + size += len(chunk) + f.write(chunk) + + # close old connection and clear cache before update + self.map_manager.close() + + # update config + self.config.map_offline_path.set(dest_path) + self.config.map_offline_enabled.set(True) + + # validate + metadata = self.map_manager.get_metadata() + if not metadata: + # delete if invalid + if os.path.exists(dest_path): + os.remove(dest_path) + self.config.map_offline_path.set(None) + self.config.map_offline_enabled.set(False) + return web.json_response({"error": "Invalid MBTiles file or unsupported format (vector maps not supported)"}, status=400) + + return web.json_response({ + "message": "Map uploaded successfully", + "metadata": metadata, + }) + except Exception as e: + RNS.log(f"Error uploading map: {e}", RNS.LOG_ERROR) + return web.json_response({"error": str(e)}, status=500) + + # start map export + @routes.post("/api/v1/map/export") + async def start_map_export(request): + try: + data = await request.json() + bbox = data.get("bbox") # [min_lon, min_lat, max_lon, max_lat] + min_zoom = int(data.get("min_zoom", 0)) + max_zoom = int(data.get("max_zoom", 10)) + name = data.get("name", "Exported Map") + + if not bbox or len(bbox) != 4: + return web.json_response({"error": "Invalid bbox"}, status=400) + + export_id = secrets.token_hex(8) + self.map_manager.start_export(export_id, bbox, min_zoom, max_zoom, name) + + return web.json_response({"export_id": export_id}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + # get map export status + @routes.get("/api/v1/map/export/{export_id}") + async def get_map_export_status(request): + export_id = request.match_info.get("export_id") + status = self.map_manager.get_export_status(export_id) + if status: + return web.json_response(status) + return web.json_response({"error": "Export not found"}, status=404) + + # download exported map + @routes.get("/api/v1/map/export/{export_id}/download") + async def download_map_export(request): + export_id = request.match_info.get("export_id") + status = self.map_manager.get_export_status(export_id) + if status and status.get("status") == "completed": + file_path = status.get("file_path") + if os.path.exists(file_path): + return web.FileResponse( + path=file_path, + headers={ + "Content-Disposition": f'attachment; filename="map_export_{export_id}.mbtiles"', + }, + ) + return web.json_response({"error": "File not ready or not found"}, status=404) + + # MIME type fix middleware - ensures JavaScript files have correct Content-Type + @web.middleware + async def mime_type_middleware(request, handler): + response = await handler(request) + path = request.path + if path.endswith(".js") or path.endswith(".mjs"): + response.headers["Content-Type"] = ( + "application/javascript; charset=utf-8" + ) + elif path.endswith(".css"): + response.headers["Content-Type"] = "text/css; charset=utf-8" + elif path.endswith(".json"): + response.headers["Content-Type"] = "application/json; charset=utf-8" + elif path.endswith(".html"): + response.headers["Content-Type"] = "text/html; charset=utf-8" + return response + + # security headers middleware + @web.middleware + async def security_middleware(request, handler): + response = await handler(request) + # Add security headers to all responses + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-Frame-Options"] = "DENY" + response.headers["X-XSS-Protection"] = "1; mode=block" + response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" + # CSP: allow localhost for development and Electron, websockets, and blob URLs + csp = ( + "default-src 'self'; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org; " + "font-src 'self' data:; " + "connect-src 'self' ws://localhost:* wss://localhost:* blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org https://nominatim.openstreetmap.org; " + "media-src 'self' blob:; " + "worker-src 'self' blob:; " + "object-src 'none'; " + "base-uri 'self';" + ) + response.headers["Content-Security-Policy"] = csp + return response + + # called when web app has started + async def on_startup(app): + # remember main event loop + AsyncUtils.set_main_loop(asyncio.get_event_loop()) + + # auto launch web browser + if launch_browser: + try: + protocol = "https" if use_https else "http" + webbrowser.open(f"{protocol}://127.0.0.1:{port}") + except Exception: + print("failed to launch web browser") + + # create and run web app + app = web.Application( + client_max_size=1024 * 1024 * 50, + ) # allow uploading files up to 50mb + + # setup session storage + # aiohttp_session.setup must be called before other middlewares that use sessions + secret_key = base64.urlsafe_b64decode( + session_secret_key.encode("utf-8").ljust(44, b"=")[:44], + ) + setup_session( + app, + EncryptedCookieStorage(secret_key), + ) + + # add other middlewares + app.middlewares.extend([auth_middleware, mime_type_middleware, security_middleware]) + + app.add_routes(routes) + app.add_routes( + [web.static("/", get_file_path("public/"))], + ) # serve anything in public folder + app.on_shutdown.append( + self.shutdown, + ) # need to force close websockets and stop reticulum now + app.on_startup.append(on_startup) + + protocol = "https" if use_https else "http" + print(f"Starting web server on {protocol}://{host}:{port}") + + if use_https and ssl_context: + web.run_app(app, host=host, port=port, ssl_context=ssl_context) + else: + web.run_app(app, host=host, port=port) + + # handle announcing + async def announce(self): + # update last announced at timestamp + self.config.last_announced_at.set(int(time.time())) + + # send announce for lxmf (ensuring name is updated before announcing) + self.local_lxmf_destination.display_name = self.config.display_name.get() + self.message_router.announce(destination_hash=self.local_lxmf_destination.hash) + + # send announce for local propagation node (if enabled) + if self.config.lxmf_local_propagation_node_enabled.get(): + self.message_router.announce_propagation_node() + + # send announce for telephone + self.telephone_manager.announce() + + # tell websocket clients we just announced + await self.send_announced_to_websocket_clients() + + # handle syncing propagation nodes + async def sync_propagation_nodes(self): + # update last synced at timestamp + self.config.lxmf_preferred_propagation_node_last_synced_at.set(int(time.time())) + + # request messages from propagation node + self.message_router.request_messages_from_propagation_node(self.identity) + + # send config to websocket clients (used to tell ui last synced at) + await self.send_config_to_websocket_clients() + + async def update_config(self, data): + # update display name in config + if "display_name" in data and data["display_name"] != "": + self.config.display_name.set(data["display_name"]) + + # update theme in config + if "theme" in data and data["theme"] != "": + self.config.theme.set(data["theme"]) + + # update language in config + if "language" in data and data["language"] != "": + self.config.language.set(data["language"]) + + # update auto announce interval + if "auto_announce_interval_seconds" in data: + # auto auto announce interval + auto_announce_interval_seconds = int(data["auto_announce_interval_seconds"]) + self.config.auto_announce_interval_seconds.set( + data["auto_announce_interval_seconds"], + ) + + # enable or disable auto announce based on interval + if auto_announce_interval_seconds > 0: + self.config.auto_announce_enabled.set(True) + else: + self.config.auto_announce_enabled.set(False) + + if "auto_resend_failed_messages_when_announce_received" in data: + value = bool(data["auto_resend_failed_messages_when_announce_received"]) + self.config.auto_resend_failed_messages_when_announce_received.set(value) + + if "allow_auto_resending_failed_messages_with_attachments" in data: + value = bool(data["allow_auto_resending_failed_messages_with_attachments"]) + self.config.allow_auto_resending_failed_messages_with_attachments.set(value) + + if "auto_send_failed_messages_to_propagation_node" in data: + value = bool(data["auto_send_failed_messages_to_propagation_node"]) + self.config.auto_send_failed_messages_to_propagation_node.set(value) + + if "show_suggested_community_interfaces" in data: + value = bool(data["show_suggested_community_interfaces"]) + self.config.show_suggested_community_interfaces.set(value) + + if "lxmf_preferred_propagation_node_destination_hash" in data: + # update config value + value = data["lxmf_preferred_propagation_node_destination_hash"] + self.config.lxmf_preferred_propagation_node_destination_hash.set(value) + + # update active propagation node + self.set_active_propagation_node(value) + + # update inbound stamp cost (for direct delivery messages) + if "lxmf_inbound_stamp_cost" in data: + value = int(data["lxmf_inbound_stamp_cost"]) + # validate stamp cost (must be between 1 and 254) + if value < 1: + value = None + elif value >= 255: + value = 254 + self.config.lxmf_inbound_stamp_cost.set(value) + # update the inbound stamp cost on the delivery destination + self.message_router.set_inbound_stamp_cost( + self.local_lxmf_destination.hash, + value, + ) + # re-announce to update the stamp cost in announces + self.local_lxmf_destination.display_name = self.config.display_name.get() + self.message_router.announce( + destination_hash=self.local_lxmf_destination.hash, + ) + + # update propagation node stamp cost (for messages propagated through your node) + if "lxmf_propagation_node_stamp_cost" in data: + value = int(data["lxmf_propagation_node_stamp_cost"]) + # validate stamp cost (must be at least 13, per LXMF minimum) + if value < 13: + value = 13 + elif value >= 255: + value = 254 + self.config.lxmf_propagation_node_stamp_cost.set(value) + # update the propagation stamp cost on the router + self.message_router.propagation_stamp_cost = value + # re-announce propagation node if enabled + if self.config.lxmf_local_propagation_node_enabled.get(): + self.message_router.announce_propagation_node() + + # update auto sync interval + if "lxmf_preferred_propagation_node_auto_sync_interval_seconds" in data: + value = int( + data["lxmf_preferred_propagation_node_auto_sync_interval_seconds"], + ) + self.config.lxmf_preferred_propagation_node_auto_sync_interval_seconds.set( + value, + ) + + if "lxmf_local_propagation_node_enabled" in data: + # update config value + value = bool(data["lxmf_local_propagation_node_enabled"]) + self.config.lxmf_local_propagation_node_enabled.set(value) + + # enable or disable local propagation node + self.enable_local_propagation_node(value) + + # update lxmf user icon name in config + if "lxmf_user_icon_name" in data: + self.config.lxmf_user_icon_name.set(data["lxmf_user_icon_name"]) + + # update lxmf user icon foreground colour in config + if "lxmf_user_icon_foreground_colour" in data: + self.config.lxmf_user_icon_foreground_colour.set( + data["lxmf_user_icon_foreground_colour"], + ) + + # update lxmf user icon background colour in config + if "lxmf_user_icon_background_colour" in data: + self.config.lxmf_user_icon_background_colour.set( + data["lxmf_user_icon_background_colour"], + ) + + # update archiver settings + if "page_archiver_enabled" in data: + self.config.page_archiver_enabled.set(bool(data["page_archiver_enabled"])) + + if "page_archiver_max_versions" in data: + self.config.page_archiver_max_versions.set(int(data["page_archiver_max_versions"])) + + if "archives_max_storage_gb" in data: + self.config.archives_max_storage_gb.set(int(data["archives_max_storage_gb"])) + + # update crawler settings + if "crawler_enabled" in data: + self.config.crawler_enabled.set(bool(data["crawler_enabled"])) + + if "crawler_max_retries" in data: + self.config.crawler_max_retries.set(int(data["crawler_max_retries"])) + + if "crawler_retry_delay_seconds" in data: + self.config.crawler_retry_delay_seconds.set(int(data["crawler_retry_delay_seconds"])) + + if "crawler_max_concurrent" in data: + self.config.crawler_max_concurrent.set(int(data["crawler_max_concurrent"])) + + if "auth_enabled" in data: + value = bool(data["auth_enabled"]) + self.config.auth_enabled.set(value) + self.auth_enabled = value + + # if disabling auth, also remove the password hash from config + if not value: + self.config.auth_password_hash.set(None) + + # update map settings + if "map_offline_enabled" in data: + self.config.map_offline_enabled.set(bool(data["map_offline_enabled"])) + + if "map_default_lat" in data: + self.config.map_default_lat.set(str(data["map_default_lat"])) + + if "map_default_lon" in data: + self.config.map_default_lon.set(str(data["map_default_lon"])) + + if "map_default_zoom" in data: + self.config.map_default_zoom.set(int(data["map_default_zoom"])) + + if "map_mbtiles_dir" in data: + self.config.map_mbtiles_dir.set(data["map_mbtiles_dir"]) + + if "map_tile_cache_enabled" in data: + self.config.map_tile_cache_enabled.set(bool(data["map_tile_cache_enabled"])) + + if "map_tile_server_url" in data: + self.config.map_tile_server_url.set(data["map_tile_server_url"]) + + if "map_nominatim_api_url" in data: + self.config.map_nominatim_api_url.set(data["map_nominatim_api_url"]) + + # send config to websocket clients + await self.send_config_to_websocket_clients() + + # converts nomadnetwork page variables from a string to a map + # converts: "field1=123|field2=456" + # to the following map: + # - var_field1: 123 + # - var_field2: 456 + @staticmethod + def convert_nomadnet_string_data_to_map(path_data: str | None): + data = {} + if path_data is not None: + for field in path_data.split("|"): + if "=" in field: + variable_name, variable_value = field.split("=") + data[f"var_{variable_name}"] = variable_value + else: + print(f"unhandled field: {field}") + return data + + @staticmethod + def convert_nomadnet_field_data_to_map(field_data): + data = {} + if field_data is not None or "{}": + try: + json_data = field_data + if isinstance(json_data, dict): + # add the prefixed keys to the result dictionary + data = {f"field_{key}": value for key, value in json_data.items()} + else: + return None + except Exception as e: + print(f"skipping invalid field data: {e}") + + return data + + # archives a page version + def archive_page(self, destination_hash: str, page_path: str, content: str, is_manual: bool = False): + if not is_manual and not self.config.page_archiver_enabled.get(): + return + + self.archiver_manager.archive_page( + destination_hash, + page_path, + content, + max_versions=self.config.page_archiver_max_versions.get(), + max_storage_gb=self.config.archives_max_storage_gb.get(), + ) + + # returns archived page versions for a given destination and path + def get_archived_page_versions(self, destination_hash: str, page_path: str): + return self.database.misc.get_archived_page_versions(destination_hash, page_path) + + # flushes all archived pages + def flush_all_archived_pages(self): + self.database.misc.delete_archived_pages() + + # handle data received from websocket client + async def on_websocket_data_received(self, client, data): + # get type from client data + _type = data["type"] + + # handle ping + if _type == "ping": + AsyncUtils.run_async( + client.send_str( + json.dumps( + { + "type": "pong", + }, + ), + ), + ) + + # handle updating config + elif _type == "config.set": + # get config from websocket + config = data["config"] + + # update config + await self.update_config(config) + + # handle canceling a download + elif _type == "nomadnet.download.cancel": + # get data from websocket client + download_id = data["download_id"] + + # cancel the download + if download_id in self.active_downloads: + downloader = self.active_downloads[download_id] + downloader.cancel() + del self.active_downloads[download_id] + + # notify client + AsyncUtils.run_async( + client.send_str( + json.dumps( + { + "type": "nomadnet.download.cancelled", + "download_id": download_id, + }, + ), + ), + ) + + # handle getting page archives + elif _type == "nomadnet.page.archives.get": + destination_hash = data["destination_hash"] + page_path = data["page_path"] + archives = self.get_archived_page_versions(destination_hash, page_path) + + AsyncUtils.run_async( + client.send_str( + json.dumps( + { + "type": "nomadnet.page.archives", + "destination_hash": destination_hash, + "page_path": page_path, + "archives": [ + { + "id": archive.id, + "hash": archive.hash, + "created_at": archive.created_at.isoformat() if hasattr(archive.created_at, "isoformat") else str(archive.created_at), + } + for archive in archives + ], + }, + ), + ), + ) + + # handle loading a specific archived page version + elif _type == "nomadnet.page.archive.load": + archive_id = data["archive_id"] + archive = self.database.misc.get_archived_page_by_id(archive_id) + + if archive: + AsyncUtils.run_async( + client.send_str( + json.dumps( + { + "type": "nomadnet.page.download", + "download_id": data.get("download_id"), + "nomadnet_page_download": { + "status": "success", + "destination_hash": archive["destination_hash"], + "page_path": archive["page_path"], + "page_content": archive["content"], + "is_archived_version": True, + "archived_at": archive["created_at"], + }, + }, + ), + ), + ) + + # handle flushing all archived pages + elif _type == "nomadnet.page.archive.flush": + self.flush_all_archived_pages() + # notify config updated + AsyncUtils.run_async(self.send_config_to_websocket_clients()) + + # handle manual page archiving + elif _type == "nomadnet.page.archive.add": + destination_hash = data["destination_hash"] + page_path = data["page_path"] + content = data["content"] + self.archive_page(destination_hash, page_path, content, is_manual=True) + + # notify client that page was archived + AsyncUtils.run_async( + client.send_str( + json.dumps( + { + "type": "nomadnet.page.archive.added", + "destination_hash": destination_hash, + "page_path": page_path, + }, + ), + ), + ) + + # handle downloading a file from a nomadnet node + elif _type == "nomadnet.file.download": + # get data from websocket client + destination_hash = data["nomadnet_file_download"]["destination_hash"] + file_path = data["nomadnet_file_download"]["file_path"] + + # convert destination hash to bytes + destination_hash = bytes.fromhex(destination_hash) + + # generate download id + self.download_id_counter += 1 + download_id = self.download_id_counter + + # handle successful file download + def on_file_download_success(file_name, file_bytes): + # remove from active downloads + if download_id in self.active_downloads: + del self.active_downloads[download_id] + + # Track download speed + download_size = len(file_bytes) + if hasattr(downloader, "start_time") and downloader.start_time: + download_duration = time.time() - downloader.start_time + if download_duration > 0: + self.download_speeds.append((download_size, download_duration)) + # Keep only last 100 downloads for average calculation + if len(self.download_speeds) > 100: + self.download_speeds.pop(0) + + AsyncUtils.run_async( + client.send_str( + json.dumps( + { + "type": "nomadnet.file.download", + "download_id": download_id, + "nomadnet_file_download": { + "status": "success", + "destination_hash": destination_hash.hex(), + "file_path": file_path, + "file_name": file_name, + "file_bytes": base64.b64encode(file_bytes).decode( + "utf-8", + ), + }, + }, + ), + ), + ) + + # handle file download failure + def on_file_download_failure(failure_reason): + # remove from active downloads + if download_id in self.active_downloads: + del self.active_downloads[download_id] + + AsyncUtils.run_async( + client.send_str( + json.dumps( + { + "type": "nomadnet.file.download", + "download_id": download_id, + "nomadnet_file_download": { + "status": "failure", + "failure_reason": failure_reason, + "destination_hash": destination_hash.hex(), + "file_path": file_path, + }, + }, + ), + ), + ) + + # handle file download progress + def on_file_download_progress(progress): + AsyncUtils.run_async( + client.send_str( + json.dumps( + { + "type": "nomadnet.file.download", + "download_id": download_id, + "nomadnet_file_download": { + "status": "progress", + "progress": progress, + "destination_hash": destination_hash.hex(), + "file_path": file_path, + }, + }, + ), + ), + ) + + # download the file + downloader = NomadnetFileDownloader( + destination_hash, + file_path, + on_file_download_success, + on_file_download_failure, + on_file_download_progress, + ) + downloader.start_time = time.time() + self.active_downloads[download_id] = downloader + + # notify client download started + AsyncUtils.run_async( + client.send_str( + json.dumps( + { + "type": "nomadnet.file.download", + "download_id": download_id, + "nomadnet_file_download": { + "status": "started", + "destination_hash": destination_hash.hex(), + "file_path": file_path, + }, + }, + ), + ), + ) + + AsyncUtils.run_async(downloader.download()) + + # handle downloading a page from a nomadnet node + elif _type == "nomadnet.page.download": + # get data from websocket client + destination_hash = data["nomadnet_page_download"]["destination_hash"] + page_path = data["nomadnet_page_download"]["page_path"] + field_data = data["nomadnet_page_download"]["field_data"] + + # generate download id + self.download_id_counter += 1 + download_id = self.download_id_counter + + combined_data = {} + # parse data from page path + # example: hash:/page/index.mu`field1=123|field2=456 + page_data = None + page_path_to_download = page_path + if "`" in page_path: + page_path_parts = page_path.split("`") + page_path_to_download = page_path_parts[0] + page_data = self.convert_nomadnet_string_data_to_map(page_path_parts[1]) + + # Field data + field_data = self.convert_nomadnet_field_data_to_map(field_data) + + # Combine page data and field data + if page_data is not None: + combined_data.update(page_data) + if field_data is not None: + combined_data.update(field_data) + + # convert destination hash to bytes + destination_hash = bytes.fromhex(destination_hash) + + # handle successful page download + def on_page_download_success(page_content): + # remove from active downloads + if download_id in self.active_downloads: + del self.active_downloads[download_id] + + # archive the page if enabled + self.archive_page(destination_hash.hex(), page_path, page_content) + + AsyncUtils.run_async( + client.send_str( + json.dumps( + { + "type": "nomadnet.page.download", + "download_id": download_id, + "nomadnet_page_download": { + "status": "success", + "destination_hash": destination_hash.hex(), + "page_path": page_path, + "page_content": page_content, + }, + }, + ), + ), + ) + + # handle page download failure + def on_page_download_failure(failure_reason): + # remove from active downloads + if download_id in self.active_downloads: + del self.active_downloads[download_id] + + # check if there are any archived versions + has_archives = ( + len( + self.get_archived_page_versions( + destination_hash.hex(), page_path, + ), + ) + > 0 + ) + + AsyncUtils.run_async( + client.send_str( + json.dumps( + { + "type": "nomadnet.page.download", + "download_id": download_id, + "nomadnet_page_download": { + "status": "failure", + "failure_reason": failure_reason, + "destination_hash": destination_hash.hex(), + "page_path": page_path, + "has_archives": has_archives, + }, + }, + ), + ), + ) + + # handle page download progress + def on_page_download_progress(progress): + AsyncUtils.run_async( + client.send_str( + json.dumps( + { + "type": "nomadnet.page.download", + "download_id": download_id, + "nomadnet_page_download": { + "status": "progress", + "progress": progress, + "destination_hash": destination_hash.hex(), + "page_path": page_path, + }, + }, + ), + ), + ) + + # download the page + downloader = NomadnetPageDownloader( + destination_hash, + page_path_to_download, + combined_data, + on_page_download_success, + on_page_download_failure, + on_page_download_progress, + ) + self.active_downloads[download_id] = downloader + + # notify client download started + AsyncUtils.run_async( + client.send_str( + json.dumps( + { + "type": "nomadnet.page.download", + "download_id": download_id, + "nomadnet_page_download": { + "status": "started", + "destination_hash": destination_hash.hex(), + "page_path": page_path, + }, + }, + ), + ), + ) + + AsyncUtils.run_async(downloader.download()) + + # handle lxmf forwarding rules + elif _type == "lxmf.forwarding.rules.get": + rules = self.database.misc.get_forwarding_rules() + AsyncUtils.run_async( + client.send_str( + json.dumps( + { + "type": "lxmf.forwarding.rules", + "rules": [ + { + "id": rule["id"], + "identity_hash": rule["identity_hash"], + "forward_to_hash": rule["forward_to_hash"], + "source_filter_hash": rule["source_filter_hash"], + "is_active": bool(rule["is_active"]), + } + for rule in rules + ], + }, + ), + ), + ) + + elif _type == "lxmf.forwarding.rule.add": + rule_data = data["rule"] + self.database.misc.create_forwarding_rule( + identity_hash=rule_data.get("identity_hash"), + forward_to_hash=rule_data["forward_to_hash"], + source_filter_hash=rule_data.get("source_filter_hash"), + is_active=rule_data.get("is_active", True), + ) + # notify updated + AsyncUtils.run_async( + self.on_websocket_data_received( + client, + {"type": "lxmf.forwarding.rules.get"}, + ), + ) + + elif _type == "lxmf.forwarding.rule.delete": + rule_id = data["id"] + self.database.misc.delete_forwarding_rule(rule_id) + # notify updated + AsyncUtils.run_async( + self.on_websocket_data_received( + client, + {"type": "lxmf.forwarding.rules.get"}, + ), + ) + + elif _type == "lxmf.forwarding.rule.toggle": + rule_id = data["id"] + self.database.misc.toggle_forwarding_rule(rule_id) + # notify updated + AsyncUtils.run_async( + self.on_websocket_data_received( + client, + {"type": "lxmf.forwarding.rules.get"}, + ), + ) + + # unhandled type + else: + print("unhandled client message type: " + _type) + + # broadcast provided data to all connected websocket clients + async def websocket_broadcast(self, data): + for websocket_client in self.websocket_clients: + try: + await websocket_client.send_str(data) + except Exception as e: + # do nothing if failed to broadcast to a specific websocket client + print(f"Failed to broadcast to websocket client: {e}") + + # broadcasts config to all websocket clients + async def send_config_to_websocket_clients(self): + await self.websocket_broadcast( + json.dumps( + { + "type": "config", + "config": self.get_config_dict(), + }, + ), + ) + + # broadcasts to all websocket clients that we just announced + async def send_announced_to_websocket_clients(self): + await self.websocket_broadcast( + json.dumps( + { + "type": "announced", + }, + ), + ) + + # returns a dictionary of config + def get_config_dict(self): + return { + "display_name": self.config.display_name.get(), + "identity_hash": self.identity.hexhash, + "lxmf_address_hash": self.local_lxmf_destination.hexhash, + "telephone_address_hash": self.telephone_manager.telephone.destination.hexhash + if self.telephone_manager.telephone + else None, + "is_transport_enabled": self.reticulum.transport_enabled(), + "auto_announce_enabled": self.config.auto_announce_enabled.get(), + "auto_announce_interval_seconds": self.config.auto_announce_interval_seconds.get(), + "last_announced_at": self.config.last_announced_at.get(), + "theme": self.config.theme.get(), + "language": self.config.language.get(), + "auto_resend_failed_messages_when_announce_received": self.config.auto_resend_failed_messages_when_announce_received.get(), + "allow_auto_resending_failed_messages_with_attachments": self.config.allow_auto_resending_failed_messages_with_attachments.get(), + "auto_send_failed_messages_to_propagation_node": self.config.auto_send_failed_messages_to_propagation_node.get(), + "show_suggested_community_interfaces": self.config.show_suggested_community_interfaces.get(), + "lxmf_local_propagation_node_enabled": self.config.lxmf_local_propagation_node_enabled.get(), + "lxmf_local_propagation_node_address_hash": self.message_router.propagation_destination.hexhash, + "lxmf_preferred_propagation_node_destination_hash": self.config.lxmf_preferred_propagation_node_destination_hash.get(), + "lxmf_preferred_propagation_node_auto_sync_interval_seconds": self.config.lxmf_preferred_propagation_node_auto_sync_interval_seconds.get(), + "lxmf_preferred_propagation_node_last_synced_at": self.config.lxmf_preferred_propagation_node_last_synced_at.get(), + "lxmf_user_icon_name": self.config.lxmf_user_icon_name.get(), + "lxmf_user_icon_foreground_colour": self.config.lxmf_user_icon_foreground_colour.get(), + "lxmf_user_icon_background_colour": self.config.lxmf_user_icon_background_colour.get(), + "lxmf_inbound_stamp_cost": self.config.lxmf_inbound_stamp_cost.get(), + "lxmf_propagation_node_stamp_cost": self.config.lxmf_propagation_node_stamp_cost.get(), + "page_archiver_enabled": self.config.page_archiver_enabled.get(), + "page_archiver_max_versions": self.config.page_archiver_max_versions.get(), + "archives_max_storage_gb": self.config.archives_max_storage_gb.get(), + "crawler_enabled": self.config.crawler_enabled.get(), + "crawler_max_retries": self.config.crawler_max_retries.get(), + "crawler_retry_delay_seconds": self.config.crawler_retry_delay_seconds.get(), + "crawler_max_concurrent": self.config.crawler_max_concurrent.get(), + "auth_enabled": self.auth_enabled, + } + + # try and get a name for the provided identity hash + def get_name_for_identity_hash(self, identity_hash: str): + # 1. try recall identity and calculate lxmf destination hash + identity = self.recall_identity(identity_hash) + if identity is not None: + # get lxmf.delivery destination hash + lxmf_destination_hash = RNS.Destination.hash(identity, "lxmf", "delivery").hex() + + # use custom name if available + custom_name = self.database.announces.get_custom_display_name( + lxmf_destination_hash, + ) + if custom_name is not None: + return custom_name + + # use lxmf name if available + lxmf_name = self.get_lxmf_conversation_name( + lxmf_destination_hash, + default_name=None, + ) + if lxmf_name is not None: + return lxmf_name + + # 2. if identity recall failed, or we couldn't find a name for the calculated hash + # try to look up an lxmf.delivery announce with this identity_hash in the database + announces = self.database.announces.get_filtered_announces( + aspect="lxmf.delivery", + search_term=identity_hash, + ) + if announces: + for announce in announces: + # search_term matches destination_hash OR identity_hash in the DAO. + # We want to be sure it's the identity_hash we're looking for. + if announce["identity_hash"] == identity_hash: + lxmf_destination_hash = announce["destination_hash"] + + # check custom name for this hash + custom_name = self.database.announces.get_custom_display_name( + lxmf_destination_hash, + ) + if custom_name is not None: + return custom_name + + # check lxmf name from app_data + if announce["app_data"] is not None: + lxmf_name = ReticulumMeshChat.parse_lxmf_display_name( + app_data_base64=announce["app_data"], + default_value=None, + ) + if lxmf_name is not None: + return lxmf_name + + # couldn't find a name for this identity + return None + + # recall identity from reticulum or database + def recall_identity(self, hash_hex: str) -> RNS.Identity | None: + try: + # 1. try reticulum recall (works for both identity and destination hashes) + hash_bytes = bytes.fromhex(hash_hex) + identity = RNS.Identity.recall(hash_bytes) + if identity: + return identity + + # 2. try database lookup + # lookup by destination hash first + announce = self.database.announces.get_announce_by_hash(hash_hex) + if announce: + announce = dict(announce) + + if not announce: + # lookup by identity hash + results = self.database.announces.get_filtered_announces( + search_term=hash_hex, + ) + if results: + # find first one with a public key + for res in results: + res_dict = dict(res) + if res_dict.get("identity_public_key"): + announce = res_dict + break + + if announce and announce.get("identity_public_key"): + public_key = base64.b64decode(announce["identity_public_key"]) + identity = RNS.Identity(create_keys=False) + if identity.load_public_key(public_key): + return identity + + except Exception as e: + print(f"Error recalling identity for {hash_hex}: {e}") + + return None + + # convert an lxmf message to a dictionary, for sending over websocket + def convert_lxmf_message_to_dict( + self, + lxmf_message: LXMF.LXMessage, + include_attachments: bool = True, + ): + # handle fields + fields = {} + message_fields = lxmf_message.get_fields() + for field_type in message_fields: + value = message_fields[field_type] + + # handle file attachments field + if field_type == LXMF.FIELD_FILE_ATTACHMENTS: + # process file attachments + file_attachments = [] + for file_attachment in value: + file_name = file_attachment[0] + file_bytes = None + if include_attachments: + file_bytes = base64.b64encode(file_attachment[1]).decode( + "utf-8", + ) + + file_attachments.append( + { + "file_name": file_name, + "file_bytes": file_bytes, + }, + ) + + # add to fields + fields["file_attachments"] = file_attachments + + # handle image field + if field_type == LXMF.FIELD_IMAGE: + image_type = value[0] + image_bytes = None + if include_attachments: + image_bytes = base64.b64encode(value[1]).decode("utf-8") + + fields["image"] = { + "image_type": image_type, + "image_bytes": image_bytes, + } + + # handle audio field + if field_type == LXMF.FIELD_AUDIO: + audio_mode = value[0] + audio_bytes = None + if include_attachments: + audio_bytes = base64.b64encode(value[1]).decode("utf-8") + + fields["audio"] = { + "audio_mode": audio_mode, + "audio_bytes": audio_bytes, + } + + # convert 0.0-1.0 progress to 0.00-100 percentage + progress_percentage = round(lxmf_message.progress * 100, 2) + + # get rssi + rssi = lxmf_message.rssi + if rssi is None: + rssi = self.reticulum.get_packet_rssi(lxmf_message.hash) + + # get snr + snr = lxmf_message.snr + if snr is None: + snr = self.reticulum.get_packet_snr(lxmf_message.hash) + + # get quality + quality = lxmf_message.q + if quality is None: + quality = self.reticulum.get_packet_q(lxmf_message.hash) + + return { + "hash": lxmf_message.hash.hex(), + "source_hash": lxmf_message.source_hash.hex(), + "destination_hash": lxmf_message.destination_hash.hex(), + "is_incoming": lxmf_message.incoming, + "state": self.convert_lxmf_state_to_string(lxmf_message), + "progress": progress_percentage, + "method": self.convert_lxmf_method_to_string(lxmf_message), + "delivery_attempts": lxmf_message.delivery_attempts, + "next_delivery_attempt_at": getattr( + lxmf_message, + "next_delivery_attempt", + None, + ), # attribute may not exist yet + "title": lxmf_message.title.decode("utf-8") if lxmf_message.title else "", + "content": lxmf_message.content.decode("utf-8") + if lxmf_message.content + else "", + "fields": fields, + "timestamp": lxmf_message.timestamp, + "rssi": rssi, + "snr": snr, + "quality": quality, + } + + # convert lxmf state to a human friendly string + @staticmethod + def convert_lxmf_state_to_string(lxmf_message: LXMF.LXMessage): + # convert state to string + lxmf_message_state = "unknown" + if lxmf_message.state == LXMF.LXMessage.GENERATING: + lxmf_message_state = "generating" + elif lxmf_message.state == LXMF.LXMessage.OUTBOUND: + lxmf_message_state = "outbound" + elif lxmf_message.state == LXMF.LXMessage.SENDING: + lxmf_message_state = "sending" + elif lxmf_message.state == LXMF.LXMessage.SENT: + lxmf_message_state = "sent" + elif lxmf_message.state == LXMF.LXMessage.DELIVERED: + lxmf_message_state = "delivered" + elif lxmf_message.state == LXMF.LXMessage.REJECTED: + lxmf_message_state = "rejected" + elif lxmf_message.state == LXMF.LXMessage.CANCELLED: + lxmf_message_state = "cancelled" + elif lxmf_message.state == LXMF.LXMessage.FAILED: + lxmf_message_state = "failed" + + return lxmf_message_state + + # convert lxmf method to a human friendly string + @staticmethod + def convert_lxmf_method_to_string(lxmf_message: LXMF.LXMessage): + # convert method to string + lxmf_message_method = "unknown" + if lxmf_message.method == LXMF.LXMessage.OPPORTUNISTIC: + lxmf_message_method = "opportunistic" + elif lxmf_message.method == LXMF.LXMessage.DIRECT: + lxmf_message_method = "direct" + elif lxmf_message.method == LXMF.LXMessage.PROPAGATED: + lxmf_message_method = "propagated" + elif lxmf_message.method == LXMF.LXMessage.PAPER: + lxmf_message_method = "paper" + + return lxmf_message_method + + @staticmethod + def convert_propagation_node_state_to_string(state): + # map states to strings + state_map = { + LXMRouter.PR_IDLE: "idle", + LXMRouter.PR_PATH_REQUESTED: "path_requested", + LXMRouter.PR_LINK_ESTABLISHING: "link_establishing", + LXMRouter.PR_LINK_ESTABLISHED: "link_established", + LXMRouter.PR_REQUEST_SENT: "request_sent", + LXMRouter.PR_RECEIVING: "receiving", + LXMRouter.PR_RESPONSE_RECEIVED: "response_received", + LXMRouter.PR_COMPLETE: "complete", + LXMRouter.PR_NO_PATH: "no_path", + LXMRouter.PR_LINK_FAILED: "link_failed", + LXMRouter.PR_TRANSFER_FAILED: "transfer_failed", + LXMRouter.PR_NO_IDENTITY_RCVD: "no_identity_received", + LXMRouter.PR_NO_ACCESS: "no_access", + LXMRouter.PR_FAILED: "failed", + } + + # return string for state, or fallback to unknown + if state in state_map: + return state_map[state] + return "unknown" + + # convert database announce to a dictionary + def convert_db_announce_to_dict(self, announce): + # parse display name from announce + display_name = None + if announce["aspect"] == "lxmf.delivery": + display_name = self.parse_lxmf_display_name(announce["app_data"]) + elif announce["aspect"] == "nomadnetwork.node": + display_name = ReticulumMeshChat.parse_nomadnetwork_node_display_name( + announce["app_data"], + ) + + # find lxmf user icon from database + lxmf_user_icon = None + db_lxmf_user_icon = self.database.misc.get_user_icon(announce["destination_hash"]) + if db_lxmf_user_icon: + lxmf_user_icon = { + "icon_name": db_lxmf_user_icon["icon_name"], + "foreground_colour": db_lxmf_user_icon["foreground_colour"], + "background_colour": db_lxmf_user_icon["background_colour"], + } + + # get current hops away + hops = RNS.Transport.hops_to(bytes.fromhex(announce["destination_hash"])) + + # ensure created_at and updated_at have Z suffix for UTC if they don't have a timezone + created_at = str(announce["created_at"]) + if created_at and "+" not in created_at and "Z" not in created_at: + created_at += "Z" + + updated_at = str(announce["updated_at"]) + if updated_at and "+" not in updated_at and "Z" not in updated_at: + updated_at += "Z" + + return { + "id": announce["id"], + "destination_hash": announce["destination_hash"], + "aspect": announce["aspect"], + "identity_hash": announce["identity_hash"], + "identity_public_key": announce["identity_public_key"], + "app_data": announce["app_data"], + "hops": hops, + "rssi": announce["rssi"], + "snr": announce["snr"], + "quality": announce["quality"], + "display_name": display_name, + "custom_display_name": self.get_custom_destination_display_name( + announce["destination_hash"], + ), + "lxmf_user_icon": lxmf_user_icon, + "created_at": created_at, + "updated_at": updated_at, + } + + # convert database favourite to a dictionary + @staticmethod + def convert_db_favourite_to_dict(favourite): + # ensure created_at and updated_at have Z suffix for UTC if they don't have a timezone + created_at = str(favourite["created_at"]) + if created_at and "+" not in created_at and "Z" not in created_at: + created_at += "Z" + + updated_at = str(favourite["updated_at"]) + if updated_at and "+" not in updated_at and "Z" not in updated_at: + updated_at += "Z" + + return { + "id": favourite["id"], + "destination_hash": favourite["destination_hash"], + "display_name": favourite["display_name"], + "aspect": favourite["aspect"], + "created_at": created_at, + "updated_at": updated_at, + } + + # convert database lxmf message to a dictionary + @staticmethod + def convert_db_lxmf_message_to_dict( + db_lxmf_message, + include_attachments: bool = False, + ): + fields = json.loads(db_lxmf_message["fields"]) + + # strip attachments if requested + if not include_attachments: + if "image" in fields: + # keep type but strip bytes + image_size = 0 + if fields["image"].get("image_bytes"): + try: + image_size = len( + base64.b64decode(fields["image"]["image_bytes"]), + ) + except Exception as e: + print(f"Failed to decode image bytes: {e}") + fields["image"] = { + "image_type": fields["image"].get("image_type"), + "image_size": image_size, + "image_bytes": None, + } + if "audio" in fields: + # keep mode but strip bytes + audio_size = 0 + if fields["audio"].get("audio_bytes"): + try: + audio_size = len( + base64.b64decode(fields["audio"]["audio_bytes"]), + ) + except Exception as e: + print(f"Failed to decode audio bytes: {e}") + fields["audio"] = { + "audio_mode": fields["audio"].get("audio_mode"), + "audio_size": audio_size, + "audio_bytes": None, + } + if "file_attachments" in fields: + # keep file names but strip bytes + for i in range(len(fields["file_attachments"])): + file_size = 0 + if fields["file_attachments"][i].get("file_bytes"): + try: + file_size = len( + base64.b64decode( + fields["file_attachments"][i]["file_bytes"], + ), + ) + except Exception as e: + print(f"Failed to decode file attachment bytes: {e}") + fields["file_attachments"][i] = { + "file_name": fields["file_attachments"][i].get("file_name"), + "file_size": file_size, + "file_bytes": None, + } + + # ensure created_at and updated_at have Z suffix for UTC if they don't have a timezone + created_at = str(db_lxmf_message["created_at"]) + if created_at and "+" not in created_at and "Z" not in created_at: + created_at += "Z" + + updated_at = str(db_lxmf_message["updated_at"]) + if updated_at and "+" not in updated_at and "Z" not in updated_at: + updated_at += "Z" + + return { + "id": db_lxmf_message["id"], + "hash": db_lxmf_message["hash"], + "source_hash": db_lxmf_message["source_hash"], + "destination_hash": db_lxmf_message["destination_hash"], + "is_incoming": bool(db_lxmf_message["is_incoming"]), + "state": db_lxmf_message["state"], + "progress": db_lxmf_message["progress"], + "method": db_lxmf_message["method"], + "delivery_attempts": db_lxmf_message["delivery_attempts"], + "next_delivery_attempt_at": db_lxmf_message["next_delivery_attempt_at"], + "title": db_lxmf_message["title"], + "content": db_lxmf_message["content"], + "fields": fields, + "timestamp": db_lxmf_message["timestamp"], + "rssi": db_lxmf_message["rssi"], + "snr": db_lxmf_message["snr"], + "quality": db_lxmf_message["quality"], + "is_spam": bool(db_lxmf_message["is_spam"]), + "created_at": created_at, + "updated_at": updated_at, + } + + # updates the lxmf user icon for the provided destination hash + @staticmethod + def update_lxmf_user_icon( + self, + destination_hash: str, + icon_name: str, + foreground_colour: str, + background_colour: str, + ): + # log + print( + f"updating lxmf user icon for {destination_hash} to icon_name={icon_name}, foreground_colour={foreground_colour}, background_colour={background_colour}", + ) + + self.database.misc.update_lxmf_user_icon( + destination_hash, + icon_name, + foreground_colour, + background_colour, + ) + + # check if a destination is blocked + def is_destination_blocked(self, destination_hash: str) -> bool: + try: + return self.database.misc.is_destination_blocked(destination_hash) + except Exception: + return False + + # check if message content matches spam keywords + def check_spam_keywords(self, title: str, content: str) -> bool: + try: + return self.database.misc.check_spam_keywords(title, content) + except Exception: + return False + + # check if message has attachments and should be rejected + @staticmethod + def has_attachments(lxmf_fields: dict) -> bool: + try: + if LXMF.FIELD_FILE_ATTACHMENTS in lxmf_fields: + return len(lxmf_fields[LXMF.FIELD_FILE_ATTACHMENTS]) > 0 + if LXMF.FIELD_IMAGE in lxmf_fields: + return True + if LXMF.FIELD_AUDIO in lxmf_fields: + return True + return False + except Exception: + return False + + # handle an lxmf delivery from reticulum + # NOTE: cant be async, as Reticulum doesn't await it + def on_lxmf_delivery(self, lxmf_message: LXMF.LXMessage): + try: + source_hash = lxmf_message.source_hash.hex() + + # check if source is blocked - reject immediately + if self.is_destination_blocked(source_hash): + print(f"Rejecting LXMF message from blocked source: {source_hash}") + return + + # check if this lxmf message contains a telemetry request command from sideband + is_sideband_telemetry_request = False + lxmf_fields = lxmf_message.get_fields() + if LXMF.FIELD_COMMANDS in lxmf_fields: + for command in lxmf_fields[LXMF.FIELD_COMMANDS]: + if SidebandCommands.TELEMETRY_REQUEST in command: + is_sideband_telemetry_request = True + + # ignore telemetry requests from sideband + if is_sideband_telemetry_request: + print( + "Ignoring received LXMF message as it is a telemetry request command", + ) + return + + # check for spam keywords + is_spam = False + message_title = lxmf_message.title if hasattr(lxmf_message, "title") else "" + message_content = ( + lxmf_message.content if hasattr(lxmf_message, "content") else "" + ) + + # check spam keywords + if self.check_spam_keywords(message_title, message_content): + is_spam = True + print( + f"Marking LXMF message as spam due to keyword match: {source_hash}", + ) + + # reject attachments from blocked sources (already checked above, but double-check) + if self.has_attachments(lxmf_fields): + if self.is_destination_blocked(source_hash): + print( + f"Rejecting LXMF message with attachments from blocked source: {source_hash}", + ) + return + # reject attachments from spam sources + if is_spam: + print( + f"Rejecting LXMF message with attachments from spam source: {source_hash}", + ) + return + + # upsert lxmf message to database with spam flag + self.db_upsert_lxmf_message(lxmf_message, is_spam=is_spam) + + # handle forwarding + self.handle_forwarding(lxmf_message) + + # update lxmf user icon if icon appearance field is available + try: + message_fields = lxmf_message.get_fields() + if LXMF.FIELD_ICON_APPEARANCE in message_fields: + icon_appearance = message_fields[LXMF.FIELD_ICON_APPEARANCE] + icon_name = icon_appearance[0] + foreground_colour = "#" + icon_appearance[1].hex() + background_colour = "#" + icon_appearance[2].hex() + self.update_lxmf_user_icon( + lxmf_message.source_hash.hex(), + icon_name, + foreground_colour, + background_colour, + ) + except Exception as e: + print("failed to update lxmf user icon from lxmf message") + print(e) + + # find message from database + db_lxmf_message = self.database.messages.get_lxmf_message_by_hash(lxmf_message.hash.hex()) + if not db_lxmf_message: + return + + # send received lxmf message data to all websocket clients + AsyncUtils.run_async( + self.websocket_broadcast( + json.dumps( + { + "type": "lxmf.delivery", + "lxmf_message": self.convert_db_lxmf_message_to_dict( + db_lxmf_message, + include_attachments=False, + ), + }, + ), + ), + ) + + except Exception as e: + # do nothing on error + print(f"lxmf_delivery error: {e}") + + # handles lxmf message forwarding logic + def handle_forwarding(self, lxmf_message: LXMF.LXMessage): + try: + source_hash = lxmf_message.source_hash.hex() + destination_hash = lxmf_message.destination_hash.hex() + + # check if this message is for an alias identity (REPLY PATH) + mapping = self.database.messages.get_forwarding_mapping(alias_hash=destination_hash) + + if mapping: + # this is a reply from User C to User B (alias). Forward to User A. + print( + f"Forwarding reply from {source_hash} back to original sender {mapping['original_sender_hash']}", + ) + AsyncUtils.run_async( + self.send_message( + destination_hash=mapping["original_sender_hash"], + content=lxmf_message.content, + title=lxmf_message.title + if hasattr(lxmf_message, "title") + else "", + ), + ) + return + + # check if this message matches a forwarding rule (FORWARD PATH) + # we check for rules that apply to the destination of this message + rules = self.database.misc.get_forwarding_rules(identity_hash=destination_hash, active_only=True) + + for rule in rules: + # check source filter if set + if rule["source_filter_hash"] and rule["source_filter_hash"] != source_hash: + continue + + # find or create mapping for this (Source, Final Recipient) pair + mapping = self.forwarding_manager.get_or_create_mapping( + source_hash, + rule["forward_to_hash"], + destination_hash, + ) + + # forward to User C from Alias Identity + print( + f"Forwarding message from {source_hash} to {rule['forward_to_hash']} via alias {mapping['alias_hash']}", + ) + AsyncUtils.run_async( + self.send_message( + destination_hash=rule["forward_to_hash"], + content=lxmf_message.content, + title=lxmf_message.title + if hasattr(lxmf_message, "title") + else "", + sender_identity_hash=mapping["alias_hash"], + ), + ) + except Exception as e: + print(f"Error in handle_forwarding: {e}") + + # handle delivery status update for an outbound lxmf message + def on_lxmf_sending_state_updated(self, lxmf_message): + # upsert lxmf message to database + self.db_upsert_lxmf_message(lxmf_message) + + # send lxmf message state to all websocket clients + AsyncUtils.run_async( + self.websocket_broadcast( + json.dumps( + { + "type": "lxmf_message_state_updated", + "lxmf_message": self.convert_lxmf_message_to_dict( + lxmf_message, + include_attachments=False, + ), + }, + ), + ), + ) + + # handle delivery failed for an outbound lxmf message + def on_lxmf_sending_failed(self, lxmf_message): + # check if this failed message should fall back to sending via a propagation node + if ( + lxmf_message.state == LXMF.LXMessage.FAILED + and hasattr(lxmf_message, "try_propagation_on_fail") + and lxmf_message.try_propagation_on_fail + ): + self.send_failed_message_via_propagation_node(lxmf_message) + + # update state + self.on_lxmf_sending_state_updated(lxmf_message) + + # sends a previously failed message via a propagation node + def send_failed_message_via_propagation_node(self, lxmf_message: LXMF.LXMessage): + # reset internal message state + lxmf_message.packed = None + lxmf_message.delivery_attempts = 0 + if hasattr(lxmf_message, "next_delivery_attempt"): + del lxmf_message.next_delivery_attempt + + # this message should now be sent via a propagation node + lxmf_message.desired_method = LXMF.LXMessage.PROPAGATED + lxmf_message.try_propagation_on_fail = False + + # resend message + self.message_router.handle_outbound(lxmf_message) + + # upserts the provided lxmf message to the database + def db_upsert_lxmf_message( + self, + lxmf_message: LXMF.LXMessage, + is_spam: bool = False, + ): + # convert lxmf message to dict + lxmf_message_dict = self.convert_lxmf_message_to_dict(lxmf_message) + lxmf_message_dict["is_spam"] = 1 if is_spam else 0 + self.database.messages.upsert_lxmf_message(lxmf_message_dict) + + # upserts the provided announce to the database + # handle sending an lxmf message to reticulum + async def send_message( + self, + destination_hash: str, + content: str, + image_field: LxmfImageField = None, + audio_field: LxmfAudioField = None, + file_attachments_field: LxmfFileAttachmentsField = None, + delivery_method: str = None, + title: str = "", + sender_identity_hash: str = None, + ) -> LXMF.LXMessage: + # convert destination hash to bytes + destination_hash_bytes = bytes.fromhex(destination_hash) + + # determine when to timeout finding path + timeout_after_seconds = time.time() + 10 + + # check if we have a path to the destination + if not RNS.Transport.has_path(destination_hash_bytes): + # we don't have a path, so we need to request it + RNS.Transport.request_path(destination_hash_bytes) + + # wait until we have a path, or give up after the configured timeout + while ( + not RNS.Transport.has_path(destination_hash_bytes) + and time.time() < timeout_after_seconds + ): + await asyncio.sleep(0.1) + + # find destination identity from hash + destination_identity = RNS.Identity.recall(destination_hash_bytes) + if destination_identity is None: + # we have to bail out of sending, since we don't have the identity/path yet + msg = "Could not find path to destination. Try again later." + raise Exception(msg) + + # create destination for recipients lxmf delivery address + lxmf_destination = RNS.Destination( + destination_identity, + RNS.Destination.OUT, + RNS.Destination.SINGLE, + "lxmf", + "delivery", + ) + + # determine how the user wants to send the message + desired_delivery_method = None + if delivery_method == "direct": + desired_delivery_method = LXMF.LXMessage.DIRECT + elif delivery_method == "opportunistic": + desired_delivery_method = LXMF.LXMessage.OPPORTUNISTIC + elif delivery_method == "propagated": + desired_delivery_method = LXMF.LXMessage.PROPAGATED + + # determine how to send the message if the user didn't provide a method + if desired_delivery_method is None: + # send messages over a direct link by default + desired_delivery_method = LXMF.LXMessage.DIRECT + if ( + not self.message_router.delivery_link_available(destination_hash_bytes) + and RNS.Identity.current_ratchet_id(destination_hash_bytes) is not None + ): + # since there's no link established to the destination, it's faster to send opportunistically + # this is because it takes several packets to establish a link, and then we still have to send the message over it + # oppotunistic mode will send the message in a single packet (if the message is small enough, otherwise it falls back to a direct link) + # we will only do this if an encryption ratchet is available, so single packet delivery is more secure + desired_delivery_method = LXMF.LXMessage.OPPORTUNISTIC + + # determine which identity to send from + source_destination = self.local_lxmf_destination + if sender_identity_hash is not None: + if sender_identity_hash in self.forwarding_destinations: + source_destination = self.forwarding_destinations[sender_identity_hash] + else: + print( + f"Warning: requested sender identity {sender_identity_hash} not found, using default.", + ) + + # create lxmf message + lxmf_message = LXMF.LXMessage( + lxmf_destination, + source_destination, + content, + title=title, + desired_method=desired_delivery_method, + ) + lxmf_message.try_propagation_on_fail = ( + self.config.auto_send_failed_messages_to_propagation_node.get() + ) + + lxmf_message.fields = {} + + # add file attachments field + if file_attachments_field is not None: + # create array of [[file_name, file_bytes], [file_name, file_bytes], ...] + file_attachments = [ + [file_attachment.file_name, file_attachment.file_bytes] + for file_attachment in file_attachments_field.file_attachments + ] + + # set field attachments field + lxmf_message.fields[LXMF.FIELD_FILE_ATTACHMENTS] = file_attachments + + # add image field + if image_field is not None: + lxmf_message.fields[LXMF.FIELD_IMAGE] = [ + image_field.image_type, + image_field.image_bytes, + ] + + # add audio field + if audio_field is not None: + lxmf_message.fields[LXMF.FIELD_AUDIO] = [ + audio_field.audio_mode, + audio_field.audio_bytes, + ] + + # add icon appearance if configured + # fixme: we could save a tiny amount of bandwidth here, but this requires more effort... + # we could keep track of when the icon appearance was last sent to this destination, and when it last changed + # we could save 6 bytes for the 2x colours, and also however long the icon name is, but not today! + lxmf_user_icon_name = self.config.lxmf_user_icon_name.get() + lxmf_user_icon_foreground_colour = ( + self.config.lxmf_user_icon_foreground_colour.get() + ) + lxmf_user_icon_background_colour = ( + self.config.lxmf_user_icon_background_colour.get() + ) + if ( + lxmf_user_icon_name is not None + and lxmf_user_icon_foreground_colour is not None + and lxmf_user_icon_background_colour is not None + ): + lxmf_message.fields[LXMF.FIELD_ICON_APPEARANCE] = [ + lxmf_user_icon_name, + ColourUtils.hex_colour_to_byte_array(lxmf_user_icon_foreground_colour), + ColourUtils.hex_colour_to_byte_array(lxmf_user_icon_background_colour), + ] + + # register delivery callbacks + lxmf_message.register_delivery_callback(self.on_lxmf_sending_state_updated) + lxmf_message.register_failed_callback(self.on_lxmf_sending_failed) + + # send lxmf message to be routed to destination + self.message_router.handle_outbound(lxmf_message) + + # upsert lxmf message to database + self.db_upsert_lxmf_message(lxmf_message) + + # tell all websocket clients that old failed message was deleted so it can remove from ui + await self.websocket_broadcast( + json.dumps( + { + "type": "lxmf_message_created", + "lxmf_message": self.convert_lxmf_message_to_dict( + lxmf_message, + include_attachments=False, + ), + }, + ), + ) + + # handle lxmf message progress loop without blocking or awaiting + # otherwise other incoming websocket packets will not be processed until sending is complete + # which results in the next message not showing up until the first message is finished + AsyncUtils.run_async(self.handle_lxmf_message_progress(lxmf_message)) + + return lxmf_message + + # updates lxmf message in database and broadcasts to websocket until it's delivered, or it fails + async def handle_lxmf_message_progress(self, lxmf_message): + # FIXME: there's no register_progress_callback on the lxmf message, so manually send progress until delivered, propagated or failed + # we also can't use on_lxmf_sending_state_updated method to do this, because of async/await issues... + should_update_message = True + while should_update_message: + # wait 1 second between sending updates + await asyncio.sleep(1) + + # upsert lxmf message to database (as we want to update the progress in database too) + self.db_upsert_lxmf_message(lxmf_message) + + # send update to websocket clients + await self.websocket_broadcast( + json.dumps( + { + "type": "lxmf_message_state_updated", + "lxmf_message": self.convert_lxmf_message_to_dict( + lxmf_message, + include_attachments=False, + ), + }, + ), + ) + + # check message state + has_delivered = lxmf_message.state == LXMF.LXMessage.DELIVERED + has_propagated = ( + lxmf_message.state == LXMF.LXMessage.SENT + and lxmf_message.method == LXMF.LXMessage.PROPAGATED + ) + has_failed = lxmf_message.state == LXMF.LXMessage.FAILED + is_cancelled = lxmf_message.state == LXMF.LXMessage.CANCELLED + + # check if we should stop updating + if has_delivered or has_propagated or has_failed or is_cancelled: + should_update_message = False + + # handle an announce received from reticulum, for a telephone address + # NOTE: cant be async, as Reticulum doesn't await it + def on_telephone_announce_received( + self, + aspect, + destination_hash, + announced_identity, + app_data, + announce_packet_hash, + ): + # check if source is blocked - drop announce and path if blocked + identity_hash = announced_identity.hash.hex() + if self.is_destination_blocked(identity_hash): + print(f"Dropping telephone announce from blocked source: {identity_hash}") + self.reticulum.drop_path(destination_hash) + return + + # log received announce + print( + "Received an announce from " + + RNS.prettyhexrep(destination_hash) + + " for [lxst.telephony]", + ) + + # track announce timestamp + self.announce_timestamps.append(time.time()) + + # upsert announce to database + self.announce_manager.upsert_announce( + self.reticulum, + announced_identity, + destination_hash, + aspect, + app_data, + announce_packet_hash, + ) + + # find announce from database + announce = self.database.announces.get_announce_by_hash(destination_hash.hex()) + if not announce: + return + + # send database announce to all websocket clients + AsyncUtils.run_async( + self.websocket_broadcast( + json.dumps( + { + "type": "announce", + "announce": self.convert_db_announce_to_dict(announce), + }, + ), + ), + ) + + # handle an announce received from reticulum, for an lxmf address + # NOTE: cant be async, as Reticulum doesn't await it + def on_lxmf_announce_received( + self, + aspect, + destination_hash, + announced_identity, + app_data, + announce_packet_hash, + ): + # check if source is blocked - drop announce and path if blocked + identity_hash = announced_identity.hash.hex() + if self.is_destination_blocked(identity_hash): + print(f"Dropping announce from blocked source: {identity_hash}") + self.reticulum.drop_path(destination_hash) + return + + # log received announce + print( + "Received an announce from " + + RNS.prettyhexrep(destination_hash) + + " for [lxmf.delivery]", + ) + + # track announce timestamp + self.announce_timestamps.append(time.time()) + + # upsert announce to database + self.announce_manager.upsert_announce( + self.reticulum, + announced_identity, + destination_hash, + aspect, + app_data, + announce_packet_hash, + ) + + # find announce from database + announce = self.database.announces.get_announce_by_hash(destination_hash.hex()) + if not announce: + return + + # send database announce to all websocket clients + AsyncUtils.run_async( + self.websocket_broadcast( + json.dumps( + { + "type": "announce", + "announce": self.convert_db_announce_to_dict(announce), + }, + ), + ), + ) + + # resend all failed messages that were intended for this destination + if self.config.auto_resend_failed_messages_when_announce_received.get(): + AsyncUtils.run_async( + self.resend_failed_messages_for_destination(destination_hash.hex()), + ) + + # handle an announce received from reticulum, for an lxmf propagation node address + # NOTE: cant be async, as Reticulum doesn't await it + def on_lxmf_propagation_announce_received( + self, + aspect, + destination_hash, + announced_identity, + app_data, + announce_packet_hash, + ): + # log received announce + print( + "Received an announce from " + + RNS.prettyhexrep(destination_hash) + + " for [lxmf.propagation]", + ) + + # track announce timestamp + self.announce_timestamps.append(time.time()) + + # upsert announce to database + self.announce_manager.upsert_announce( + self.reticulum, + announced_identity, + destination_hash, + aspect, + app_data, + announce_packet_hash, + ) + + # find announce from database + announce = self.database.announces.get_announce_by_hash(destination_hash.hex()) + if not announce: + return + + # send database announce to all websocket clients + AsyncUtils.run_async( + self.websocket_broadcast( + json.dumps( + { + "type": "announce", + "announce": self.convert_db_announce_to_dict(announce), + }, + ), + ), + ) + + # resends all messages that previously failed to send to the provided destination hash + async def resend_failed_messages_for_destination(self, destination_hash: str): + # get messages that failed to send to this destination + failed_messages = self.database.messages.get_failed_messages_for_destination(destination_hash) + + # resend failed messages + for failed_message in failed_messages: + try: + # parse fields as json + fields = json.loads(failed_message["fields"]) + + # parse image field + image_field = None + if "image" in fields: + image_field = LxmfImageField( + fields["image"]["image_type"], + base64.b64decode(fields["image"]["image_bytes"]), + ) + + # parse audio field + audio_field = None + if "audio" in fields: + audio_field = LxmfAudioField( + fields["audio"]["audio_mode"], + base64.b64decode(fields["audio"]["audio_bytes"]), + ) + + # parse file attachments field + file_attachments_field = None + if "file_attachments" in fields: + file_attachments = [ + LxmfFileAttachment( + file_attachment["file_name"], + base64.b64decode(file_attachment["file_bytes"]), + ) + for file_attachment in fields["file_attachments"] + ] + file_attachments_field = LxmfFileAttachmentsField(file_attachments) + + # don't resend message with attachments if not allowed + if not self.config.allow_auto_resending_failed_messages_with_attachments.get(): + if ( + image_field is not None + or audio_field is not None + or file_attachments_field is not None + ): + print( + "Not resending failed message with attachments, as setting is disabled", + ) + continue + + # send new message with failed message content + await self.send_message( + failed_message["destination_hash"], + failed_message["content"], + image_field=image_field, + audio_field=audio_field, + file_attachments_field=file_attachments_field, + ) + + # remove original failed message from database + self.database.messages.delete_lxmf_message_by_hash(failed_message["hash"]) + + # tell all websocket clients that old failed message was deleted so it can remove from ui + await self.websocket_broadcast( + json.dumps( + { + "type": "lxmf_message_deleted", + "hash": failed_message["hash"], + }, + ), + ) + + except Exception as e: + print("Error resending failed message: " + str(e)) + + # handle an announce received from reticulum, for a nomadnet node + # NOTE: cant be async, as Reticulum doesn't await it + def on_nomadnet_node_announce_received( + self, + aspect, + destination_hash, + announced_identity, + app_data, + announce_packet_hash, + ): + # check if source is blocked - drop announce and path if blocked + identity_hash = announced_identity.hash.hex() + if self.is_destination_blocked(identity_hash): + print(f"Dropping announce from blocked source: {identity_hash}") + self.reticulum.drop_path(destination_hash) + return + + # log received announce + print( + "Received an announce from " + + RNS.prettyhexrep(destination_hash) + + " for [nomadnetwork.node]", + ) + + # track announce timestamp + self.announce_timestamps.append(time.time()) + + # upsert announce to database + self.announce_manager.upsert_announce( + self.reticulum, + announced_identity, + destination_hash, + aspect, + app_data, + announce_packet_hash, + ) + + # find announce from database + announce = self.database.announces.get_announce_by_hash(destination_hash.hex()) + if announce is None: + return + + # send database announce to all websocket clients + AsyncUtils.run_async( + self.websocket_broadcast( + json.dumps( + { + "type": "announce", + "announce": self.convert_db_announce_to_dict(announce), + }, + ), + ), + ) + + # queue crawler task (existence check in queue_crawler_task handles duplicates) + self.queue_crawler_task(destination_hash.hex(), "/page/index.mu") + + # queues a crawler task for the provided destination and path + def queue_crawler_task(self, destination_hash: str, page_path: str): + self.database.misc.upsert_crawl_task(destination_hash, page_path) + + # gets the custom display name a user has set for the provided destination hash + def get_custom_destination_display_name(self, destination_hash: str): + db_destination_display_name = self.database.announces.get_custom_display_name(destination_hash) + if db_destination_display_name is not None: + return db_destination_display_name.display_name + + return None + + # get name to show for an lxmf conversation + # currently, this will use the app data from the most recent announce + # TODO: we should fetch this from our contacts database, when it gets implemented, and if not found, fallback to app data + def get_lxmf_conversation_name(self, destination_hash, default_name: str | None = "Anonymous Peer"): + # get lxmf.delivery announce from database for the provided destination hash + results = self.database.announces.get_announces(aspect="lxmf.delivery") + lxmf_announce = next((a for a in results if a["destination_hash"] == destination_hash), None) + + # if app data is available in database, it should be base64 encoded text that was announced + # we will return the parsed lxmf display name as the conversation name + if lxmf_announce is not None and lxmf_announce["app_data"] is not None: + return ReticulumMeshChat.parse_lxmf_display_name( + app_data_base64=lxmf_announce["app_data"], + ) + + # announce did not have app data, so provide a fallback name + return default_name + + # reads the lxmf display name from the provided base64 app data + @staticmethod + def parse_lxmf_display_name( + app_data_base64: str | None, + default_value: str | None = "Anonymous Peer", + ): + if app_data_base64 is None: + return default_value + + try: + app_data_bytes = base64.b64decode(app_data_base64) + display_name = LXMF.display_name_from_app_data(app_data_bytes) + if display_name is not None: + return display_name + except Exception as e: + print(f"Failed to parse LXMF display name: {e}") + + return default_value + + # reads the lxmf stamp cost from the provided base64 app data + @staticmethod + def parse_lxmf_stamp_cost(app_data_base64: str | None): + if app_data_base64 is None: + return None + + try: + app_data_bytes = base64.b64decode(app_data_base64) + return LXMF.stamp_cost_from_app_data(app_data_bytes) + except Exception as e: + print(f"Failed to parse LXMF stamp cost: {e}") + return None + + # reads the nomadnetwork node display name from the provided base64 app data + @staticmethod + def parse_nomadnetwork_node_display_name( + app_data_base64: str | None, + default_value: str | None = "Anonymous Node", + ): + if app_data_base64 is None: + return default_value + + try: + app_data_bytes = base64.b64decode(app_data_base64) + return app_data_bytes.decode("utf-8") + except Exception as e: + print(f"Failed to parse NomadNetwork display name: {e}") + return default_value + + # parses lxmf propagation node app data + @staticmethod + def parse_lxmf_propagation_node_app_data(app_data_base64: str | None): + if app_data_base64 is None: + return None + + try: + app_data_bytes = base64.b64decode(app_data_base64) + data = msgpack.unpackb(app_data_bytes) + + # ensure data is a list and has enough elements + if not isinstance(data, list) or len(data) < 4: + return None + + return { + "enabled": bool(data[2]) if data[2] is not None else False, + "timebase": int(data[1]) if data[1] is not None else 0, + "per_transfer_limit": int(data[3]) if data[3] is not None else 0, + } + except Exception as e: + print(f"Failed to parse LXMF propagation node app data: {e}") + return None + + # returns true if the conversation has messages newer than the last read at timestamp + @staticmethod + def is_lxmf_conversation_unread(self, destination_hash): + return self.database.messages.is_conversation_unread(destination_hash) + + # returns number of messages that failed to send in a conversation + def lxmf_conversation_failed_messages_count(self, destination_hash: str): + return self.database.messages.get_failed_messages_count(destination_hash) + + # find an interface by name + @staticmethod + def find_interface_by_name(name: str): + for interface in RNS.Transport.interfaces: + interface_name = str(interface) + if name == interface_name: + return interface + + return None + + +# class to manage config stored in database +# FIXME: we should probably set this as an instance variable of ReticulumMeshChat so it has a proper home, and pass it in to the constructor? +nomadnet_cached_links = {} + + +class NomadnetDownloader: + def __init__( + self, + destination_hash: bytes, + path: str, + data: str | None, + on_download_success: Callable[[RNS.RequestReceipt], None], + on_download_failure: Callable[[str], None], + on_progress_update: Callable[[float], None], + timeout: int | None = None, + ): + self.app_name = "nomadnetwork" + self.aspects = "node" + self.destination_hash = destination_hash + self.path = path + self.data = data + self.timeout = timeout + self._download_success_callback = on_download_success + self._download_failure_callback = on_download_failure + self.on_progress_update = on_progress_update + self.request_receipt = None + self.is_cancelled = False + self.link = None + + # cancel the download + def cancel(self): + self.is_cancelled = True + + # cancel the request if it exists + if self.request_receipt is not None: + try: + self.request_receipt.cancel() + except Exception as e: + print(f"Failed to cancel request: {e}") + + # clean up the link if we created it + if self.link is not None: + try: + self.link.teardown() + except Exception as e: + print(f"Failed to teardown link: {e}") + + # notify that download was cancelled + self._download_failure_callback("cancelled") + + # setup link to destination and request download + async def download( + self, + path_lookup_timeout: int = 15, + link_establishment_timeout: int = 15, + ): + # check if cancelled before starting + if self.is_cancelled: + return + + # use existing established link if it's active + if self.destination_hash in nomadnet_cached_links: + link = nomadnet_cached_links[self.destination_hash] + if link.status is RNS.Link.ACTIVE: + print("[NomadnetDownloader] using existing link for request") + self.link_established(link) + return + + # determine when to timeout + timeout_after_seconds = time.time() + path_lookup_timeout + + # check if we have a path to the destination + if not RNS.Transport.has_path(self.destination_hash): + # we don't have a path, so we need to request it + RNS.Transport.request_path(self.destination_hash) + + # wait until we have a path, or give up after the configured timeout + while ( + not RNS.Transport.has_path(self.destination_hash) + and time.time() < timeout_after_seconds + ): + # check if cancelled during path lookup + if self.is_cancelled: + return + await asyncio.sleep(0.1) + + # if we still don't have a path, we can't establish a link, so bail out + if not RNS.Transport.has_path(self.destination_hash): + self._download_failure_callback("Could not find path to destination.") + return + + # check if cancelled before establishing link + if self.is_cancelled: + return + + # create destination to nomadnet node + identity = RNS.Identity.recall(self.destination_hash) + destination = RNS.Destination( + identity, + RNS.Destination.OUT, + RNS.Destination.SINGLE, + self.app_name, + self.aspects, + ) + + # create link to destination + print("[NomadnetDownloader] establishing new link for request") + link = RNS.Link(destination, established_callback=self.link_established) + self.link = link + + # determine when to timeout + timeout_after_seconds = time.time() + link_establishment_timeout + + # wait until we have established a link, or give up after the configured timeout + while ( + link.status is not RNS.Link.ACTIVE and time.time() < timeout_after_seconds + ): + # check if cancelled during link establishment + if self.is_cancelled: + return + await asyncio.sleep(0.1) + + # if we still haven't established a link, bail out + if link.status is not RNS.Link.ACTIVE: + self._download_failure_callback("Could not establish link to destination.") + + # link to destination was established, we should now request the download + def link_established(self, link): + # check if cancelled before requesting + if self.is_cancelled: + return + + # cache link for using in future requests + nomadnet_cached_links[self.destination_hash] = link + + # request download over link + self.request_receipt = link.request( + self.path, + data=self.data, + response_callback=self.on_response, + failed_callback=self.on_failed, + progress_callback=self.on_progress, + timeout=self.timeout, + ) + + # handle successful download + def on_response(self, request_receipt: RNS.RequestReceipt): + self._download_success_callback(request_receipt) + + # handle failure + def on_failed(self, request_receipt=None): + self._download_failure_callback("request_failed") + + # handle download progress + def on_progress(self, request_receipt): + self.on_progress_update(request_receipt.progress) + + +class NomadnetPageDownloader(NomadnetDownloader): + def __init__( + self, + destination_hash: bytes, + page_path: str, + data: str | None, + on_page_download_success: Callable[[str], None], + on_page_download_failure: Callable[[str], None], + on_progress_update: Callable[[float], None], + timeout: int | None = None, + ): + self.on_page_download_success = on_page_download_success + self.on_page_download_failure = on_page_download_failure + super().__init__( + destination_hash, + page_path, + data, + self.on_download_success, + self.on_download_failure, + on_progress_update, + timeout, + ) + + # page download was successful, decode the response and send to provided callback + def on_download_success(self, request_receipt: RNS.RequestReceipt): + micron_markup_response = request_receipt.response.decode("utf-8") + self.on_page_download_success(micron_markup_response) + + # page download failed, send error to provided callback + def on_download_failure(self, failure_reason): + self.on_page_download_failure(failure_reason) + + +class NomadnetFileDownloader(NomadnetDownloader): + def __init__( + self, + destination_hash: bytes, + page_path: str, + on_file_download_success: Callable[[str, bytes], None], + on_file_download_failure: Callable[[str], None], + on_progress_update: Callable[[float], None], + timeout: int | None = None, + ): + self.on_file_download_success = on_file_download_success + self.on_file_download_failure = on_file_download_failure + super().__init__( + destination_hash, + page_path, + None, + self.on_download_success, + self.on_download_failure, + on_progress_update, + timeout, + ) + + # file download was successful, decode the response and send to provided callback + def on_download_success(self, request_receipt: RNS.RequestReceipt): + # get response + response = request_receipt.response + + # handle buffered reader response + if isinstance(response, io.BufferedReader): + # get file name from metadata + file_name = "downloaded_file" + metadata = request_receipt.metadata + if metadata is not None and "name" in metadata: + file_path = metadata["name"].decode("utf-8") + file_name = os.path.basename(file_path) + + # get file data + file_data: bytes = response.read() + + self.on_file_download_success(file_name, file_data) + return + + # check for list response with bytes in position 0, and metadata dict in position 1 + # e.g: [file_bytes, {name: "filename.ext"}] + if isinstance(response, list) and isinstance(response[1], dict): + file_data: bytes = response[0] + metadata: dict = response[1] + + # get file name from metadata + file_name = "downloaded_file" + if metadata is not None and "name" in metadata: + file_path = metadata["name"].decode("utf-8") + file_name = os.path.basename(file_path) + + self.on_file_download_success(file_name, file_data) + return + + # try using original response format + # unsure if this is actually used anymore now that a buffered reader is provided + # have left here just in case... + try: + file_name: str = response[0] + file_data: bytes = response[1] + self.on_file_download_success(file_name, file_data) + except Exception: + self.on_download_failure("unsupported_response") + + # page download failed, send error to provided callback + def on_download_failure(self, failure_reason): + self.on_file_download_failure(failure_reason) + + +def main(): + # parse command line args + parser = argparse.ArgumentParser(description="ReticulumMeshChat") + parser.add_argument( + "--host", + nargs="?", + default="127.0.0.1", + type=str, + help="The address the web server should listen on.", + ) + parser.add_argument( + "--port", + nargs="?", + default="8000", + type=int, + help="The port the web server should listen on.", + ) + parser.add_argument( + "--headless", + action="store_true", + help="Web browser will not automatically launch when this flag is passed.", + ) + parser.add_argument( + "--identity-file", + type=str, + help="Path to a Reticulum Identity file to use as your LXMF address.", + ) + parser.add_argument( + "--identity-base64", + type=str, + help="A base64 encoded Reticulum Identity to use as your LXMF address.", + ) + parser.add_argument( + "--identity-base32", + type=str, + help="A base32 encoded Reticulum Identity to use as your LXMF address.", + ) + parser.add_argument( + "--generate-identity-file", + type=str, + help="Generates and saves a new Reticulum Identity to the provided file path and then exits.", + ) + parser.add_argument( + "--generate-identity-base64", + action="store_true", + help="Outputs a randomly generated Reticulum Identity as base64 and then exits.", + ) + parser.add_argument( + "--auto-recover", + action="store_true", + help="Attempt to automatically recover the SQLite database on startup before serving the app.", + ) + parser.add_argument( + "--auth", + action="store_true", + help="Enable basic authentication for the web interface.", + ) + parser.add_argument( + "--no-https", + action="store_true", + help="Disable HTTPS and use HTTP instead.", + ) + parser.add_argument( + "--backup-db", + type=str, + help="Create a database backup zip at the given path and exit.", + ) + parser.add_argument( + "--restore-db", + type=str, + help="Restore the database from the given path (zip or db file) and exit.", + ) + parser.add_argument( + "--reticulum-config-dir", + type=str, + help="Path to a Reticulum config directory for the RNS stack to use (e.g: ~/.reticulum)", + ) + parser.add_argument( + "--storage-dir", + type=str, + help="Path to a directory for storing databases and config files (default: ./storage)", + ) + parser.add_argument( + "--test-exception-message", + type=str, + help="Throws an exception. Used for testing the electron error dialog", + ) + parser.add_argument( + "args", + nargs=argparse.REMAINDER, + ) # allow unknown command line args + args = parser.parse_args() + + # check if we want to test exception messages + if args.test_exception_message is not None: + raise Exception(args.test_exception_message) + + # util to generate reticulum identity and save to file without using rnid + if args.generate_identity_file is not None: + # do not overwrite existing files, otherwise user could lose existing keys + if os.path.exists(args.generate_identity_file): + print( + "DANGER: the provided identity file path already exists, not overwriting!", + ) + return + + # generate a new identity and save to provided file path + identity = RNS.Identity(create_keys=True) + with open(args.generate_identity_file, "wb") as file: + file.write(identity.get_private_key()) + + print( + f"A new Reticulum Identity has been saved to: {args.generate_identity_file}", + ) + return + + # util to generate reticulum identity as base64 without using rnid + if args.generate_identity_base64 is True: + identity = RNS.Identity(create_keys=True) + print(base64.b64encode(identity.get_private_key()).decode("utf-8")) + return + + identity_file_path = None + + # use provided identity, or fallback to a random one + if args.identity_file is not None: + identity = RNS.Identity(create_keys=False) + identity.load(args.identity_file) + identity_file_path = args.identity_file + print( + f"Reticulum Identity <{identity.hash.hex()}> has been loaded from file {args.identity_file}.", + ) + elif args.identity_base64 is not None or args.identity_base32 is not None: + identity = RNS.Identity(create_keys=False) + if args.identity_base64 is not None: + identity.load_private_key(base64.b64decode(args.identity_base64)) + else: + try: + identity.load_private_key( + base64.b32decode(args.identity_base32, casefold=True), + ) + except Exception as exc: + msg = f"Invalid base32 identity: {exc}" + raise ValueError(msg) from exc + base_storage_dir = args.storage_dir or os.path.join("storage") + os.makedirs(base_storage_dir, exist_ok=True) + default_identity_file = os.path.join(base_storage_dir, "identity") + if not os.path.exists(default_identity_file): + with open(default_identity_file, "wb") as file: + file.write(identity.get_private_key()) + identity_file_path = default_identity_file + print( + f"Reticulum Identity <{identity.hash.hex()}> has been loaded from provided key.", + ) + else: + # ensure provided storage dir exists, or the default storage dir exists + base_storage_dir = args.storage_dir or os.path.join("storage") + os.makedirs(base_storage_dir, exist_ok=True) + + # configure path to default identity file + default_identity_file = os.path.join(base_storage_dir, "identity") + + # if default identity file does not exist, generate a new identity and save it + if not os.path.exists(default_identity_file): + identity = RNS.Identity(create_keys=True) + with open(default_identity_file, "wb") as file: + file.write(identity.get_private_key()) + print( + f"Reticulum Identity <{identity.hash.hex()}> has been randomly generated and saved to {default_identity_file}.", + ) + + # default identity file exists, load it + identity = RNS.Identity(create_keys=False) + identity.load(default_identity_file) + identity_file_path = default_identity_file + print( + f"Reticulum Identity <{identity.hash.hex()}> has been loaded from file {default_identity_file}.", + ) + + # init app (allow optional one-shot backup/restore before running) + reticulum_meshchat = ReticulumMeshChat( + identity, + args.storage_dir, + args.reticulum_config_dir, + auto_recover=args.auto_recover, + identity_file_path=identity_file_path, + auth_enabled=args.auth, + ) + + if args.backup_db: + result = reticulum_meshchat.backup_database(args.backup_db) + print(f"Backup written to {result['path']} ({result['size']} bytes)") + return + + if args.restore_db: + result = reticulum_meshchat.restore_database(args.restore_db) + print(f"Restored database from {args.restore_db}") + print(f"Integrity check: {result['integrity_check']}") + return + + enable_https = not args.no_https + reticulum_meshchat.run(args.host, args.port, launch_browser=args.headless is False, enable_https=enable_https) + + +if __name__ == "__main__": + main() diff --git a/meshchatx/src/__init__.py b/meshchatx/src/__init__.py new file mode 100644 index 0000000..ca25e9c --- /dev/null +++ b/meshchatx/src/__init__.py @@ -0,0 +1,29 @@ +import sys + +# NOTE: this class is required to be able to use print/log commands and have them flush to stdout and stderr immediately +# without wrapper stdout and stderr, when using `childProcess.stdout.on('data', ...)` in NodeJS script, we never get +# any events fired until the process exits. However, force flushing the streams does fire the callbacks in NodeJS. + + +# this class forces stream writes to be flushed immediately +class ImmediateFlushingStreamWrapper: + def __init__(self, stream): + self.stream = stream + + # force write to flush immediately + def write(self, data): + self.stream.write(data) + self.stream.flush() + + # force writelines to flush immediately + def writelines(self, lines): + self.stream.writelines(lines) + self.stream.flush() + + def __getattr__(self, attr): + return getattr(self.stream, attr) + + +# wrap stdout and stderr with our custom wrapper +sys.stdout = ImmediateFlushingStreamWrapper(sys.stdout) +sys.stderr = ImmediateFlushingStreamWrapper(sys.stderr) diff --git a/meshchatx/src/backend/__init__.py b/meshchatx/src/backend/__init__.py new file mode 100644 index 0000000..a844222 --- /dev/null +++ b/meshchatx/src/backend/__init__.py @@ -0,0 +1 @@ +"""Backend utilities shared by the Reticulum MeshChatX CLI.""" diff --git a/meshchatx/src/backend/announce_handler.py b/meshchatx/src/backend/announce_handler.py new file mode 100644 index 0000000..0f2d468 --- /dev/null +++ b/meshchatx/src/backend/announce_handler.py @@ -0,0 +1,27 @@ +# an announce handler that forwards announces to a provided callback for the provided aspect filter +# this handler exists so we can have access to the original aspect, as this is not provided in the announce itself +class AnnounceHandler: + def __init__(self, aspect_filter: str, received_announce_callback): + self.aspect_filter = aspect_filter + self.received_announce_callback = received_announce_callback + + # we will just pass the received announce back to the provided callback + def received_announce( + self, + destination_hash, + announced_identity, + app_data, + announce_packet_hash, + ): + try: + # handle received announce + self.received_announce_callback( + self.aspect_filter, + destination_hash, + announced_identity, + app_data, + announce_packet_hash, + ) + except Exception as e: + # ignore failure to handle received announce + print(f"Failed to handle received announce: {e}") diff --git a/meshchatx/src/backend/announce_manager.py b/meshchatx/src/backend/announce_manager.py new file mode 100644 index 0000000..6a9b4a7 --- /dev/null +++ b/meshchatx/src/backend/announce_manager.py @@ -0,0 +1,59 @@ +import base64 + +from .database import Database + + +class AnnounceManager: + def __init__(self, db: Database): + self.db = db + + def upsert_announce(self, reticulum, identity, destination_hash, aspect, app_data, announce_packet_hash): + # get rssi, snr and signal quality if available + rssi = reticulum.get_packet_rssi(announce_packet_hash) + snr = reticulum.get_packet_snr(announce_packet_hash) + quality = reticulum.get_packet_q(announce_packet_hash) + + # prepare data to insert or update + data = { + "destination_hash": destination_hash.hex() if isinstance(destination_hash, bytes) else destination_hash, + "aspect": aspect, + "identity_hash": identity.hash.hex(), + "identity_public_key": base64.b64encode(identity.get_public_key()).decode( + "utf-8", + ), + "rssi": rssi, + "snr": snr, + "quality": quality, + } + + # only set app data if provided + if app_data is not None: + data["app_data"] = base64.b64encode(app_data).decode("utf-8") + + self.db.announces.upsert_announce(data) + + def get_filtered_announces(self, aspect=None, identity_hash=None, destination_hash=None, query=None, blocked_identity_hashes=None): + sql = "SELECT * FROM announces WHERE 1=1" + params = [] + + if aspect: + sql += " AND aspect = ?" + params.append(aspect) + if identity_hash: + sql += " AND identity_hash = ?" + params.append(identity_hash) + if destination_hash: + sql += " AND destination_hash = ?" + params.append(destination_hash) + if query: + like_term = f"%{query}%" + sql += " AND (destination_hash LIKE ? OR identity_hash LIKE ?)" + params.extend([like_term, like_term]) + if blocked_identity_hashes: + placeholders = ", ".join(["?"] * len(blocked_identity_hashes)) + sql += f" AND identity_hash NOT IN ({placeholders})" + params.extend(blocked_identity_hashes) + + sql += " ORDER BY updated_at DESC" + return self.db.provider.fetchall(sql, params) + diff --git a/meshchatx/src/backend/archiver_manager.py b/meshchatx/src/backend/archiver_manager.py new file mode 100644 index 0000000..94dadfa --- /dev/null +++ b/meshchatx/src/backend/archiver_manager.py @@ -0,0 +1,44 @@ +import hashlib + +from .database import Database + + +class ArchiverManager: + def __init__(self, db: Database): + self.db = db + + def archive_page(self, destination_hash, page_path, content, max_versions=5, max_storage_gb=1): + content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest() + + # Check if already exists + existing = self.db.provider.fetchone( + "SELECT id FROM archived_pages WHERE destination_hash = ? AND page_path = ? AND hash = ?", + (destination_hash, page_path, content_hash), + ) + if existing: + return + + # Insert new version + self.db.misc.archive_page(destination_hash, page_path, content, content_hash) + + # Enforce max versions per page + versions = self.db.misc.get_archived_page_versions(destination_hash, page_path) + if len(versions) > max_versions: + # Delete older versions + to_delete = versions[max_versions:] + for version in to_delete: + self.db.provider.execute("DELETE FROM archived_pages WHERE id = ?", (version["id"],)) + + # Enforce total storage limit (approximate) + total_size_row = self.db.provider.fetchone("SELECT SUM(LENGTH(content)) as total_size FROM archived_pages") + total_size = total_size_row["total_size"] or 0 + max_bytes = max_storage_gb * 1024 * 1024 * 1024 + + while total_size > max_bytes: + oldest = self.db.provider.fetchone("SELECT id, LENGTH(content) as size FROM archived_pages ORDER BY created_at ASC LIMIT 1") + if oldest: + self.db.provider.execute("DELETE FROM archived_pages WHERE id = ?", (oldest["id"],)) + total_size -= oldest["size"] + else: + break + diff --git a/meshchatx/src/backend/async_utils.py b/meshchatx/src/backend/async_utils.py new file mode 100644 index 0000000..200dfc8 --- /dev/null +++ b/meshchatx/src/backend/async_utils.py @@ -0,0 +1,23 @@ +import asyncio +from collections.abc import Coroutine + + +class AsyncUtils: + # remember main loop + main_loop: asyncio.AbstractEventLoop | None = None + + @staticmethod + def set_main_loop(loop: asyncio.AbstractEventLoop): + AsyncUtils.main_loop = loop + + # this method allows running the provided async coroutine from within a sync function + # it will run the async function on the main event loop if possible, otherwise it logs a warning + @staticmethod + def run_async(coroutine: Coroutine): + # run provided coroutine on main event loop, ensuring thread safety + if AsyncUtils.main_loop and AsyncUtils.main_loop.is_running(): + asyncio.run_coroutine_threadsafe(coroutine, AsyncUtils.main_loop) + return + + # main event loop not running... + print("WARNING: Main event loop not available. Could not schedule task.") diff --git a/meshchatx/src/backend/colour_utils.py b/meshchatx/src/backend/colour_utils.py new file mode 100644 index 0000000..f1ae02d --- /dev/null +++ b/meshchatx/src/backend/colour_utils.py @@ -0,0 +1,8 @@ +class ColourUtils: + @staticmethod + def hex_colour_to_byte_array(hex_colour): + # remove leading "#" + hex_colour = hex_colour.lstrip("#") + + # convert the remaining hex string to bytes + return bytes.fromhex(hex_colour) diff --git a/meshchatx/src/backend/config_manager.py b/meshchatx/src/backend/config_manager.py new file mode 100644 index 0000000..e7bc911 --- /dev/null +++ b/meshchatx/src/backend/config_manager.py @@ -0,0 +1,131 @@ + +class ConfigManager: + def __init__(self, db): + self.db = db + + # all possible config items + self.database_version = self.IntConfig(self, "database_version", None) + self.display_name = self.StringConfig(self, "display_name", "Anonymous Peer") + self.auto_announce_enabled = self.BoolConfig(self, "auto_announce_enabled", False) + self.auto_announce_interval_seconds = self.IntConfig(self, "auto_announce_interval_seconds", 0) + self.last_announced_at = self.IntConfig(self, "last_announced_at", None) + self.theme = self.StringConfig(self, "theme", "light") + self.language = self.StringConfig(self, "language", "en") + self.auto_resend_failed_messages_when_announce_received = self.BoolConfig( + self, "auto_resend_failed_messages_when_announce_received", True, + ) + self.allow_auto_resending_failed_messages_with_attachments = self.BoolConfig( + self, "allow_auto_resending_failed_messages_with_attachments", False, + ) + self.auto_send_failed_messages_to_propagation_node = self.BoolConfig( + self, "auto_send_failed_messages_to_propagation_node", False, + ) + self.show_suggested_community_interfaces = self.BoolConfig( + self, "show_suggested_community_interfaces", True, + ) + self.lxmf_delivery_transfer_limit_in_bytes = self.IntConfig( + self, "lxmf_delivery_transfer_limit_in_bytes", 1000 * 1000 * 10, + ) # 10MB + self.lxmf_preferred_propagation_node_destination_hash = self.StringConfig( + self, "lxmf_preferred_propagation_node_destination_hash", None, + ) + self.lxmf_preferred_propagation_node_auto_sync_interval_seconds = self.IntConfig( + self, "lxmf_preferred_propagation_node_auto_sync_interval_seconds", 0, + ) + self.lxmf_preferred_propagation_node_last_synced_at = self.IntConfig( + self, "lxmf_preferred_propagation_node_last_synced_at", None, + ) + self.lxmf_local_propagation_node_enabled = self.BoolConfig( + self, "lxmf_local_propagation_node_enabled", False, + ) + self.lxmf_user_icon_name = self.StringConfig(self, "lxmf_user_icon_name", None) + self.lxmf_user_icon_foreground_colour = self.StringConfig( + self, "lxmf_user_icon_foreground_colour", None, + ) + self.lxmf_user_icon_background_colour = self.StringConfig( + self, "lxmf_user_icon_background_colour", None, + ) + self.lxmf_inbound_stamp_cost = self.IntConfig( + self, "lxmf_inbound_stamp_cost", 8, + ) # for direct delivery messages + self.lxmf_propagation_node_stamp_cost = self.IntConfig( + self, "lxmf_propagation_node_stamp_cost", 16, + ) # for propagation node messages + self.page_archiver_enabled = self.BoolConfig(self, "page_archiver_enabled", True) + self.page_archiver_max_versions = self.IntConfig(self, "page_archiver_max_versions", 5) + self.archives_max_storage_gb = self.IntConfig(self, "archives_max_storage_gb", 1) + self.crawler_enabled = self.BoolConfig(self, "crawler_enabled", False) + self.crawler_max_retries = self.IntConfig(self, "crawler_max_retries", 3) + self.crawler_retry_delay_seconds = self.IntConfig(self, "crawler_retry_delay_seconds", 3600) + self.crawler_max_concurrent = self.IntConfig(self, "crawler_max_concurrent", 1) + self.auth_enabled = self.BoolConfig(self, "auth_enabled", False) + self.auth_password_hash = self.StringConfig(self, "auth_password_hash", None) + self.auth_session_secret = self.StringConfig(self, "auth_session_secret", None) + + # map config + self.map_offline_enabled = self.BoolConfig(self, "map_offline_enabled", False) + self.map_offline_path = self.StringConfig(self, "map_offline_path", None) + self.map_mbtiles_dir = self.StringConfig(self, "map_mbtiles_dir", None) + self.map_tile_cache_enabled = self.BoolConfig(self, "map_tile_cache_enabled", True) + self.map_default_lat = self.StringConfig(self, "map_default_lat", "0.0") + self.map_default_lon = self.StringConfig(self, "map_default_lon", "0.0") + self.map_default_zoom = self.IntConfig(self, "map_default_zoom", 2) + self.map_tile_server_url = self.StringConfig( + self, "map_tile_server_url", "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + ) + self.map_nominatim_api_url = self.StringConfig( + self, "map_nominatim_api_url", "https://nominatim.openstreetmap.org", + ) + + def get(self, key: str, default_value=None) -> str | None: + return self.db.config.get(key, default_value) + + def set(self, key: str, value: str | None): + self.db.config.set(key, value) + + class StringConfig: + def __init__(self, manager, key: str, default_value: str | None = None): + self.manager = manager + self.key = key + self.default_value = default_value + + def get(self, default_value: str = None) -> str | None: + _default_value = default_value or self.default_value + return self.manager.get(self.key, default_value=_default_value) + + def set(self, value: str | None): + self.manager.set(self.key, value) + + class BoolConfig: + def __init__(self, manager, key: str, default_value: bool = False): + self.manager = manager + self.key = key + self.default_value = default_value + + def get(self) -> bool: + config_value = self.manager.get(self.key, default_value=None) + if config_value is None: + return self.default_value + return config_value == "true" + + def set(self, value: bool): + self.manager.set(self.key, "true" if value else "false") + + class IntConfig: + def __init__(self, manager, key: str, default_value: int | None = 0): + self.manager = manager + self.key = key + self.default_value = default_value + + def get(self) -> int | None: + config_value = self.manager.get(self.key, default_value=None) + if config_value is None: + return self.default_value + try: + return int(config_value) + except (ValueError, TypeError): + return self.default_value + + def set(self, value: int): + self.manager.set(self.key, str(value)) + diff --git a/meshchatx/src/backend/database/__init__.py b/meshchatx/src/backend/database/__init__.py new file mode 100644 index 0000000..afc943d --- /dev/null +++ b/meshchatx/src/backend/database/__init__.py @@ -0,0 +1,35 @@ +from .announces import AnnounceDAO +from .config import ConfigDAO +from .legacy_migrator import LegacyMigrator +from .messages import MessageDAO +from .misc import MiscDAO +from .provider import DatabaseProvider +from .schema import DatabaseSchema +from .telephone import TelephoneDAO + + +class Database: + def __init__(self, db_path): + self.provider = DatabaseProvider.get_instance(db_path) + self.schema = DatabaseSchema(self.provider) + self.config = ConfigDAO(self.provider) + self.messages = MessageDAO(self.provider) + self.announces = AnnounceDAO(self.provider) + self.misc = MiscDAO(self.provider) + self.telephone = TelephoneDAO(self.provider) + + def initialize(self): + self.schema.initialize() + + def migrate_from_legacy(self, reticulum_config_dir, identity_hash_hex): + migrator = LegacyMigrator(self.provider, reticulum_config_dir, identity_hash_hex) + if migrator.should_migrate(): + return migrator.migrate() + return False + + def execute_sql(self, query, params=None): + return self.provider.execute(query, params) + + def close(self): + self.provider.close() + diff --git a/meshchatx/src/backend/database/announces.py b/meshchatx/src/backend/database/announces.py new file mode 100644 index 0000000..93bfd8e --- /dev/null +++ b/meshchatx/src/backend/database/announces.py @@ -0,0 +1,90 @@ +from datetime import UTC, datetime + +from .provider import DatabaseProvider + + +class AnnounceDAO: + def __init__(self, provider: DatabaseProvider): + self.provider = provider + + def upsert_announce(self, data): + # Ensure data is a dict if it's a sqlite3.Row + if not isinstance(data, dict): + data = dict(data) + + fields = [ + "destination_hash", "aspect", "identity_hash", "identity_public_key", + "app_data", "rssi", "snr", "quality", + ] + # These are safe as they are from a hardcoded list + columns = ", ".join(fields) + placeholders = ", ".join(["?"] * len(fields)) + update_set = ", ".join([f"{f} = EXCLUDED.{f}" for f in fields if f != "destination_hash"]) + + query = f"INSERT INTO announces ({columns}, updated_at) VALUES ({placeholders}, ?) " \ + f"ON CONFLICT(destination_hash) DO UPDATE SET {update_set}, updated_at = EXCLUDED.updated_at" # noqa: S608 + + params = [data.get(f) for f in fields] + params.append(datetime.now(UTC)) + self.provider.execute(query, params) + + def get_announces(self, aspect=None): + if aspect: + return self.provider.fetchall("SELECT * FROM announces WHERE aspect = ?", (aspect,)) + return self.provider.fetchall("SELECT * FROM announces") + + def get_announce_by_hash(self, destination_hash): + return self.provider.fetchone("SELECT * FROM announces WHERE destination_hash = ?", (destination_hash,)) + + def get_filtered_announces(self, aspect=None, search_term=None, limit=None, offset=0): + query = "SELECT * FROM announces WHERE 1=1" + params = [] + if aspect: + query += " AND aspect = ?" + params.append(aspect) + if search_term: + query += " AND (destination_hash LIKE ? OR identity_hash LIKE ?)" + like_term = f"%{search_term}%" + params.extend([like_term, like_term]) + + query += " ORDER BY updated_at DESC" + + if limit: + query += " LIMIT ? OFFSET ?" + params.extend([limit, offset]) + + return self.provider.fetchall(query, params) + + # Custom Display Names + def upsert_custom_display_name(self, destination_hash, display_name): + now = datetime.now(UTC) + self.provider.execute(""" + INSERT INTO custom_destination_display_names (destination_hash, display_name, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(destination_hash) DO UPDATE SET display_name = EXCLUDED.display_name, updated_at = EXCLUDED.updated_at + """, (destination_hash, display_name, now)) + + def get_custom_display_name(self, destination_hash): + row = self.provider.fetchone("SELECT display_name FROM custom_destination_display_names WHERE destination_hash = ?", (destination_hash,)) + return row["display_name"] if row else None + + def delete_custom_display_name(self, destination_hash): + self.provider.execute("DELETE FROM custom_destination_display_names WHERE destination_hash = ?", (destination_hash,)) + + # Favourites + def upsert_favourite(self, destination_hash, display_name, aspect): + now = datetime.now(UTC) + self.provider.execute(""" + INSERT INTO favourite_destinations (destination_hash, display_name, aspect, updated_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(destination_hash) DO UPDATE SET display_name = EXCLUDED.display_name, aspect = EXCLUDED.aspect, updated_at = EXCLUDED.updated_at + """, (destination_hash, display_name, aspect, now)) + + def get_favourites(self, aspect=None): + if aspect: + return self.provider.fetchall("SELECT * FROM favourite_destinations WHERE aspect = ?", (aspect,)) + return self.provider.fetchall("SELECT * FROM favourite_destinations") + + def delete_favourite(self, destination_hash): + self.provider.execute("DELETE FROM favourite_destinations WHERE destination_hash = ?", (destination_hash,)) + diff --git a/meshchatx/src/backend/database/config.py b/meshchatx/src/backend/database/config.py new file mode 100644 index 0000000..7c7925d --- /dev/null +++ b/meshchatx/src/backend/database/config.py @@ -0,0 +1,27 @@ +from datetime import UTC, datetime + +from .provider import DatabaseProvider + + +class ConfigDAO: + def __init__(self, provider: DatabaseProvider): + self.provider = provider + + def get(self, key, default=None): + row = self.provider.fetchone("SELECT value FROM config WHERE key = ?", (key,)) + if row: + return row["value"] + return default + + def set(self, key, value): + if value is None: + self.provider.execute("DELETE FROM config WHERE key = ?", (key,)) + else: + self.provider.execute( + "INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)", + (key, str(value), datetime.now(UTC)), + ) + + def delete(self, key): + self.provider.execute("DELETE FROM config WHERE key = ?", (key,)) + diff --git a/meshchatx/src/backend/database/legacy_migrator.py b/meshchatx/src/backend/database/legacy_migrator.py new file mode 100644 index 0000000..db23a6c --- /dev/null +++ b/meshchatx/src/backend/database/legacy_migrator.py @@ -0,0 +1,126 @@ +import os + + +class LegacyMigrator: + def __init__(self, provider, reticulum_config_dir, identity_hash_hex): + self.provider = provider + self.reticulum_config_dir = reticulum_config_dir + self.identity_hash_hex = identity_hash_hex + + def get_legacy_db_path(self): + """Detect the path to the legacy database based on the Reticulum config directory. + """ + possible_dirs = [] + if self.reticulum_config_dir: + possible_dirs.append(self.reticulum_config_dir) + + # Add common default locations + home = os.path.expanduser("~") + possible_dirs.append(os.path.join(home, ".reticulum-meshchat")) + possible_dirs.append(os.path.join(home, ".reticulum")) + + # Check each directory + for config_dir in possible_dirs: + legacy_path = os.path.join(config_dir, "identities", self.identity_hash_hex, "database.db") + if os.path.exists(legacy_path): + # Ensure it's not the same as our current DB path + # (though this is unlikely given the different base directories) + try: + current_db_path = os.path.abspath(self.provider.db_path) + if os.path.abspath(legacy_path) == current_db_path: + continue + except (AttributeError, OSError): + # If we can't get the absolute path, just skip this check + pass + return legacy_path + + return None + + def should_migrate(self): + """Check if migration should be performed. + Only migrates if the current database is empty and a legacy database exists. + """ + legacy_path = self.get_legacy_db_path() + if not legacy_path: + return False + + # Check if current DB has any messages + try: + res = self.provider.fetchone("SELECT COUNT(*) as count FROM lxmf_messages") + if res and res["count"] > 0: + # Already have data, don't auto-migrate + return False + except Exception: # noqa: S110 + # Table doesn't exist yet, which is fine + # We use a broad Exception here as the database might not even be initialized + pass + + return True + + def migrate(self): + """Perform the migration from the legacy database. + """ + legacy_path = self.get_legacy_db_path() + if not legacy_path: + return False + + print(f"Detecting legacy database at {legacy_path}...") + + try: + # Attach the legacy database + # We use a randomized alias to avoid collisions + alias = f"legacy_{os.urandom(4).hex()}" + self.provider.execute(f"ATTACH DATABASE '{legacy_path}' AS {alias}") + + # Tables that existed in the legacy Peewee version + tables_to_migrate = [ + "announces", + "blocked_destinations", + "config", + "custom_destination_display_names", + "favourite_destinations", + "lxmf_conversation_read_state", + "lxmf_messages", + "lxmf_user_icons", + "spam_keywords", + ] + + print("Auto-migrating data from legacy database...") + for table in tables_to_migrate: + # Basic validation to ensure table name is from our whitelist + if table not in tables_to_migrate: + continue + + try: + # Check if table exists in legacy DB + # We use a f-string here for the alias and table name, which are controlled by us + check_query = f"SELECT name FROM {alias}.sqlite_master WHERE type='table' AND name=?" # noqa: S608 + res = self.provider.fetchone(check_query, (table,)) + + if res: + # Get columns from both databases to ensure compatibility + # These PRAGMA calls are safe as they use controlled table/alias names + legacy_columns = [row["name"] for row in self.provider.fetchall(f"PRAGMA {alias}.table_info({table})")] + current_columns = [row["name"] for row in self.provider.fetchall(f"PRAGMA table_info({table})")] + + # Find common columns + common_columns = [col for col in legacy_columns if col in current_columns] + + if common_columns: + cols_str = ", ".join(common_columns) + # We use INSERT OR IGNORE to avoid duplicates + # The table and columns are controlled by us + migrate_query = f"INSERT OR IGNORE INTO {table} ({cols_str}) SELECT {cols_str} FROM {alias}.{table}" # noqa: S608 + self.provider.execute(migrate_query) + print(f" - Migrated table: {table} ({len(common_columns)} columns)") + else: + print(f" - Skipping table {table}: No common columns found") + except Exception as e: + print(f" - Failed to migrate table {table}: {e}") + + self.provider.execute(f"DETACH DATABASE {alias}") + print("Legacy migration completed successfully.") + return True + except Exception as e: + print(f"Migration from legacy failed: {e}") + return False diff --git a/meshchatx/src/backend/database/messages.py b/meshchatx/src/backend/database/messages.py new file mode 100644 index 0000000..cdd2819 --- /dev/null +++ b/meshchatx/src/backend/database/messages.py @@ -0,0 +1,146 @@ +import json +from datetime import UTC, datetime + +from .provider import DatabaseProvider + + +class MessageDAO: + def __init__(self, provider: DatabaseProvider): + self.provider = provider + + def upsert_lxmf_message(self, data): + # Ensure data is a dict if it's a sqlite3.Row + if not isinstance(data, dict): + data = dict(data) + + # Ensure all required fields are present and handle defaults + fields = [ + "hash", "source_hash", "destination_hash", "state", "progress", + "is_incoming", "method", "delivery_attempts", "next_delivery_attempt_at", + "title", "content", "fields", "timestamp", "rssi", "snr", "quality", "is_spam", + ] + + columns = ", ".join(fields) + placeholders = ", ".join(["?"] * len(fields)) + update_set = ", ".join([f"{f} = EXCLUDED.{f}" for f in fields if f != "hash"]) + + query = f"INSERT INTO lxmf_messages ({columns}, updated_at) VALUES ({placeholders}, ?) " \ + f"ON CONFLICT(hash) DO UPDATE SET {update_set}, updated_at = EXCLUDED.updated_at" # noqa: S608 + + params = [] + for f in fields: + val = data.get(f) + if f == "fields" and isinstance(val, dict): + val = json.dumps(val) + params.append(val) + params.append(datetime.now(UTC).isoformat()) + + self.provider.execute(query, params) + + def get_lxmf_message_by_hash(self, message_hash): + return self.provider.fetchone("SELECT * FROM lxmf_messages WHERE hash = ?", (message_hash,)) + + def delete_lxmf_message_by_hash(self, message_hash): + self.provider.execute("DELETE FROM lxmf_messages WHERE hash = ?", (message_hash,)) + + def get_conversation_messages(self, destination_hash, limit=100, offset=0): + return self.provider.fetchall( + "SELECT * FROM lxmf_messages WHERE destination_hash = ? OR source_hash = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?", + (destination_hash, destination_hash, limit, offset), + ) + + def get_conversations(self): + # This is a bit complex in raw SQL, we need the latest message for each destination + query = """ + SELECT m1.* FROM lxmf_messages m1 + JOIN ( + SELECT + CASE WHEN is_incoming = 1 THEN source_hash ELSE destination_hash END as peer_hash, + MAX(timestamp) as max_ts + FROM lxmf_messages + GROUP BY peer_hash + ) m2 ON (CASE WHEN m1.is_incoming = 1 THEN m1.source_hash ELSE m1.destination_hash END = m2.peer_hash + AND m1.timestamp = m2.max_ts) + ORDER BY m1.timestamp DESC + """ + return self.provider.fetchall(query) + + def mark_conversation_as_read(self, destination_hash): + now = datetime.now(UTC).isoformat() + self.provider.execute( + "INSERT OR REPLACE INTO lxmf_conversation_read_state (destination_hash, last_read_at, updated_at) VALUES (?, ?, ?)", + (destination_hash, now, now), + ) + + def is_conversation_unread(self, destination_hash): + row = self.provider.fetchone(""" + SELECT m.timestamp, r.last_read_at + FROM lxmf_messages m + LEFT JOIN lxmf_conversation_read_state r ON r.destination_hash = ? + WHERE (m.destination_hash = ? OR m.source_hash = ?) + ORDER BY m.timestamp DESC LIMIT 1 + """, (destination_hash, destination_hash, destination_hash)) + + if not row: + return False + if not row["last_read_at"]: + return True + + last_read_at = datetime.fromisoformat(row["last_read_at"]) + if last_read_at.tzinfo is None: + last_read_at = last_read_at.replace(tzinfo=UTC) + + return row["timestamp"] > last_read_at.timestamp() + + def mark_stuck_messages_as_failed(self): + self.provider.execute(""" + UPDATE lxmf_messages + SET state = 'failed', updated_at = ? + WHERE state = 'outbound' + OR (state = 'sent' AND method = 'opportunistic') + OR state = 'sending' + """, (datetime.now(UTC).isoformat(),)) + + def get_failed_messages_for_destination(self, destination_hash): + return self.provider.fetchall( + "SELECT * FROM lxmf_messages WHERE state = 'failed' AND destination_hash = ? ORDER BY id ASC", + (destination_hash,), + ) + + def get_failed_messages_count(self, destination_hash): + row = self.provider.fetchone( + "SELECT COUNT(*) as count FROM lxmf_messages WHERE state = 'failed' AND destination_hash = ?", + (destination_hash,), + ) + return row["count"] if row else 0 + + # Forwarding Mappings + def get_forwarding_mapping(self, alias_hash=None, original_sender_hash=None, final_recipient_hash=None): + if alias_hash: + return self.provider.fetchone("SELECT * FROM lxmf_forwarding_mappings WHERE alias_hash = ?", (alias_hash,)) + if original_sender_hash and final_recipient_hash: + return self.provider.fetchone( + "SELECT * FROM lxmf_forwarding_mappings WHERE original_sender_hash = ? AND final_recipient_hash = ?", + (original_sender_hash, final_recipient_hash), + ) + return None + + def create_forwarding_mapping(self, data): + # Ensure data is a dict if it's a sqlite3.Row + if not isinstance(data, dict): + data = dict(data) + + fields = [ + "alias_identity_private_key", "alias_hash", "original_sender_hash", + "final_recipient_hash", "original_destination_hash", + ] + columns = ", ".join(fields) + placeholders = ", ".join(["?"] * len(fields)) + query = f"INSERT INTO lxmf_forwarding_mappings ({columns}, created_at) VALUES ({placeholders}, ?)" # noqa: S608 + params = [data.get(f) for f in fields] + params.append(datetime.now(UTC).isoformat()) + self.provider.execute(query, params) + + def get_all_forwarding_mappings(self): + return self.provider.fetchall("SELECT * FROM lxmf_forwarding_mappings") + diff --git a/meshchatx/src/backend/database/misc.py b/meshchatx/src/backend/database/misc.py new file mode 100644 index 0000000..49cf6d1 --- /dev/null +++ b/meshchatx/src/backend/database/misc.py @@ -0,0 +1,154 @@ +from datetime import UTC, datetime + +from .provider import DatabaseProvider + + +class MiscDAO: + def __init__(self, provider: DatabaseProvider): + self.provider = provider + + # Blocked Destinations + def add_blocked_destination(self, destination_hash): + self.provider.execute( + "INSERT OR IGNORE INTO blocked_destinations (destination_hash, updated_at) VALUES (?, ?)", + (destination_hash, datetime.now(UTC)), + ) + + def is_destination_blocked(self, destination_hash): + return self.provider.fetchone("SELECT 1 FROM blocked_destinations WHERE destination_hash = ?", (destination_hash,)) is not None + + def get_blocked_destinations(self): + return self.provider.fetchall("SELECT * FROM blocked_destinations") + + def delete_blocked_destination(self, destination_hash): + self.provider.execute("DELETE FROM blocked_destinations WHERE destination_hash = ?", (destination_hash,)) + + # Spam Keywords + def add_spam_keyword(self, keyword): + self.provider.execute( + "INSERT OR IGNORE INTO spam_keywords (keyword, updated_at) VALUES (?, ?)", + (keyword, datetime.now(UTC)), + ) + + def get_spam_keywords(self): + return self.provider.fetchall("SELECT * FROM spam_keywords") + + def delete_spam_keyword(self, keyword_id): + self.provider.execute("DELETE FROM spam_keywords WHERE id = ?", (keyword_id,)) + + def check_spam_keywords(self, title, content): + keywords = self.get_spam_keywords() + search_text = (title + " " + content).lower() + for kw in keywords: + if kw["keyword"].lower() in search_text: + return True + return False + + # User Icons + def update_lxmf_user_icon(self, destination_hash, icon_name, foreground_colour, background_colour): + now = datetime.now(UTC) + self.provider.execute(""" + INSERT INTO lxmf_user_icons (destination_hash, icon_name, foreground_colour, background_colour, updated_at) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(destination_hash) DO UPDATE SET + icon_name = EXCLUDED.icon_name, + foreground_colour = EXCLUDED.foreground_colour, + background_colour = EXCLUDED.background_colour, + updated_at = EXCLUDED.updated_at + """, (destination_hash, icon_name, foreground_colour, background_colour, now)) + + def get_user_icon(self, destination_hash): + return self.provider.fetchone("SELECT * FROM lxmf_user_icons WHERE destination_hash = ?", (destination_hash,)) + + # Forwarding Rules + def get_forwarding_rules(self, identity_hash=None, active_only=False): + query = "SELECT * FROM lxmf_forwarding_rules WHERE 1=1" + params = [] + if identity_hash: + query += " AND (identity_hash = ? OR identity_hash IS NULL)" + params.append(identity_hash) + if active_only: + query += " AND is_active = 1" + return self.provider.fetchall(query, params) + + def create_forwarding_rule(self, identity_hash, forward_to_hash, source_filter_hash, is_active=True): + now = datetime.now(UTC) + self.provider.execute( + "INSERT INTO lxmf_forwarding_rules (identity_hash, forward_to_hash, source_filter_hash, is_active, updated_at) VALUES (?, ?, ?, ?, ?)", + (identity_hash, forward_to_hash, source_filter_hash, 1 if is_active else 0, now), + ) + + def delete_forwarding_rule(self, rule_id): + self.provider.execute("DELETE FROM lxmf_forwarding_rules WHERE id = ?", (rule_id,)) + + def toggle_forwarding_rule(self, rule_id): + self.provider.execute("UPDATE lxmf_forwarding_rules SET is_active = NOT is_active WHERE id = ?", (rule_id,)) + + # Archived Pages + def archive_page(self, destination_hash, page_path, content, page_hash): + self.provider.execute( + "INSERT INTO archived_pages (destination_hash, page_path, content, hash) VALUES (?, ?, ?, ?)", + (destination_hash, page_path, content, page_hash), + ) + + def get_archived_page_versions(self, destination_hash, page_path): + return self.provider.fetchall( + "SELECT * FROM archived_pages WHERE destination_hash = ? AND page_path = ? ORDER BY created_at DESC", + (destination_hash, page_path), + ) + + def get_archived_pages_paginated(self, destination_hash=None, query=None): + sql = "SELECT * FROM archived_pages WHERE 1=1" + params = [] + if destination_hash: + sql += " AND destination_hash = ?" + params.append(destination_hash) + if query: + like_term = f"%{query}%" + sql += " AND (destination_hash LIKE ? OR page_path LIKE ? OR content LIKE ?)" + params.extend([like_term, like_term, like_term]) + + sql += " ORDER BY created_at DESC" + return self.provider.fetchall(sql, params) + + def delete_archived_pages(self, destination_hash=None, page_path=None): + if destination_hash and page_path: + self.provider.execute("DELETE FROM archived_pages WHERE destination_hash = ? AND page_path = ?", (destination_hash, page_path)) + else: + self.provider.execute("DELETE FROM archived_pages") + + # Crawl Tasks + def upsert_crawl_task(self, destination_hash, page_path, status="pending", retry_count=0): + self.provider.execute(""" + INSERT INTO crawl_tasks (destination_hash, page_path, status, retry_count) + VALUES (?, ?, ?, ?) + ON CONFLICT(destination_hash, page_path) DO UPDATE SET + status = EXCLUDED.status, + retry_count = EXCLUDED.retry_count + """, (destination_hash, page_path, status, retry_count)) + + def get_pending_crawl_tasks(self): + return self.provider.fetchall("SELECT * FROM crawl_tasks WHERE status = 'pending'") + + def update_crawl_task(self, task_id, **kwargs): + allowed_keys = {"destination_hash", "page_path", "status", "retry_count", "updated_at"} + filtered_kwargs = {k: v for k, v in kwargs.items() if k in allowed_keys} + + if not filtered_kwargs: + return + + set_clause = ", ".join([f"{k} = ?" for k in filtered_kwargs]) + params = list(filtered_kwargs.values()) + params.append(task_id) + query = f"UPDATE crawl_tasks SET {set_clause} WHERE id = ?" # noqa: S608 + self.provider.execute(query, params) + + def get_pending_or_failed_crawl_tasks(self, max_retries, max_concurrent): + return self.provider.fetchall( + "SELECT * FROM crawl_tasks WHERE status IN ('pending', 'failed') AND retry_count < ? LIMIT ?", + (max_retries, max_concurrent), + ) + + def get_archived_page_by_id(self, archive_id): + return self.provider.fetchone("SELECT * FROM archived_pages WHERE id = ?", (archive_id,)) + diff --git a/meshchatx/src/backend/database/provider.py b/meshchatx/src/backend/database/provider.py new file mode 100644 index 0000000..4291f60 --- /dev/null +++ b/meshchatx/src/backend/database/provider.py @@ -0,0 +1,65 @@ +import sqlite3 +import threading + + +class DatabaseProvider: + _instance = None + _lock = threading.Lock() + + def __init__(self, db_path=None): + self.db_path = db_path + self._local = threading.local() + + @classmethod + def get_instance(cls, db_path=None): + with cls._lock: + if cls._instance is None: + if db_path is None: + msg = "Database path must be provided for the first initialization" + raise ValueError(msg) + cls._instance = cls(db_path) + return cls._instance + + @property + def connection(self): + if not hasattr(self._local, "connection"): + self._local.connection = sqlite3.connect(self.db_path, check_same_thread=False) + self._local.connection.row_factory = sqlite3.Row + # Enable WAL mode for better concurrency + self._local.connection.execute("PRAGMA journal_mode=WAL") + return self._local.connection + + def execute(self, query, params=None): + cursor = self.connection.cursor() + if params: + cursor.execute(query, params) + else: + cursor.execute(query) + self.connection.commit() + return cursor + + def fetchone(self, query, params=None): + cursor = self.execute(query, params) + return cursor.fetchone() + + def fetchall(self, query, params=None): + cursor = self.execute(query, params) + return cursor.fetchall() + + def close(self): + if hasattr(self._local, "connection"): + self._local.connection.close() + del self._local.connection + + def vacuum(self): + self.execute("VACUUM") + + def integrity_check(self): + return self.fetchall("PRAGMA integrity_check") + + def quick_check(self): + return self.fetchall("PRAGMA quick_check") + + def checkpoint(self): + return self.fetchall("PRAGMA wal_checkpoint(TRUNCATE)") + diff --git a/meshchatx/src/backend/database/schema.py b/meshchatx/src/backend/database/schema.py new file mode 100644 index 0000000..a304256 --- /dev/null +++ b/meshchatx/src/backend/database/schema.py @@ -0,0 +1,317 @@ +from .provider import DatabaseProvider + + +class DatabaseSchema: + LATEST_VERSION = 12 + + def __init__(self, provider: DatabaseProvider): + self.provider = provider + + def initialize(self): + # Create core tables if they don't exist + self._create_initial_tables() + + # Run migrations + current_version = self._get_current_version() + self.migrate(current_version) + + def _get_current_version(self): + row = self.provider.fetchone("SELECT value FROM config WHERE key = ?", ("database_version",)) + if row: + return int(row["value"]) + return 0 + + def _create_initial_tables(self): + # We create the config table first so we can track version + self.provider.execute(""" + CREATE TABLE IF NOT EXISTS config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT UNIQUE, + value TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """) + + # Other essential tables that were present from version 1 + # Peewee automatically creates tables if they don't exist. + # Here we define the full schema for all tables as they should be now. + + tables = { + "announces": """ + CREATE TABLE IF NOT EXISTS announces ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + destination_hash TEXT UNIQUE, + aspect TEXT, + identity_hash TEXT, + identity_public_key TEXT, + app_data TEXT, + rssi INTEGER, + snr REAL, + quality REAL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """, + "custom_destination_display_names": """ + CREATE TABLE IF NOT EXISTS custom_destination_display_names ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + destination_hash TEXT UNIQUE, + display_name TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """, + "favourite_destinations": """ + CREATE TABLE IF NOT EXISTS favourite_destinations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + destination_hash TEXT UNIQUE, + display_name TEXT, + aspect TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """, + "lxmf_messages": """ + CREATE TABLE IF NOT EXISTS lxmf_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hash TEXT UNIQUE, + source_hash TEXT, + destination_hash TEXT, + state TEXT, + progress REAL, + is_incoming INTEGER, + method TEXT, + delivery_attempts INTEGER DEFAULT 0, + next_delivery_attempt_at REAL, + title TEXT, + content TEXT, + fields TEXT, + timestamp REAL, + rssi INTEGER, + snr REAL, + quality REAL, + is_spam INTEGER DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """, + "lxmf_conversation_read_state": """ + CREATE TABLE IF NOT EXISTS lxmf_conversation_read_state ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + destination_hash TEXT UNIQUE, + last_read_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """, + "lxmf_user_icons": """ + CREATE TABLE IF NOT EXISTS lxmf_user_icons ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + destination_hash TEXT UNIQUE, + icon_name TEXT, + foreground_colour TEXT, + background_colour TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """, + "blocked_destinations": """ + CREATE TABLE IF NOT EXISTS blocked_destinations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + destination_hash TEXT UNIQUE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """, + "spam_keywords": """ + CREATE TABLE IF NOT EXISTS spam_keywords ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + keyword TEXT UNIQUE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """, + "archived_pages": """ + CREATE TABLE IF NOT EXISTS archived_pages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + destination_hash TEXT, + page_path TEXT, + content TEXT, + hash TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """, + "crawl_tasks": """ + CREATE TABLE IF NOT EXISTS crawl_tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + destination_hash TEXT, + page_path TEXT, + retry_count INTEGER DEFAULT 0, + last_retry_at DATETIME, + next_retry_at DATETIME, + status TEXT DEFAULT 'pending', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(destination_hash, page_path) + ) + """, + "lxmf_forwarding_rules": """ + CREATE TABLE IF NOT EXISTS lxmf_forwarding_rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + identity_hash TEXT, + forward_to_hash TEXT, + source_filter_hash TEXT, + is_active INTEGER DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """, + "lxmf_forwarding_mappings": """ + CREATE TABLE IF NOT EXISTS lxmf_forwarding_mappings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + alias_identity_private_key TEXT, + alias_hash TEXT UNIQUE, + original_sender_hash TEXT, + final_recipient_hash TEXT, + original_destination_hash TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """, + "call_history": """ + CREATE TABLE IF NOT EXISTS call_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + remote_identity_hash TEXT, + remote_identity_name TEXT, + is_incoming INTEGER, + status TEXT, + duration_seconds INTEGER, + timestamp REAL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """, + } + + for table_name, create_sql in tables.items(): + self.provider.execute(create_sql) + # Create indexes that were present + if table_name == "announces": + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_announces_aspect ON announces(aspect)") + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_announces_identity_hash ON announces(identity_hash)") + elif table_name == "lxmf_messages": + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_lxmf_messages_source_hash ON lxmf_messages(source_hash)") + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_lxmf_messages_destination_hash ON lxmf_messages(destination_hash)") + elif table_name == "blocked_destinations": + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_blocked_destinations_hash ON blocked_destinations(destination_hash)") + elif table_name == "spam_keywords": + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_spam_keywords_keyword ON spam_keywords(keyword)") + + def migrate(self, current_version): + if current_version < 7: + self.provider.execute(""" + CREATE TABLE IF NOT EXISTS archived_pages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + destination_hash TEXT, + page_path TEXT, + content TEXT, + hash TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """) + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_archived_pages_destination_hash ON archived_pages(destination_hash)") + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_archived_pages_page_path ON archived_pages(page_path)") + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_archived_pages_hash ON archived_pages(hash)") + + if current_version < 8: + self.provider.execute(""" + CREATE TABLE IF NOT EXISTS crawl_tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + destination_hash TEXT, + page_path TEXT, + retry_count INTEGER DEFAULT 0, + last_retry_at DATETIME, + next_retry_at DATETIME, + status TEXT DEFAULT 'pending', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """) + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_crawl_tasks_destination_hash ON crawl_tasks(destination_hash)") + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_crawl_tasks_page_path ON crawl_tasks(page_path)") + + if current_version < 9: + self.provider.execute(""" + CREATE TABLE IF NOT EXISTS lxmf_forwarding_rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + identity_hash TEXT, + forward_to_hash TEXT, + source_filter_hash TEXT, + is_active INTEGER DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """) + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_lxmf_forwarding_rules_identity_hash ON lxmf_forwarding_rules(identity_hash)") + + self.provider.execute(""" + CREATE TABLE IF NOT EXISTS lxmf_forwarding_mappings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + alias_identity_private_key TEXT, + alias_hash TEXT UNIQUE, + original_sender_hash TEXT, + final_recipient_hash TEXT, + original_destination_hash TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """) + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_lxmf_forwarding_mappings_alias_hash ON lxmf_forwarding_mappings(alias_hash)") + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_lxmf_forwarding_mappings_sender_hash ON lxmf_forwarding_mappings(original_sender_hash)") + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_lxmf_forwarding_mappings_recipient_hash ON lxmf_forwarding_mappings(final_recipient_hash)") + + if current_version < 10: + # Ensure unique constraints exist for ON CONFLICT clauses + # SQLite doesn't support adding UNIQUE constraints via ALTER TABLE, + # but a UNIQUE index works for ON CONFLICT. + + # Clean up duplicates before adding unique indexes + self.provider.execute("DELETE FROM announces WHERE id NOT IN (SELECT MAX(id) FROM announces GROUP BY destination_hash)") + self.provider.execute("DELETE FROM crawl_tasks WHERE id NOT IN (SELECT MAX(id) FROM crawl_tasks GROUP BY destination_hash, page_path)") + self.provider.execute("DELETE FROM custom_destination_display_names WHERE id NOT IN (SELECT MAX(id) FROM custom_destination_display_names GROUP BY destination_hash)") + self.provider.execute("DELETE FROM favourite_destinations WHERE id NOT IN (SELECT MAX(id) FROM favourite_destinations GROUP BY destination_hash)") + self.provider.execute("DELETE FROM lxmf_user_icons WHERE id NOT IN (SELECT MAX(id) FROM lxmf_user_icons GROUP BY destination_hash)") + self.provider.execute("DELETE FROM lxmf_conversation_read_state WHERE id NOT IN (SELECT MAX(id) FROM lxmf_conversation_read_state GROUP BY destination_hash)") + self.provider.execute("DELETE FROM lxmf_messages WHERE id NOT IN (SELECT MAX(id) FROM lxmf_messages GROUP BY hash)") + + self.provider.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_announces_destination_hash_unique ON announces(destination_hash)") + self.provider.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_crawl_tasks_destination_path_unique ON crawl_tasks(destination_hash, page_path)") + self.provider.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_custom_display_names_dest_hash_unique ON custom_destination_display_names(destination_hash)") + self.provider.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_favourite_destinations_dest_hash_unique ON favourite_destinations(destination_hash)") + self.provider.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_lxmf_messages_hash_unique ON lxmf_messages(hash)") + self.provider.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_lxmf_user_icons_dest_hash_unique ON lxmf_user_icons(destination_hash)") + self.provider.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_lxmf_conversation_read_state_dest_hash_unique ON lxmf_conversation_read_state(destination_hash)") + + if current_version < 11: + # Add is_spam column to lxmf_messages if it doesn't exist + try: + self.provider.execute("ALTER TABLE lxmf_messages ADD COLUMN is_spam INTEGER DEFAULT 0") + except Exception: + # Column might already exist if table was created with newest schema + pass + + if current_version < 12: + self.provider.execute(""" + CREATE TABLE IF NOT EXISTS call_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + remote_identity_hash TEXT, + remote_identity_name TEXT, + is_incoming INTEGER, + status TEXT, + duration_seconds INTEGER, + timestamp REAL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """) + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_call_history_remote_hash ON call_history(remote_identity_hash)") + self.provider.execute("CREATE INDEX IF NOT EXISTS idx_call_history_timestamp ON call_history(timestamp)") + + # Update version in config + self.provider.execute("INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", ("database_version", str(self.LATEST_VERSION))) + diff --git a/meshchatx/src/backend/database/telephone.py b/meshchatx/src/backend/database/telephone.py new file mode 100644 index 0000000..c3caa84 --- /dev/null +++ b/meshchatx/src/backend/database/telephone.py @@ -0,0 +1,44 @@ + +from .provider import DatabaseProvider + + +class TelephoneDAO: + def __init__(self, provider: DatabaseProvider): + self.provider = provider + + def add_call_history( + self, + remote_identity_hash, + remote_identity_name, + is_incoming, + status, + duration_seconds, + timestamp, + ): + self.provider.execute( + """ + INSERT INTO call_history ( + remote_identity_hash, + remote_identity_name, + is_incoming, + status, + duration_seconds, + timestamp + ) VALUES (?, ?, ?, ?, ?, ?) + """, + ( + remote_identity_hash, + remote_identity_name, + 1 if is_incoming else 0, + status, + duration_seconds, + timestamp, + ), + ) + + def get_call_history(self, limit=10): + return self.provider.fetchall( + "SELECT * FROM call_history ORDER BY timestamp DESC LIMIT ?", + (limit,), + ) + diff --git a/meshchatx/src/backend/forwarding_manager.py b/meshchatx/src/backend/forwarding_manager.py new file mode 100644 index 0000000..4d112cd --- /dev/null +++ b/meshchatx/src/backend/forwarding_manager.py @@ -0,0 +1,48 @@ +import base64 + +import RNS + +from .database import Database + + +class ForwardingManager: + def __init__(self, db: Database, message_router): + self.db = db + self.message_router = message_router + self.forwarding_destinations = {} + + def load_aliases(self): + mappings = self.db.messages.get_all_forwarding_mappings() + for mapping in mappings: + try: + private_key_bytes = base64.b64decode(mapping["alias_identity_private_key"]) + alias_identity = RNS.Identity.from_bytes(private_key_bytes) + alias_destination = self.message_router.register_delivery_identity(identity=alias_identity) + self.forwarding_destinations[mapping["alias_hash"]] = alias_destination + except Exception as e: + print(f"Failed to load forwarding alias {mapping['alias_hash']}: {e}") + + def get_or_create_mapping(self, source_hash, final_recipient_hash, original_destination_hash): + mapping = self.db.messages.get_forwarding_mapping( + original_sender_hash=source_hash, + final_recipient_hash=final_recipient_hash, + ) + + if not mapping: + alias_identity = RNS.Identity() + alias_hash = alias_identity.hash.hex() + + alias_destination = self.message_router.register_delivery_identity(alias_identity) + self.forwarding_destinations[alias_hash] = alias_destination + + data = { + "alias_identity_private_key": base64.b64encode(alias_identity.get_private_key()).decode(), + "alias_hash": alias_hash, + "original_sender_hash": source_hash, + "final_recipient_hash": final_recipient_hash, + "original_destination_hash": original_destination_hash, + } + self.db.messages.create_forwarding_mapping(data) + return data + return mapping + diff --git a/meshchatx/src/backend/interface_config_parser.py b/meshchatx/src/backend/interface_config_parser.py new file mode 100644 index 0000000..3941055 --- /dev/null +++ b/meshchatx/src/backend/interface_config_parser.py @@ -0,0 +1,91 @@ +import RNS.vendor.configobj + + +class InterfaceConfigParser: + @staticmethod + def parse(text): + # get lines from provided text + lines = text.splitlines() + stripped_lines = [line.strip() for line in lines] + + # ensure [interfaces] section exists + if "[interfaces]" not in stripped_lines: + lines.insert(0, "[interfaces]") + stripped_lines.insert(0, "[interfaces]") + + try: + # parse lines as rns config object + config = RNS.vendor.configobj.ConfigObj(lines) + except Exception as e: + print(f"Failed to parse interface config with ConfigObj: {e}") + return InterfaceConfigParser._parse_best_effort(lines) + + # get interfaces from config + config_interfaces = config.get("interfaces", {}) + if config_interfaces is None: + return [] + + # process interfaces + interfaces = [] + for interface_name in config_interfaces: + # ensure interface has a name + interface_config = config_interfaces[interface_name] + interface_config["name"] = interface_name + interfaces.append(interface_config) + + return interfaces + + @staticmethod + def _parse_best_effort(lines): + interfaces = [] + current_interface_name = None + current_interface = {} + current_sub_name = None + current_sub = None + + def commit_sub(): + nonlocal current_sub_name, current_sub + if current_sub_name and current_sub is not None: + current_interface[current_sub_name] = current_sub + current_sub_name = None + current_sub = None + + def commit_interface(): + nonlocal current_interface_name, current_interface + if current_interface_name: + # shallow copy to avoid future mutation + interfaces.append(dict(current_interface)) + current_interface_name = None + current_interface = {} + + for raw_line in lines: + line = raw_line.strip() + if line == "" or line.startswith("#"): + continue + + if line.lower() == "[interfaces]": + continue + + if line.startswith("[[[") and line.endswith("]]]"): + commit_sub() + current_sub_name = line[3:-3].strip() + current_sub = {} + continue + + if line.startswith("[[") and line.endswith("]]"): + commit_sub() + commit_interface() + current_interface_name = line[2:-2].strip() + current_interface = {"name": current_interface_name} + continue + + if "=" in line and current_interface_name is not None: + key, value = line.split("=", 1) + target = current_sub if current_sub is not None else current_interface + target[key.strip()] = value.strip() + + # commit any pending sections + commit_sub() + commit_interface() + + return interfaces diff --git a/meshchatx/src/backend/interface_editor.py b/meshchatx/src/backend/interface_editor.py new file mode 100644 index 0000000..65763c6 --- /dev/null +++ b/meshchatx/src/backend/interface_editor.py @@ -0,0 +1,11 @@ +class InterfaceEditor: + @staticmethod + def update_value(interface_details: dict, data: dict, key: str): + # update value if provided and not empty + value = data.get(key) + if value is not None and value != "": + interface_details[key] = value + return + + # otherwise remove existing value + interface_details.pop(key, None) diff --git a/meshchatx/src/backend/interfaces/WebsocketClientInterface.py b/meshchatx/src/backend/interfaces/WebsocketClientInterface.py new file mode 100644 index 0000000..9f4f957 --- /dev/null +++ b/meshchatx/src/backend/interfaces/WebsocketClientInterface.py @@ -0,0 +1,136 @@ +import threading +import time + +import RNS +from RNS.Interfaces.Interface import Interface +from websockets.sync.client import connect +from websockets.sync.connection import Connection + + +class WebsocketClientInterface(Interface): + # TODO: required? + DEFAULT_IFAC_SIZE = 16 + + RECONNECT_DELAY_SECONDS = 5 + + def __str__(self): + return f"WebsocketClientInterface[{self.name}/{self.target_url}]" + + def __init__(self, owner, configuration, websocket: Connection = None): + super().__init__() + + self.owner = owner + self.parent_interface = None + + self.IN = True + self.OUT = False + self.HW_MTU = 262144 # 256KiB + self.bitrate = 1_000_000_000 # 1Gbps + self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL + + # parse config + ifconf = Interface.get_config_obj(configuration) + self.name = ifconf.get("name") + self.target_url = ifconf.get("target_url", None) + + # ensure target url is provided + if self.target_url is None: + msg = f"target_url is required for interface '{self.name}'" + raise SystemError(msg) + + # connect to websocket server if an existing connection was not provided + self.websocket = websocket + if self.websocket is None: + thread = threading.Thread(target=self.connect) + thread.daemon = True + thread.start() + + # called when a full packet has been received over the websocket + def process_incoming(self, data): + # do nothing if offline or detached + if not self.online or self.detached: + return + + # update received bytes counter + self.rxb += len(data) + + # update received bytes counter for parent interface + if self.parent_interface is not None: + self.parent_interface.rxb += len(data) + + # send received data to transport instance + self.owner.inbound(data, self) + + # the running reticulum transport instance will call this method whenever the interface must transmit a packet + def process_outgoing(self, data): + # do nothing if offline or detached + if not self.online or self.detached: + return + + # send to websocket server + try: + self.websocket.send(data) + except Exception as e: + RNS.log( + f"Exception occurred while transmitting via {self!s}", + RNS.LOG_ERROR, + ) + RNS.log(f"The contained exception was: {e!s}", RNS.LOG_ERROR) + return + + # update sent bytes counter + self.txb += len(data) + + # update received bytes counter for parent interface + if self.parent_interface is not None: + self.parent_interface.txb += len(data) + + # connect to the configured websocket server + def connect(self): + # do nothing if interface is detached + if self.detached: + return + + # connect to websocket server + try: + RNS.log(f"Connecting to Websocket for {self!s}...", RNS.LOG_DEBUG) + self.websocket = connect( + f"{self.target_url}", + max_size=None, + compression=None, + ) + RNS.log(f"Connected to Websocket for {self!s}", RNS.LOG_DEBUG) + self.read_loop() + except Exception as e: + RNS.log(f"{self} failed with error: {e}", RNS.LOG_ERROR) + + # auto reconnect after delay + RNS.log(f"Websocket disconnected for {self!s}...", RNS.LOG_DEBUG) + time.sleep(self.RECONNECT_DELAY_SECONDS) + self.connect() + + def read_loop(self): + self.online = True + + try: + for message in self.websocket: + self.process_incoming(message) + except Exception as e: + RNS.log(f"{self} read loop error: {e}", RNS.LOG_ERROR) + + self.online = False + + def detach(self): + # mark as offline + self.online = False + + # close websocket + if self.websocket is not None: + self.websocket.close() + + # mark as detached + self.detached = True + + +# set interface class RNS should use when importing this external interface +interface_class = WebsocketClientInterface diff --git a/meshchatx/src/backend/interfaces/WebsocketServerInterface.py b/meshchatx/src/backend/interfaces/WebsocketServerInterface.py new file mode 100644 index 0000000..e1454ce --- /dev/null +++ b/meshchatx/src/backend/interfaces/WebsocketServerInterface.py @@ -0,0 +1,165 @@ +import threading +import time + +import RNS +from RNS.Interfaces.Interface import Interface +from src.backend.interfaces.WebsocketClientInterface import WebsocketClientInterface +from websockets.sync.server import Server, ServerConnection, serve + + +class WebsocketServerInterface(Interface): + # TODO: required? + DEFAULT_IFAC_SIZE = 16 + + RESTART_DELAY_SECONDS = 5 + + def __str__(self): + return ( + f"WebsocketServerInterface[{self.name}/{self.listen_ip}:{self.listen_port}]" + ) + + def __init__(self, owner, configuration): + super().__init__() + + self.owner = owner + + self.IN = True + self.OUT = False + self.HW_MTU = 262144 # 256KiB + self.bitrate = 1_000_000_000 # 1Gbps + self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL + + self.server: Server | None = None + self.spawned_interfaces: [WebsocketClientInterface] = [] + + # parse config + ifconf = Interface.get_config_obj(configuration) + self.name = ifconf.get("name") + self.listen_ip = ifconf.get("listen_ip", None) + self.listen_port = ifconf.get("listen_port", None) + + # ensure listen ip is provided + if self.listen_ip is None: + msg = f"listen_ip is required for interface '{self.name}'" + raise SystemError(msg) + + # ensure listen port is provided + if self.listen_port is None: + msg = f"listen_port is required for interface '{self.name}'" + raise SystemError(msg) + + # convert listen port to int + self.listen_port = int(self.listen_port) + + # run websocket server + thread = threading.Thread(target=self.serve) + thread.daemon = True + thread.start() + + @property + def clients(self): + return len(self.spawned_interfaces) + + # TODO docs + def received_announce(self, from_spawned=False): + if from_spawned: + self.ia_freq_deque.append(time.time()) + + # TODO docs + def sent_announce(self, from_spawned=False): + if from_spawned: + self.oa_freq_deque.append(time.time()) + + # do nothing as the spawned child interface will take care of rx/tx + def process_incoming(self, data): + pass + + # do nothing as the spawned child interface will take care of rx/tx + def process_outgoing(self, data): + pass + + def serve(self): + # handle new websocket client connections + def on_websocket_client_connected(websocket: ServerConnection): + # create new child interface + RNS.log("Accepting incoming WebSocket connection", RNS.LOG_VERBOSE) + spawned_interface = WebsocketClientInterface( + self.owner, + { + "name": f"Client on {self.name}", + "target_host": websocket.remote_address[0], + "target_port": str(websocket.remote_address[1]), + }, + websocket=websocket, + ) + + # configure child interface + spawned_interface.IN = self.IN + spawned_interface.OUT = self.OUT + spawned_interface.HW_MTU = self.HW_MTU + spawned_interface.bitrate = self.bitrate + spawned_interface.mode = self.mode + spawned_interface.parent_interface = self + spawned_interface.online = True + + # TODO implement? + spawned_interface.announce_rate_target = None + spawned_interface.announce_rate_grace = None + spawned_interface.announce_rate_penalty = None + + # TODO ifac? + # TODO announce rates? + + # activate child interface + RNS.log( + f"Spawned new WebsocketClientInterface: {spawned_interface}", + RNS.LOG_VERBOSE, + ) + RNS.Transport.interfaces.append(spawned_interface) + + # associate child interface with this interface + while spawned_interface in self.spawned_interfaces: + self.spawned_interfaces.remove(spawned_interface) + self.spawned_interfaces.append(spawned_interface) + + # run read loop + spawned_interface.read_loop() + + # client must have disconnected as the read loop finished, so forget the spawned interface + self.spawned_interfaces.remove(spawned_interface) + + # run websocket server + try: + RNS.log(f"Starting Websocket server for {self!s}...", RNS.LOG_DEBUG) + with serve( + on_websocket_client_connected, + self.listen_ip, + self.listen_port, + compression=None, + ) as server: + self.online = True + self.server = server + server.serve_forever() + except Exception as e: + RNS.log(f"{self} failed with error: {e}", RNS.LOG_ERROR) + + # websocket server is no longer running, let's restart it + self.online = False + RNS.log(f"Websocket server stopped for {self!s}...", RNS.LOG_DEBUG) + time.sleep(self.RESTART_DELAY_SECONDS) + self.serve() + + def detach(self): + # mark as offline + self.online = False + + # stop websocket server + if self.server is not None: + self.server.shutdown() + + # mark as detached + self.detached = True + + +# set interface class RNS should use when importing this external interface +interface_class = WebsocketServerInterface diff --git a/meshchatx/src/backend/interfaces/__init__.py b/meshchatx/src/backend/interfaces/__init__.py new file mode 100644 index 0000000..8b8d1dd --- /dev/null +++ b/meshchatx/src/backend/interfaces/__init__.py @@ -0,0 +1 @@ +"""Shared transport interfaces for MeshChatX.""" diff --git a/meshchatx/src/backend/lxmf_message_fields.py b/meshchatx/src/backend/lxmf_message_fields.py new file mode 100644 index 0000000..0d34b93 --- /dev/null +++ b/meshchatx/src/backend/lxmf_message_fields.py @@ -0,0 +1,25 @@ +# helper class for passing around an lxmf audio field +class LxmfAudioField: + def __init__(self, audio_mode: int, audio_bytes: bytes): + self.audio_mode = audio_mode + self.audio_bytes = audio_bytes + + +# helper class for passing around an lxmf image field +class LxmfImageField: + def __init__(self, image_type: str, image_bytes: bytes): + self.image_type = image_type + self.image_bytes = image_bytes + + +# helper class for passing around an lxmf file attachment +class LxmfFileAttachment: + def __init__(self, file_name: str, file_bytes: bytes): + self.file_name = file_name + self.file_bytes = file_bytes + + +# helper class for passing around an lxmf file attachments field +class LxmfFileAttachmentsField: + def __init__(self, file_attachments: list[LxmfFileAttachment]): + self.file_attachments = file_attachments diff --git a/meshchatx/src/backend/map_manager.py b/meshchatx/src/backend/map_manager.py new file mode 100644 index 0000000..1e197c3 --- /dev/null +++ b/meshchatx/src/backend/map_manager.py @@ -0,0 +1,249 @@ +import math +import os +import sqlite3 +import threading +import time + +import requests +import RNS + + +class MapManager: + def __init__(self, config_manager, storage_dir): + self.config = config_manager + self.storage_dir = storage_dir + self._local = threading.local() + self._metadata_cache = None + self._export_progress = {} + + def get_connection(self, path): + if not hasattr(self._local, "connections"): + self._local.connections = {} + + if path not in self._local.connections: + if not os.path.exists(path): + return None + conn = sqlite3.connect(path, check_same_thread=False) + conn.row_factory = sqlite3.Row + self._local.connections[path] = conn + + return self._local.connections[path] + + def get_offline_path(self): + path = self.config.map_offline_path.get() + if path: + return path + + # Fallback to default if not set but file exists + default_path = os.path.join(self.storage_dir, "offline_map.mbtiles") + if os.path.exists(default_path): + return default_path + + return None + + def get_mbtiles_dir(self): + dir_path = self.config.map_mbtiles_dir.get() + if dir_path and os.path.isdir(dir_path): + return dir_path + return self.storage_dir + + def list_mbtiles(self): + mbtiles_dir = self.get_mbtiles_dir() + files = [] + if os.path.exists(mbtiles_dir): + for f in os.listdir(mbtiles_dir): + if f.endswith(".mbtiles"): + full_path = os.path.join(mbtiles_dir, f) + stats = os.stat(full_path) + files.append({ + "name": f, + "path": full_path, + "size": stats.st_size, + "mtime": stats.st_mtime, + "is_active": full_path == self.get_offline_path(), + }) + return sorted(files, key=lambda x: x["mtime"], reverse=True) + + def delete_mbtiles(self, filename): + mbtiles_dir = self.get_mbtiles_dir() + file_path = os.path.join(mbtiles_dir, filename) + if os.path.exists(file_path) and file_path.endswith(".mbtiles"): + if file_path == self.get_offline_path(): + self.config.map_offline_path.set(None) + self.config.map_offline_enabled.set(False) + os.remove(file_path) + self._metadata_cache = None + return True + return False + + def get_metadata(self): + path = self.get_offline_path() + if not path or not os.path.exists(path): + return None + + if self._metadata_cache and self._metadata_cache.get("path") == path: + return self._metadata_cache + + conn = self.get_connection(path) + if not conn: + return None + + try: + cursor = conn.cursor() + cursor.execute("SELECT name, value FROM metadata") + rows = cursor.fetchall() + metadata = {row["name"]: row["value"] for row in rows} + metadata["path"] = path + + # Basic validation: ensure it's raster (format is not pbf) + if metadata.get("format") == "pbf": + RNS.log("MBTiles file is in vector (PBF) format, which is not supported.", RNS.LOG_ERROR) + return None + + self._metadata_cache = metadata + return metadata + except Exception as e: + RNS.log(f"Error reading MBTiles metadata: {e}", RNS.LOG_ERROR) + return None + + def get_tile(self, z, x, y): + path = self.get_offline_path() + if not path or not os.path.exists(path): + return None + + conn = self.get_connection(path) + if not conn: + return None + + try: + # MBTiles uses TMS tiling scheme (y is flipped) + tms_y = (1 << z) - 1 - y + + cursor = conn.cursor() + cursor.execute( + "SELECT tile_data FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?", + (z, x, tms_y), + ) + row = cursor.fetchone() + if row: + return row["tile_data"] + + return None + except Exception as e: + RNS.log(f"Error reading MBTiles tile {z}/{x}/{y}: {e}", RNS.LOG_ERROR) + return None + + def start_export(self, export_id, bbox, min_zoom, max_zoom, name="Exported Map"): + """Start downloading tiles and creating an MBTiles file in a background thread.""" + thread = threading.Thread( + target=self._run_export, + args=(export_id, bbox, min_zoom, max_zoom, name), + daemon=True, + ) + self._export_progress[export_id] = { + "status": "starting", + "progress": 0, + "total": 0, + "current": 0, + "start_time": time.time(), + } + thread.start() + return export_id + + def get_export_status(self, export_id): + return self._export_progress.get(export_id) + + def _run_export(self, export_id, bbox, min_zoom, max_zoom, name): + # bbox: [min_lon, min_lat, max_lon, max_lat] + min_lon, min_lat, max_lon, max_lat = bbox + + # calculate total tiles + total_tiles = 0 + zoom_levels = range(min_zoom, max_zoom + 1) + for z in zoom_levels: + x1, y1 = self._lonlat_to_tile(min_lon, max_lat, z) + x2, y2 = self._lonlat_to_tile(max_lon, min_lat, z) + total_tiles += (x2 - x1 + 1) * (y2 - y1 + 1) + + self._export_progress[export_id]["total"] = total_tiles + self._export_progress[export_id]["status"] = "downloading" + + dest_path = os.path.join(self.storage_dir, f"export_{export_id}.mbtiles") + + try: + conn = sqlite3.connect(dest_path) + cursor = conn.cursor() + + # create schema + cursor.execute("CREATE TABLE metadata (name text, value text)") + cursor.execute("CREATE TABLE tiles (zoom_level integer, tile_column integer, tile_row integer, tile_data blob)") + cursor.execute("CREATE UNIQUE INDEX tile_index on tiles (zoom_level, tile_column, tile_row)") + + # insert metadata + metadata = [ + ("name", name), + ("type", "baselayer"), + ("version", "1.1"), + ("description", f"Exported from MeshChatX on {time.ctime()}"), + ("format", "png"), + ("bounds", f"{min_lon},{min_lat},{max_lon},{max_lat}"), + ] + cursor.executemany("INSERT INTO metadata VALUES (?, ?)", metadata) + + current_count = 0 + for z in zoom_levels: + x1, y1 = self._lonlat_to_tile(min_lon, max_lat, z) + x2, y2 = self._lonlat_to_tile(max_lon, min_lat, z) + + for x in range(x1, x2 + 1): + for y in range(y1, y2 + 1): + # check if we should stop (if we add a cancel mechanism) + + # download tile + tile_url = f"https://tile.openstreetmap.org/{z}/{x}/{y}.png" + try: + # wait a bit to be nice to OSM + time.sleep(0.1) + + response = requests.get(tile_url, headers={"User-Agent": "MeshChatX/1.0 MapExporter"}, timeout=10) + if response.status_code == 200: + # MBTiles uses TMS (y flipped) + tms_y = (1 << z) - 1 - y + cursor.execute( + "INSERT INTO tiles VALUES (?, ?, ?, ?)", + (z, x, tms_y, response.content), + ) + except Exception as e: + RNS.log(f"Export failed to download tile {z}/{x}/{y}: {e}", RNS.LOG_ERROR) + + current_count += 1 + self._export_progress[export_id]["current"] = current_count + self._export_progress[export_id]["progress"] = int((current_count / total_tiles) * 100) + + # commit after each zoom level + conn.commit() + + conn.close() + self._export_progress[export_id]["status"] = "completed" + self._export_progress[export_id]["file_path"] = dest_path + + except Exception as e: + RNS.log(f"Map export failed: {e}", RNS.LOG_ERROR) + self._export_progress[export_id]["status"] = "failed" + self._export_progress[export_id]["error"] = str(e) + if os.path.exists(dest_path): + os.remove(dest_path) + + def _lonlat_to_tile(self, lon, lat, zoom): + lat_rad = math.radians(lat) + n = 2.0 ** zoom + x = int((lon + 180.0) / 360.0 * n) + y = int((1.0 - math.log(math.tan(lat_rad) + (1 / math.cos(lat_rad))) / math.pi) / 2.0 * n) + return x, y + + def close(self): + if hasattr(self._local, "connections"): + for conn in self._local.connections.values(): + conn.close() + self._local.connections = {} + self._metadata_cache = None diff --git a/meshchatx/src/backend/message_handler.py b/meshchatx/src/backend/message_handler.py new file mode 100644 index 0000000..076b08b --- /dev/null +++ b/meshchatx/src/backend/message_handler.py @@ -0,0 +1,66 @@ +from .database import Database + + +class MessageHandler: + def __init__(self, db: Database): + self.db = db + + def get_conversation_messages(self, local_hash, destination_hash, limit=100, offset=0, after_id=None, before_id=None): + query = """ + SELECT * FROM lxmf_messages + WHERE ((source_hash = ? AND destination_hash = ?) + OR (destination_hash = ? AND source_hash = ?)) + """ + params = [local_hash, destination_hash, local_hash, destination_hash] + + if after_id: + query += " AND id > ?" + params.append(after_id) + if before_id: + query += " AND id < ?" + params.append(before_id) + + query += " ORDER BY id DESC LIMIT ? OFFSET ?" + params.extend([limit, offset]) + + return self.db.provider.fetchall(query, params) + + def delete_conversation(self, local_hash, destination_hash): + query = """ + DELETE FROM lxmf_messages + WHERE ((source_hash = ? AND destination_hash = ?) + OR (destination_hash = ? AND source_hash = ?)) + """ + self.db.provider.execute(query, [local_hash, destination_hash, local_hash, destination_hash]) + + def search_messages(self, local_hash, search_term): + like_term = f"%{search_term}%" + query = """ + SELECT source_hash, destination_hash, MAX(timestamp) as max_ts + FROM lxmf_messages + WHERE (source_hash = ? OR destination_hash = ?) + AND (title LIKE ? OR content LIKE ? OR source_hash LIKE ? OR destination_hash LIKE ?) + GROUP BY source_hash, destination_hash + """ + params = [local_hash, local_hash, like_term, like_term, like_term, like_term] + return self.db.provider.fetchall(query, params) + + def get_conversations(self, local_hash): + # Implementation moved from get_conversations DAO but with local_hash filter + query = """ + SELECT m1.* FROM lxmf_messages m1 + JOIN ( + SELECT + CASE WHEN source_hash = ? THEN destination_hash ELSE source_hash END as peer_hash, + MAX(timestamp) as max_ts + FROM lxmf_messages + WHERE source_hash = ? OR destination_hash = ? + GROUP BY peer_hash + ) m2 ON (CASE WHEN m1.source_hash = ? THEN m1.destination_hash ELSE m1.source_hash END = m2.peer_hash + AND m1.timestamp = m2.max_ts) + WHERE m1.source_hash = ? OR m1.destination_hash = ? + ORDER BY m1.timestamp DESC + """ + params = [local_hash, local_hash, local_hash, local_hash, local_hash, local_hash] + return self.db.provider.fetchall(query, params) + diff --git a/meshchatx/src/backend/rncp_handler.py b/meshchatx/src/backend/rncp_handler.py new file mode 100644 index 0000000..295f0a2 --- /dev/null +++ b/meshchatx/src/backend/rncp_handler.py @@ -0,0 +1,421 @@ +import asyncio +import os +import shutil +import time +from collections.abc import Callable + +import RNS + + +class RNCPHandler: + APP_NAME = "rncp" + REQ_FETCH_NOT_ALLOWED = 0xF0 + + def __init__(self, reticulum_instance, identity, storage_dir): + self.reticulum = reticulum_instance + self.identity = identity + self.storage_dir = storage_dir + self.active_transfers = {} + self.receive_destination = None + self.fetch_jail = None + self.fetch_auto_compress = True + self.allow_overwrite_on_receive = False + self.allowed_identity_hashes = [] + + def setup_receive_destination(self, allowed_hashes=None, fetch_allowed=False, fetch_jail=None, allow_overwrite=False): + if allowed_hashes: + self.allowed_identity_hashes = [bytes.fromhex(h) if isinstance(h, str) else h for h in allowed_hashes] + + self.fetch_jail = fetch_jail + self.allow_overwrite_on_receive = allow_overwrite + + identity_path = os.path.join(RNS.Reticulum.identitypath, self.APP_NAME) + if os.path.isfile(identity_path): + receive_identity = RNS.Identity.from_file(identity_path) + else: + receive_identity = RNS.Identity() + receive_identity.to_file(identity_path) + + self.receive_destination = RNS.Destination( + receive_identity, + RNS.Destination.IN, + RNS.Destination.SINGLE, + self.APP_NAME, + "receive", + ) + + self.receive_destination.set_link_established_callback(self._client_link_established) + + if fetch_allowed: + self.receive_destination.register_request_handler( + "fetch_file", + response_generator=self._fetch_request, + allow=RNS.Destination.ALLOW_LIST, + allowed_list=self.allowed_identity_hashes, + ) + + return self.receive_destination.hash.hex() + + def _client_link_established(self, link): + link.set_remote_identified_callback(self._receive_sender_identified) + link.set_resource_strategy(RNS.Link.ACCEPT_APP) + link.set_resource_callback(self._receive_resource_callback) + link.set_resource_started_callback(self._receive_resource_started) + link.set_resource_concluded_callback(self._receive_resource_concluded) + + def _receive_sender_identified(self, link, identity): + if identity.hash not in self.allowed_identity_hashes: + link.teardown() + + def _receive_resource_callback(self, resource): + sender_identity = resource.link.get_remote_identity() + if sender_identity and sender_identity.hash in self.allowed_identity_hashes: + return True + return False + + def _receive_resource_started(self, resource): + transfer_id = resource.hash.hex() + self.active_transfers[transfer_id] = { + "resource": resource, + "status": "receiving", + "started_at": time.time(), + } + + def _receive_resource_concluded(self, resource): + transfer_id = resource.hash.hex() + if resource.status == RNS.Resource.COMPLETE: + if resource.metadata: + try: + filename = os.path.basename(resource.metadata["name"].decode("utf-8")) + save_dir = os.path.join(self.storage_dir, "rncp_received") + os.makedirs(save_dir, exist_ok=True) + + saved_filename = os.path.join(save_dir, filename) + counter = 0 + + if self.allow_overwrite_on_receive: + if os.path.isfile(saved_filename): + try: + os.unlink(saved_filename) + except OSError: + # Failed to delete existing file, which is fine, + # we'll just fall through to the naming loop + pass + + while os.path.isfile(saved_filename): + counter += 1 + base, ext = os.path.splitext(filename) + saved_filename = os.path.join(save_dir, f"{base}.{counter}{ext}") + + shutil.move(resource.data.name, saved_filename) + + if transfer_id in self.active_transfers: + self.active_transfers[transfer_id]["status"] = "completed" + self.active_transfers[transfer_id]["saved_path"] = saved_filename + self.active_transfers[transfer_id]["filename"] = filename + except Exception as e: + if transfer_id in self.active_transfers: + self.active_transfers[transfer_id]["status"] = "error" + self.active_transfers[transfer_id]["error"] = str(e) + elif transfer_id in self.active_transfers: + self.active_transfers[transfer_id]["status"] = "failed" + + def _fetch_request(self, path, data, request_id, link_id, remote_identity, requested_at): + if self.fetch_jail: + if data.startswith(self.fetch_jail + "/"): + data = data.replace(self.fetch_jail + "/", "") + file_path = os.path.abspath(os.path.expanduser(f"{self.fetch_jail}/{data}")) + if not file_path.startswith(self.fetch_jail + "/"): + return self.REQ_FETCH_NOT_ALLOWED + else: + file_path = os.path.abspath(os.path.expanduser(data)) + + target_link = None + for link in RNS.Transport.active_links: + if link.link_id == link_id: + target_link = link + break + + if not os.path.isfile(file_path): + return False + + if target_link: + try: + metadata = {"name": os.path.basename(file_path).encode("utf-8")} + RNS.Resource( + open(file_path, "rb"), + target_link, + metadata=metadata, + auto_compress=self.fetch_auto_compress, + ) + return True + except Exception: + return False + + return None + + async def send_file( + self, + destination_hash: bytes, + file_path: str, + timeout: float = RNS.Transport.PATH_REQUEST_TIMEOUT, + on_progress: Callable[[float], None] | None = None, + no_compress: bool = False, + ): + file_path = os.path.expanduser(file_path) + if not os.path.isfile(file_path): + msg = f"File not found: {file_path}" + raise FileNotFoundError(msg) + + if not RNS.Transport.has_path(destination_hash): + RNS.Transport.request_path(destination_hash) + + timeout_after = time.time() + timeout + while not RNS.Transport.has_path(destination_hash) and time.time() < timeout_after: + await asyncio.sleep(0.1) + + if not RNS.Transport.has_path(destination_hash): + msg = "Path not found to destination" + raise TimeoutError(msg) + + receiver_identity = RNS.Identity.recall(destination_hash) + receiver_destination = RNS.Destination( + receiver_identity, + RNS.Destination.OUT, + RNS.Destination.SINGLE, + self.APP_NAME, + "receive", + ) + + link = RNS.Link(receiver_destination) + timeout_after = time.time() + timeout + while link.status != RNS.Link.ACTIVE and time.time() < timeout_after: + await asyncio.sleep(0.1) + + if link.status != RNS.Link.ACTIVE: + msg = "Could not establish link to destination" + raise TimeoutError(msg) + + link.identify(self.identity) + + auto_compress = not no_compress + metadata = {"name": os.path.basename(file_path).encode("utf-8")} + + def progress_callback(resource): + if on_progress: + progress = resource.get_progress() + on_progress(progress) + + resource = RNS.Resource( + open(file_path, "rb"), + link, + metadata=metadata, + callback=progress_callback, + progress_callback=progress_callback, + auto_compress=auto_compress, + ) + + transfer_id = resource.hash.hex() + self.active_transfers[transfer_id] = { + "resource": resource, + "status": "sending", + "started_at": time.time(), + "file_path": file_path, + } + + while resource.status < RNS.Resource.COMPLETE: + await asyncio.sleep(0.1) + if resource.status > RNS.Resource.COMPLETE: + msg = "File was not accepted by destination" + raise Exception(msg) + + if resource.status == RNS.Resource.COMPLETE: + if transfer_id in self.active_transfers: + self.active_transfers[transfer_id]["status"] = "completed" + link.teardown() + return { + "transfer_id": transfer_id, + "status": "completed", + "file_path": file_path, + } + if transfer_id in self.active_transfers: + self.active_transfers[transfer_id]["status"] = "failed" + link.teardown() + msg = "Transfer failed" + raise Exception(msg) + + async def fetch_file( + self, + destination_hash: bytes, + file_path: str, + timeout: float = RNS.Transport.PATH_REQUEST_TIMEOUT, + on_progress: Callable[[float], None] | None = None, + save_path: str | None = None, + allow_overwrite: bool = False, + ): + if not RNS.Transport.has_path(destination_hash): + RNS.Transport.request_path(destination_hash) + + timeout_after = time.time() + timeout + while not RNS.Transport.has_path(destination_hash) and time.time() < timeout_after: + await asyncio.sleep(0.1) + + if not RNS.Transport.has_path(destination_hash): + msg = "Path not found to destination" + raise TimeoutError(msg) + + listener_identity = RNS.Identity.recall(destination_hash) + listener_destination = RNS.Destination( + listener_identity, + RNS.Destination.OUT, + RNS.Destination.SINGLE, + self.APP_NAME, + "receive", + ) + + link = RNS.Link(listener_destination) + timeout_after = time.time() + timeout + while link.status != RNS.Link.ACTIVE and time.time() < timeout_after: + await asyncio.sleep(0.1) + + if link.status != RNS.Link.ACTIVE: + msg = "Could not establish link to destination" + raise TimeoutError(msg) + + link.identify(self.identity) + + request_resolved = False + request_status = "unknown" + resource_resolved = False + resource_status = "unrequested" + current_resource = None + + def request_response(request_receipt): + nonlocal request_resolved, request_status + if not request_receipt.response: + request_status = "not_found" + elif request_receipt.response is None: + request_status = "remote_error" + elif request_receipt.response == self.REQ_FETCH_NOT_ALLOWED: + request_status = "fetch_not_allowed" + else: + request_status = "found" + request_resolved = True + + def request_failed(request_receipt): + nonlocal request_resolved, request_status + request_status = "unknown" + request_resolved = True + + def fetch_resource_started(resource): + nonlocal resource_status, current_resource + current_resource = resource + + def progress_callback(resource): + if on_progress: + progress = resource.get_progress() + on_progress(progress) + + current_resource.progress_callback(progress_callback) + resource_status = "started" + + saved_filename = None + + def fetch_resource_concluded(resource): + nonlocal resource_resolved, resource_status, saved_filename + if resource.status == RNS.Resource.COMPLETE: + if resource.metadata: + try: + filename = os.path.basename(resource.metadata["name"].decode("utf-8")) + if save_path: + save_dir = os.path.abspath(os.path.expanduser(save_path)) + os.makedirs(save_dir, exist_ok=True) + saved_filename = os.path.join(save_dir, filename) + else: + saved_filename = filename + + counter = 0 + if allow_overwrite: + if os.path.isfile(saved_filename): + try: + os.unlink(saved_filename) + except OSError: + # Failed to delete existing file, which is fine, + # we'll just fall through to the naming loop + pass + + while os.path.isfile(saved_filename): + counter += 1 + base, ext = os.path.splitext(filename) + saved_filename = os.path.join( + os.path.dirname(saved_filename) if save_path else ".", + f"{base}.{counter}{ext}", + ) + + shutil.move(resource.data.name, saved_filename) + resource_status = "completed" + except Exception as e: + resource_status = "error" + raise e + else: + resource_status = "error" + else: + resource_status = "failed" + + resource_resolved = True + + link.set_resource_strategy(RNS.Link.ACCEPT_ALL) + link.set_resource_started_callback(fetch_resource_started) + link.set_resource_concluded_callback(fetch_resource_concluded) + link.request("fetch_file", data=file_path, response_callback=request_response, failed_callback=request_failed) + + while not request_resolved: + await asyncio.sleep(0.1) + + if request_status == "fetch_not_allowed": + link.teardown() + msg = "Fetch request not allowed by remote" + raise PermissionError(msg) + if request_status == "not_found": + link.teardown() + msg = f"File not found on remote: {file_path}" + raise FileNotFoundError(msg) + if request_status == "remote_error": + link.teardown() + msg = "Remote error during fetch request" + raise Exception(msg) + if request_status == "unknown": + link.teardown() + msg = "Unknown error during fetch request" + raise Exception(msg) + + while not resource_resolved: + await asyncio.sleep(0.1) + + if resource_status == "completed": + link.teardown() + return { + "status": "completed", + "file_path": saved_filename, + } + link.teardown() + msg = f"Transfer failed: {resource_status}" + raise Exception(msg) + + def get_transfer_status(self, transfer_id: str): + if transfer_id in self.active_transfers: + transfer = self.active_transfers[transfer_id] + resource = transfer.get("resource") + if resource: + progress = resource.get_progress() + return { + "transfer_id": transfer_id, + "status": transfer["status"], + "progress": progress, + "file_path": transfer.get("file_path"), + "saved_path": transfer.get("saved_path"), + "filename": transfer.get("filename"), + "error": transfer.get("error"), + } + return None + diff --git a/meshchatx/src/backend/rnprobe_handler.py b/meshchatx/src/backend/rnprobe_handler.py new file mode 100644 index 0000000..a71d15b --- /dev/null +++ b/meshchatx/src/backend/rnprobe_handler.py @@ -0,0 +1,137 @@ +import asyncio +import os +import time + +import RNS + + +class RNProbeHandler: + DEFAULT_PROBE_SIZE = 16 + DEFAULT_TIMEOUT = 12 + + def __init__(self, reticulum_instance, identity): + self.reticulum = reticulum_instance + self.identity = identity + + async def probe_destination( + self, + destination_hash: bytes, + full_name: str, + size: int = DEFAULT_PROBE_SIZE, + timeout: float | None = None, + wait: float = 0, + probes: int = 1, + ): + try: + app_name, aspects = RNS.Destination.app_and_aspects_from_name(full_name) + except Exception as e: + msg = f"Invalid destination name: {e}" + raise ValueError(msg) + + if not RNS.Transport.has_path(destination_hash): + RNS.Transport.request_path(destination_hash) + + timeout_after = time.time() + (timeout or self.DEFAULT_TIMEOUT + self.reticulum.get_first_hop_timeout(destination_hash)) + while not RNS.Transport.has_path(destination_hash) and time.time() < timeout_after: + await asyncio.sleep(0.1) + + if not RNS.Transport.has_path(destination_hash): + msg = "Path request timed out" + raise TimeoutError(msg) + + server_identity = RNS.Identity.recall(destination_hash) + request_destination = RNS.Destination( + server_identity, + RNS.Destination.OUT, + RNS.Destination.SINGLE, + app_name, + *aspects, + ) + + results = [] + sent = 0 + + while probes > 0: + if sent > 0: + await asyncio.sleep(wait) + + try: + probe = RNS.Packet(request_destination, os.urandom(size)) + probe.pack() + except OSError: + msg = f"Probe packet size of {len(probe.raw)} bytes exceeds MTU of {RNS.Reticulum.MTU} bytes" + raise ValueError(msg) + + receipt = probe.send() + sent += 1 + + next_hop = self.reticulum.get_next_hop(destination_hash) + via_str = f" via {RNS.prettyhexrep(next_hop)}" if next_hop else "" + if_name = self.reticulum.get_next_hop_if_name(destination_hash) + if_str = f" on {if_name}" if if_name and if_name != "None" else "" + + timeout_after = time.time() + (timeout or self.DEFAULT_TIMEOUT + self.reticulum.get_first_hop_timeout(destination_hash)) + while receipt.status == RNS.PacketReceipt.SENT and time.time() < timeout_after: + await asyncio.sleep(0.1) + + result: dict = { + "probe_number": sent, + "size": size, + "destination": RNS.prettyhexrep(destination_hash), + "via": via_str, + "interface": if_str, + "status": "timeout", + } + + if time.time() > timeout_after: + result["status"] = "timeout" + elif receipt.status == RNS.PacketReceipt.DELIVERED: + hops = RNS.Transport.hops_to(destination_hash) + rtt = receipt.get_rtt() + + if rtt >= 1: + rtt_str = f"{round(rtt, 3)} seconds" + else: + rtt_str = f"{round(rtt * 1000, 3)} milliseconds" + + reception_stats = {} + if self.reticulum.is_connected_to_shared_instance: + reception_rssi = self.reticulum.get_packet_rssi(receipt.proof_packet.packet_hash) + reception_snr = self.reticulum.get_packet_snr(receipt.proof_packet.packet_hash) + reception_q = self.reticulum.get_packet_q(receipt.proof_packet.packet_hash) + + if reception_rssi is not None: + reception_stats["rssi"] = reception_rssi + if reception_snr is not None: + reception_stats["snr"] = reception_snr + if reception_q is not None: + reception_stats["quality"] = reception_q + elif receipt.proof_packet: + if receipt.proof_packet.rssi is not None: + reception_stats["rssi"] = receipt.proof_packet.rssi + if receipt.proof_packet.snr is not None: + reception_stats["snr"] = receipt.proof_packet.snr + + result.update( + { + "status": "delivered", + "hops": hops, + "rtt": rtt, + "rtt_string": rtt_str, + "reception_stats": reception_stats, + }, + ) + else: + result["status"] = "failed" + + results.append(result) + probes -= 1 + + return { + "results": results, + "sent": sent, + "delivered": sum(1 for r in results if r["status"] == "delivered"), + "timeouts": sum(1 for r in results if r["status"] == "timeout"), + "failed": sum(1 for r in results if r["status"] == "failed"), + } + diff --git a/meshchatx/src/backend/rnstatus_handler.py b/meshchatx/src/backend/rnstatus_handler.py new file mode 100644 index 0000000..40ec894 --- /dev/null +++ b/meshchatx/src/backend/rnstatus_handler.py @@ -0,0 +1,184 @@ +import time +from typing import Any + + +def size_str(num, suffix="B"): + units = ["", "K", "M", "G", "T", "P", "E", "Z"] + last_unit = "Y" + + if suffix == "b": + num *= 8 + units = ["", "K", "M", "G", "T", "P", "E", "Z"] + last_unit = "Y" + + for unit in units: + if abs(num) < 1000.0: + if unit == "": + return f"{num:.0f} {unit}{suffix}" + return f"{num:.2f} {unit}{suffix}" + num /= 1000.0 + + return f"{num:.2f}{last_unit}{suffix}" + + +class RNStatusHandler: + def __init__(self, reticulum_instance): + self.reticulum = reticulum_instance + + def get_status(self, include_link_stats: bool = False, sorting: str | None = None, sort_reverse: bool = False): + stats = None + link_count = None + + try: + if include_link_stats: + link_count = self.reticulum.get_link_count() + except Exception as e: + # We can't do much here if the reticulum instance fails + print(f"Failed to get link count: {e}") + + try: + stats = self.reticulum.get_interface_stats() + except Exception as e: + # We can't do much here if the reticulum instance fails + print(f"Failed to get interface stats: {e}") + + if stats is None: + return { + "interfaces": [], + "link_count": link_count, + } + + interfaces = stats.get("interfaces", []) + + if sorting and isinstance(sorting, str): + sorting = sorting.lower() + if sorting in ("rate", "bitrate"): + interfaces.sort(key=lambda i: i.get("bitrate", 0) or 0, reverse=sort_reverse) + elif sorting == "rx": + interfaces.sort(key=lambda i: i.get("rxb", 0) or 0, reverse=sort_reverse) + elif sorting == "tx": + interfaces.sort(key=lambda i: i.get("txb", 0) or 0, reverse=sort_reverse) + elif sorting == "rxs": + interfaces.sort(key=lambda i: i.get("rxs", 0) or 0, reverse=sort_reverse) + elif sorting == "txs": + interfaces.sort(key=lambda i: i.get("txs", 0) or 0, reverse=sort_reverse) + elif sorting == "traffic": + interfaces.sort( + key=lambda i: (i.get("rxb", 0) or 0) + (i.get("txb", 0) or 0), + reverse=sort_reverse, + ) + elif sorting in ("announces", "announce"): + interfaces.sort( + key=lambda i: (i.get("incoming_announce_frequency", 0) or 0) + + (i.get("outgoing_announce_frequency", 0) or 0), + reverse=sort_reverse, + ) + elif sorting == "arx": + interfaces.sort( + key=lambda i: i.get("incoming_announce_frequency", 0) or 0, + reverse=sort_reverse, + ) + elif sorting == "atx": + interfaces.sort( + key=lambda i: i.get("outgoing_announce_frequency", 0) or 0, + reverse=sort_reverse, + ) + elif sorting == "held": + interfaces.sort(key=lambda i: i.get("held_announces", 0) or 0, reverse=sort_reverse) + + formatted_interfaces = [] + for ifstat in interfaces: + name = ifstat.get("name", "") + + if name.startswith("LocalInterface[") or name.startswith("TCPInterface[Client") or name.startswith("BackboneInterface[Client on"): + continue + + formatted_if: dict[str, Any] = { + "name": name, + "status": "Up" if ifstat.get("status") else "Down", + } + + mode = ifstat.get("mode") + if mode == 1: + formatted_if["mode"] = "Access Point" + elif mode == 2: + formatted_if["mode"] = "Point-to-Point" + elif mode == 3: + formatted_if["mode"] = "Roaming" + elif mode == 4: + formatted_if["mode"] = "Boundary" + elif mode == 5: + formatted_if["mode"] = "Gateway" + else: + formatted_if["mode"] = "Full" + + if "bitrate" in ifstat and ifstat["bitrate"] is not None: + formatted_if["bitrate"] = size_str(ifstat["bitrate"], "b") + "ps" + + if "rxb" in ifstat: + formatted_if["rx_bytes"] = ifstat["rxb"] + formatted_if["rx_bytes_str"] = size_str(ifstat["rxb"]) + if "txb" in ifstat: + formatted_if["tx_bytes"] = ifstat["txb"] + formatted_if["tx_bytes_str"] = size_str(ifstat["txb"]) + if "rxs" in ifstat: + formatted_if["rx_packets"] = ifstat["rxs"] + if "txs" in ifstat: + formatted_if["tx_packets"] = ifstat["txs"] + + if "clients" in ifstat and ifstat["clients"] is not None: + formatted_if["clients"] = ifstat["clients"] + + if "noise_floor" in ifstat and ifstat["noise_floor"] is not None: + formatted_if["noise_floor"] = f"{ifstat['noise_floor']} dBm" + + if "interference" in ifstat and ifstat["interference"] is not None: + formatted_if["interference"] = f"{ifstat['interference']} dBm" + + if "cpu_load" in ifstat and ifstat["cpu_load"] is not None: + formatted_if["cpu_load"] = f"{ifstat['cpu_load']}%" + + if "cpu_temp" in ifstat and ifstat["cpu_temp"] is not None: + formatted_if["cpu_temp"] = f"{ifstat['cpu_temp']}°C" + + if "mem_load" in ifstat and ifstat["mem_load"] is not None: + formatted_if["mem_load"] = f"{ifstat['mem_load']}%" + + if "battery_percent" in ifstat and ifstat["battery_percent"] is not None: + formatted_if["battery_percent"] = ifstat["battery_percent"] + if "battery_state" in ifstat: + formatted_if["battery_state"] = ifstat["battery_state"] + + if "airtime_short" in ifstat and "airtime_long" in ifstat: + formatted_if["airtime"] = { + "short": ifstat["airtime_short"], + "long": ifstat["airtime_long"], + } + + if "channel_load_short" in ifstat and "channel_load_long" in ifstat: + formatted_if["channel_load"] = { + "short": ifstat["channel_load_short"], + "long": ifstat["channel_load_long"], + } + + if "peers" in ifstat and ifstat["peers"] is not None: + formatted_if["peers"] = ifstat["peers"] + + if "incoming_announce_frequency" in ifstat: + formatted_if["incoming_announce_frequency"] = ifstat["incoming_announce_frequency"] + if "outgoing_announce_frequency" in ifstat: + formatted_if["outgoing_announce_frequency"] = ifstat["outgoing_announce_frequency"] + if "held_announces" in ifstat: + formatted_if["held_announces"] = ifstat["held_announces"] + + if "ifac_netname" in ifstat and ifstat["ifac_netname"] is not None: + formatted_if["network_name"] = ifstat["ifac_netname"] + + formatted_interfaces.append(formatted_if) + + return { + "interfaces": formatted_interfaces, + "link_count": link_count, + "timestamp": time.time(), + } + diff --git a/meshchatx/src/backend/sideband_commands.py b/meshchatx/src/backend/sideband_commands.py new file mode 100644 index 0000000..d28d9f0 --- /dev/null +++ b/meshchatx/src/backend/sideband_commands.py @@ -0,0 +1,3 @@ +# https://github.com/markqvist/Sideband/blob/e515889e210037f881c201e0d627a7b09a48eb69/sbapp/sideband/sense.py#L11 +class SidebandCommands: + TELEMETRY_REQUEST = 0x01 diff --git a/meshchatx/src/backend/telephone_manager.py b/meshchatx/src/backend/telephone_manager.py new file mode 100644 index 0000000..19b9651 --- /dev/null +++ b/meshchatx/src/backend/telephone_manager.py @@ -0,0 +1,95 @@ +import asyncio +import time + +import RNS +from LXST import Telephone + + +class TelephoneManager: + def __init__(self, identity: RNS.Identity, config_manager=None): + self.identity = identity + self.config_manager = config_manager + self.telephone = None + self.on_ringing_callback = None + self.on_established_callback = None + self.on_ended_callback = None + + self.call_start_time = None + self.call_status_at_end = None + self.call_is_incoming = False + + def init_telephone(self): + if self.telephone is not None: + return + + self.telephone = Telephone(self.identity) + # Disable busy tone played on caller side when remote side rejects, or doesn't answer + self.telephone.set_busy_tone_time(0) + self.telephone.set_ringing_callback(self.on_telephone_ringing) + self.telephone.set_established_callback(self.on_telephone_call_established) + self.telephone.set_ended_callback(self.on_telephone_call_ended) + + def teardown(self): + if self.telephone is not None: + self.telephone.teardown() + self.telephone = None + + def register_ringing_callback(self, callback): + self.on_ringing_callback = callback + + def register_established_callback(self, callback): + self.on_established_callback = callback + + def register_ended_callback(self, callback): + self.on_ended_callback = callback + + def on_telephone_ringing(self, caller_identity: RNS.Identity): + self.call_start_time = time.time() + self.call_is_incoming = True + if self.on_ringing_callback: + self.on_ringing_callback(caller_identity) + + def on_telephone_call_established(self, caller_identity: RNS.Identity): + # Update start time to when it was actually established for duration calculation + self.call_start_time = time.time() + if self.on_established_callback: + self.on_established_callback(caller_identity) + + def on_telephone_call_ended(self, caller_identity: RNS.Identity): + # Capture status just before ending if possible, or use the last known status + if self.telephone: + self.call_status_at_end = self.telephone.call_status + + if self.on_ended_callback: + self.on_ended_callback(caller_identity) + + def announce(self, attached_interface=None): + if self.telephone: + self.telephone.announce(attached_interface=attached_interface) + + async def initiate(self, destination_hash: bytes, timeout_seconds: int = 15): + if self.telephone is None: + msg = "Telephone is not initialized" + raise RuntimeError(msg) + + # Find destination identity + destination_identity = RNS.Identity.recall(destination_hash) + if destination_identity is None: + # If not found by identity hash, try as destination hash + destination_identity = RNS.Identity.recall(destination_hash) # Identity.recall takes identity hash + + if destination_identity is None: + msg = "Destination identity not found" + raise RuntimeError(msg) + + # In LXST, we just call the identity. Telephone class handles path requests. + # But we might want to ensure a path exists first for better UX, + # similar to how the old MeshChat did it. + + # For now, let's just use the telephone.call method which is threaded. + # We need to run it in a thread since it might block. + self.call_start_time = time.time() + self.call_is_incoming = False + await asyncio.to_thread(self.telephone.call, destination_identity) + return self.telephone.active_call + diff --git a/meshchatx/src/backend/translator_handler.py b/meshchatx/src/backend/translator_handler.py new file mode 100644 index 0000000..e735fab --- /dev/null +++ b/meshchatx/src/backend/translator_handler.py @@ -0,0 +1,363 @@ +import os +import re +import shutil +import subprocess +from typing import Any + +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + +try: + from argostranslate import package, translate + HAS_ARGOS_LIB = True +except ImportError: + HAS_ARGOS_LIB = False + +HAS_ARGOS_CLI = shutil.which("argos-translate") is not None +HAS_ARGOS = HAS_ARGOS_LIB or HAS_ARGOS_CLI + +LANGUAGE_CODE_TO_NAME = { + "en": "English", + "de": "German", + "es": "Spanish", + "fr": "French", + "it": "Italian", + "pt": "Portuguese", + "ru": "Russian", + "zh": "Chinese", + "ja": "Japanese", + "ko": "Korean", + "ar": "Arabic", + "hi": "Hindi", + "nl": "Dutch", + "pl": "Polish", + "tr": "Turkish", + "sv": "Swedish", + "da": "Danish", + "no": "Norwegian", + "fi": "Finnish", + "cs": "Czech", + "ro": "Romanian", + "hu": "Hungarian", + "el": "Greek", + "he": "Hebrew", + "th": "Thai", + "vi": "Vietnamese", + "id": "Indonesian", + "uk": "Ukrainian", + "bg": "Bulgarian", + "hr": "Croatian", + "sk": "Slovak", + "sl": "Slovenian", + "et": "Estonian", + "lv": "Latvian", + "lt": "Lithuanian", + "mt": "Maltese", + "ga": "Irish", + "cy": "Welsh", +} + + +class TranslatorHandler: + def __init__(self, libretranslate_url: str | None = None): + self.libretranslate_url = libretranslate_url or os.getenv("LIBRETRANSLATE_URL", "http://localhost:5000") + self.has_argos = HAS_ARGOS + self.has_argos_lib = HAS_ARGOS_LIB + self.has_argos_cli = HAS_ARGOS_CLI + self.has_requests = HAS_REQUESTS + + def get_supported_languages(self, libretranslate_url: str | None = None): + languages = [] + url = libretranslate_url or self.libretranslate_url + + if self.has_requests: + try: + response = requests.get(f"{url}/languages", timeout=5) + if response.status_code == 200: + libretranslate_langs = response.json() + languages.extend( + { + "code": lang.get("code"), + "name": lang.get("name"), + "source": "libretranslate", + } + for lang in libretranslate_langs + ) + return languages + except Exception as e: + # Log or handle the exception appropriately + print(f"Failed to fetch LibreTranslate languages: {e}") + + if self.has_argos_lib: + try: + installed_packages = package.get_installed_packages() + argos_langs = set() + for pkg in installed_packages: + argos_langs.add((pkg.from_code, pkg.from_name)) + argos_langs.add((pkg.to_code, pkg.to_name)) + + for code, name in sorted(argos_langs): + languages.append( + { + "code": code, + "name": name, + "source": "argos", + }, + ) + except Exception as e: + print(f"Failed to fetch Argos languages: {e}") + elif self.has_argos_cli: + try: + cli_langs = self._get_argos_languages_cli() + languages.extend(cli_langs) + except Exception as e: + print(f"Failed to fetch Argos languages via CLI: {e}") + + return languages + + def translate_text( + self, + text: str, + source_lang: str, + target_lang: str, + use_argos: bool = False, + libretranslate_url: str | None = None, + ) -> dict[str, Any]: + if not text: + msg = "Text cannot be empty" + raise ValueError(msg) + + if use_argos and self.has_argos: + return self._translate_argos(text, source_lang, target_lang) + + if self.has_requests: + try: + url = libretranslate_url or self.libretranslate_url + return self._translate_libretranslate(text, source_lang=source_lang, target_lang=target_lang, libretranslate_url=url) + except Exception as e: + if self.has_argos: + return self._translate_argos(text, source_lang, target_lang) + raise e + + if self.has_argos: + return self._translate_argos(text, source_lang, target_lang) + + msg = "No translation backend available. Install requests for LibreTranslate or argostranslate for local translation." + raise RuntimeError(msg) + + def _translate_libretranslate(self, text: str, source_lang: str, target_lang: str, libretranslate_url: str | None = None) -> dict[str, Any]: + if not self.has_requests: + msg = "requests library not available" + raise RuntimeError(msg) + + url = libretranslate_url or self.libretranslate_url + response = requests.post( + f"{url}/translate", + json={ + "q": text, + "source": source_lang, + "target": target_lang, + "format": "text", + }, + timeout=30, + ) + + if response.status_code != 200: + msg = f"LibreTranslate API error: {response.status_code} - {response.text}" + raise RuntimeError(msg) + + result = response.json() + return { + "translated_text": result.get("translatedText", ""), + "source_lang": result.get("detectedLanguage", {}).get("language", source_lang), + "target_lang": target_lang, + "source": "libretranslate", + } + + def _translate_argos(self, text: str, source_lang: str, target_lang: str) -> dict[str, Any]: + if source_lang == "auto": + if self.has_argos_lib: + detected_lang = self._detect_language(text) + if detected_lang: + source_lang = detected_lang + else: + msg = "Could not auto-detect language. Please select a source language manually." + raise ValueError(msg) + else: + msg = ( + "Auto-detection is not supported with CLI-only installation. " + "Please select a source language manually or install the Python library: pip install argostranslate" + ) + raise ValueError(msg) + + if self.has_argos_lib: + return self._translate_argos_lib(text, source_lang, target_lang) + if self.has_argos_cli: + return self._translate_argos_cli(text, source_lang, target_lang) + msg = "Argos Translate not available (neither library nor CLI)" + raise RuntimeError(msg) + + def _translate_argos_lib(self, text: str, source_lang: str, target_lang: str) -> dict[str, Any]: + try: + installed_packages = package.get_installed_packages() + translation_package = None + + for pkg in installed_packages: + if pkg.from_code == source_lang and pkg.to_code == target_lang: + translation_package = pkg + break + + if translation_package is None: + msg = ( + f"No translation package found for {source_lang} -> {target_lang}. " + "Install packages using: argostranslate --update-languages" + ) + raise ValueError(msg) + + translated_text = translate.translate(text, source_lang, target_lang) + return { + "translated_text": translated_text, + "source_lang": source_lang, + "target_lang": target_lang, + "source": "argos", + } + except Exception as e: + msg = f"Argos Translate error: {e}" + raise RuntimeError(msg) + + def _translate_argos_cli(self, text: str, source_lang: str, target_lang: str) -> dict[str, Any]: + if source_lang == "auto" or not source_lang: + msg = "Auto-detection is not supported with CLI. Please select a source language manually." + raise ValueError(msg) + + if not target_lang: + msg = "Target language is required." + raise ValueError(msg) + + if not isinstance(source_lang, str) or not isinstance(target_lang, str): + msg = "Language codes must be strings." + raise ValueError(msg) + + if len(source_lang) != 2 or len(target_lang) != 2: + msg = f"Invalid language codes: {source_lang} -> {target_lang}" + raise ValueError(msg) + + executable = shutil.which("argos-translate") + if not executable: + msg = "argos-translate executable not found in PATH" + raise RuntimeError(msg) + + try: + args = [executable, "--from-lang", source_lang, "--to-lang", target_lang, text] + result = subprocess.run(args, capture_output=True, text=True, check=True) # noqa: S603 + translated_text = result.stdout.strip() + if not translated_text: + msg = "Translation returned empty result" + raise RuntimeError(msg) + return { + "translated_text": translated_text, + "source_lang": source_lang, + "target_lang": target_lang, + "source": "argos", + } + except subprocess.CalledProcessError as e: + error_msg = e.stderr.decode() if isinstance(e.stderr, bytes) else (e.stderr or str(e)) + msg = f"Argos Translate CLI error: {error_msg}" + raise RuntimeError(msg) + except Exception as e: + msg = f"Argos Translate CLI error: {e!s}" + raise RuntimeError(msg) + + def _detect_language(self, text: str) -> str | None: + if not self.has_argos_lib: + return None + + try: + from argostranslate import translate + + installed_packages = package.get_installed_packages() + if not installed_packages: + return None + + detected = translate.detect_language(text) + if detected: + return detected.code + except Exception as e: + print(f"Language detection failed: {e}") + + return None + + def _get_argos_languages_cli(self) -> list[dict[str, str]]: + languages = [] + argospm = shutil.which("argospm") + if not argospm: + return languages + + try: + result = subprocess.run( # noqa: S603 + [argospm, "list"], + capture_output=True, + text=True, + timeout=10, + check=True, + ) + installed_packages = result.stdout.strip().split("\n") + argos_langs = set() + + for pkg_name in installed_packages: + if not pkg_name.strip(): + continue + match = re.match(r"translate-([a-z]{2})_([a-z]{2})", pkg_name.strip()) + if match: + from_code = match.group(1) + to_code = match.group(2) + argos_langs.add(from_code) + argos_langs.add(to_code) + + for code in sorted(argos_langs): + name = LANGUAGE_CODE_TO_NAME.get(code, code.upper()) + languages.append( + { + "code": code, + "name": name, + "source": "argos", + }, + ) + except subprocess.CalledProcessError as e: + print(f"argospm list failed: {e.stderr or str(e)}") + except Exception as e: + print(f"Error parsing argospm output: {e}") + + return languages + + def install_language_package(self, package_name: str = "translate") -> dict[str, Any]: + argospm = shutil.which("argospm") + if not argospm: + msg = "argospm not found in PATH. Install argostranslate first." + raise RuntimeError(msg) + + try: + result = subprocess.run( # noqa: S603 + [argospm, "install", package_name], + capture_output=True, + text=True, + timeout=300, + check=True, + ) + return { + "success": True, + "message": f"Successfully installed {package_name}", + "output": result.stdout, + } + except subprocess.TimeoutExpired: + msg = f"Installation of {package_name} timed out after 5 minutes" + raise RuntimeError(msg) + except subprocess.CalledProcessError as e: + msg = f"Failed to install {package_name}: {e.stderr or str(e)}" + raise RuntimeError(msg) + except Exception as e: + msg = f"Error installing {package_name}: {e!s}" + raise RuntimeError(msg) diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue new file mode 100644 index 0000000..7ddcf9a --- /dev/null +++ b/meshchatx/src/frontend/components/App.vue @@ -0,0 +1,731 @@ + + + diff --git a/meshchatx/src/frontend/components/CardStack.vue b/meshchatx/src/frontend/components/CardStack.vue new file mode 100644 index 0000000..bb8c1eb --- /dev/null +++ b/meshchatx/src/frontend/components/CardStack.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/meshchatx/src/frontend/components/ColourPickerDropdown.vue b/meshchatx/src/frontend/components/ColourPickerDropdown.vue new file mode 100644 index 0000000..d1c09cf --- /dev/null +++ b/meshchatx/src/frontend/components/ColourPickerDropdown.vue @@ -0,0 +1,94 @@ + + + diff --git a/meshchatx/src/frontend/components/DropDownMenu.vue b/meshchatx/src/frontend/components/DropDownMenu.vue new file mode 100644 index 0000000..9228b81 --- /dev/null +++ b/meshchatx/src/frontend/components/DropDownMenu.vue @@ -0,0 +1,101 @@ + + + diff --git a/meshchatx/src/frontend/components/DropDownMenuItem.vue b/meshchatx/src/frontend/components/DropDownMenuItem.vue new file mode 100644 index 0000000..7eee017 --- /dev/null +++ b/meshchatx/src/frontend/components/DropDownMenuItem.vue @@ -0,0 +1,13 @@ + + + diff --git a/meshchatx/src/frontend/components/IconButton.vue b/meshchatx/src/frontend/components/IconButton.vue new file mode 100644 index 0000000..3352be8 --- /dev/null +++ b/meshchatx/src/frontend/components/IconButton.vue @@ -0,0 +1,14 @@ + + + diff --git a/meshchatx/src/frontend/components/LanguageSelector.vue b/meshchatx/src/frontend/components/LanguageSelector.vue new file mode 100644 index 0000000..7ce0862 --- /dev/null +++ b/meshchatx/src/frontend/components/LanguageSelector.vue @@ -0,0 +1,95 @@ + + + diff --git a/meshchatx/src/frontend/components/LxmfUserIcon.vue b/meshchatx/src/frontend/components/LxmfUserIcon.vue new file mode 100644 index 0000000..6eeb539 --- /dev/null +++ b/meshchatx/src/frontend/components/LxmfUserIcon.vue @@ -0,0 +1,36 @@ + + + diff --git a/meshchatx/src/frontend/components/MaterialDesignIcon.vue b/meshchatx/src/frontend/components/MaterialDesignIcon.vue new file mode 100644 index 0000000..fa640c1 --- /dev/null +++ b/meshchatx/src/frontend/components/MaterialDesignIcon.vue @@ -0,0 +1,47 @@ + + + diff --git a/meshchatx/src/frontend/components/NotificationBell.vue b/meshchatx/src/frontend/components/NotificationBell.vue new file mode 100644 index 0000000..2bdd36c --- /dev/null +++ b/meshchatx/src/frontend/components/NotificationBell.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/meshchatx/src/frontend/components/SidebarLink.vue b/meshchatx/src/frontend/components/SidebarLink.vue new file mode 100644 index 0000000..2539858 --- /dev/null +++ b/meshchatx/src/frontend/components/SidebarLink.vue @@ -0,0 +1,44 @@ + + + diff --git a/meshchatx/src/frontend/components/Toast.vue b/meshchatx/src/frontend/components/Toast.vue new file mode 100644 index 0000000..1cc77f9 --- /dev/null +++ b/meshchatx/src/frontend/components/Toast.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/meshchatx/src/frontend/components/about/AboutPage.vue b/meshchatx/src/frontend/components/about/AboutPage.vue new file mode 100644 index 0000000..363662f --- /dev/null +++ b/meshchatx/src/frontend/components/about/AboutPage.vue @@ -0,0 +1,833 @@ + + + diff --git a/meshchatx/src/frontend/components/archives/ArchivesPage.vue b/meshchatx/src/frontend/components/archives/ArchivesPage.vue new file mode 100644 index 0000000..3ba1b98 --- /dev/null +++ b/meshchatx/src/frontend/components/archives/ArchivesPage.vue @@ -0,0 +1,387 @@ + + + + + diff --git a/meshchatx/src/frontend/components/auth/AuthPage.vue b/meshchatx/src/frontend/components/auth/AuthPage.vue new file mode 100644 index 0000000..c708aa8 --- /dev/null +++ b/meshchatx/src/frontend/components/auth/AuthPage.vue @@ -0,0 +1,152 @@ + + + diff --git a/meshchatx/src/frontend/components/blocked/BlockedPage.vue b/meshchatx/src/frontend/components/blocked/BlockedPage.vue new file mode 100644 index 0000000..80603ad --- /dev/null +++ b/meshchatx/src/frontend/components/blocked/BlockedPage.vue @@ -0,0 +1,263 @@ + + + diff --git a/meshchatx/src/frontend/components/call/CallOverlay.vue b/meshchatx/src/frontend/components/call/CallOverlay.vue new file mode 100644 index 0000000..40d8a97 --- /dev/null +++ b/meshchatx/src/frontend/components/call/CallOverlay.vue @@ -0,0 +1,262 @@ + + + diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue new file mode 100644 index 0000000..3e7f275 --- /dev/null +++ b/meshchatx/src/frontend/components/call/CallPage.vue @@ -0,0 +1,458 @@ + + + diff --git a/meshchatx/src/frontend/components/forms/FormLabel.vue b/meshchatx/src/frontend/components/forms/FormLabel.vue new file mode 100644 index 0000000..0892b9a --- /dev/null +++ b/meshchatx/src/frontend/components/forms/FormLabel.vue @@ -0,0 +1,10 @@ + + diff --git a/meshchatx/src/frontend/components/forms/FormSubLabel.vue b/meshchatx/src/frontend/components/forms/FormSubLabel.vue new file mode 100644 index 0000000..446034e --- /dev/null +++ b/meshchatx/src/frontend/components/forms/FormSubLabel.vue @@ -0,0 +1,10 @@ + + diff --git a/meshchatx/src/frontend/components/forms/Toggle.vue b/meshchatx/src/frontend/components/forms/Toggle.vue new file mode 100644 index 0000000..0146320 --- /dev/null +++ b/meshchatx/src/frontend/components/forms/Toggle.vue @@ -0,0 +1,36 @@ + + + diff --git a/meshchatx/src/frontend/components/forwarder/ForwarderPage.vue b/meshchatx/src/frontend/components/forwarder/ForwarderPage.vue new file mode 100644 index 0000000..fd95a91 --- /dev/null +++ b/meshchatx/src/frontend/components/forwarder/ForwarderPage.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue new file mode 100644 index 0000000..8a96a45 --- /dev/null +++ b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue @@ -0,0 +1,1597 @@ + + + + + diff --git a/meshchatx/src/frontend/components/interfaces/ExpandingSection.vue b/meshchatx/src/frontend/components/interfaces/ExpandingSection.vue new file mode 100644 index 0000000..484e630 --- /dev/null +++ b/meshchatx/src/frontend/components/interfaces/ExpandingSection.vue @@ -0,0 +1,44 @@ + + diff --git a/meshchatx/src/frontend/components/interfaces/ImportInterfacesModal.vue b/meshchatx/src/frontend/components/interfaces/ImportInterfacesModal.vue new file mode 100644 index 0000000..878d559 --- /dev/null +++ b/meshchatx/src/frontend/components/interfaces/ImportInterfacesModal.vue @@ -0,0 +1,275 @@ + + + diff --git a/meshchatx/src/frontend/components/interfaces/Interface.vue b/meshchatx/src/frontend/components/interfaces/Interface.vue new file mode 100644 index 0000000..b76c8a2 --- /dev/null +++ b/meshchatx/src/frontend/components/interfaces/Interface.vue @@ -0,0 +1,266 @@ + + + + + diff --git a/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue b/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue new file mode 100644 index 0000000..b07add8 --- /dev/null +++ b/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue @@ -0,0 +1,362 @@ + + + diff --git a/meshchatx/src/frontend/components/map/MapPage.vue b/meshchatx/src/frontend/components/map/MapPage.vue new file mode 100644 index 0000000..ee29e91 --- /dev/null +++ b/meshchatx/src/frontend/components/map/MapPage.vue @@ -0,0 +1,1314 @@ + + + + + diff --git a/meshchatx/src/frontend/components/messages/AddAudioButton.vue b/meshchatx/src/frontend/components/messages/AddAudioButton.vue new file mode 100644 index 0000000..1427f20 --- /dev/null +++ b/meshchatx/src/frontend/components/messages/AddAudioButton.vue @@ -0,0 +1,112 @@ + + + diff --git a/meshchatx/src/frontend/components/messages/AddImageButton.vue b/meshchatx/src/frontend/components/messages/AddImageButton.vue new file mode 100644 index 0000000..795a30d --- /dev/null +++ b/meshchatx/src/frontend/components/messages/AddImageButton.vue @@ -0,0 +1,164 @@ + + + diff --git a/meshchatx/src/frontend/components/messages/ConversationDropDownMenu.vue b/meshchatx/src/frontend/components/messages/ConversationDropDownMenu.vue new file mode 100644 index 0000000..ccf7f33 --- /dev/null +++ b/meshchatx/src/frontend/components/messages/ConversationDropDownMenu.vue @@ -0,0 +1,215 @@ + + + diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue new file mode 100644 index 0000000..eb5bcf6 --- /dev/null +++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue @@ -0,0 +1,2118 @@ + + + + + diff --git a/meshchatx/src/frontend/components/messages/MessagesPage.vue b/meshchatx/src/frontend/components/messages/MessagesPage.vue new file mode 100644 index 0000000..d16c6da --- /dev/null +++ b/meshchatx/src/frontend/components/messages/MessagesPage.vue @@ -0,0 +1,338 @@ + + + diff --git a/meshchatx/src/frontend/components/messages/MessagesSidebar.vue b/meshchatx/src/frontend/components/messages/MessagesSidebar.vue new file mode 100644 index 0000000..9688421 --- /dev/null +++ b/meshchatx/src/frontend/components/messages/MessagesSidebar.vue @@ -0,0 +1,436 @@ + + + diff --git a/meshchatx/src/frontend/components/messages/SendMessageButton.vue b/meshchatx/src/frontend/components/messages/SendMessageButton.vue new file mode 100644 index 0000000..378a964 --- /dev/null +++ b/meshchatx/src/frontend/components/messages/SendMessageButton.vue @@ -0,0 +1,152 @@ + + + diff --git a/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue b/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue new file mode 100644 index 0000000..3c0271a --- /dev/null +++ b/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue @@ -0,0 +1,812 @@ + + + + + diff --git a/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiserPage.vue b/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiserPage.vue new file mode 100644 index 0000000..5ff23cc --- /dev/null +++ b/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiserPage.vue @@ -0,0 +1,14 @@ + + + diff --git a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue new file mode 100644 index 0000000..b87005e --- /dev/null +++ b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue @@ -0,0 +1,1427 @@ + + + + + diff --git a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue new file mode 100644 index 0000000..792b685 --- /dev/null +++ b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue @@ -0,0 +1,379 @@ + + + + + diff --git a/meshchatx/src/frontend/components/ping/PingPage.vue b/meshchatx/src/frontend/components/ping/PingPage.vue new file mode 100644 index 0000000..e7069a9 --- /dev/null +++ b/meshchatx/src/frontend/components/ping/PingPage.vue @@ -0,0 +1,326 @@ + + + diff --git a/meshchatx/src/frontend/components/profile/ProfileIconPage.vue b/meshchatx/src/frontend/components/profile/ProfileIconPage.vue new file mode 100644 index 0000000..117bd95 --- /dev/null +++ b/meshchatx/src/frontend/components/profile/ProfileIconPage.vue @@ -0,0 +1,220 @@ + + + diff --git a/meshchatx/src/frontend/components/propagation-nodes/PropagationNodesPage.vue b/meshchatx/src/frontend/components/propagation-nodes/PropagationNodesPage.vue new file mode 100644 index 0000000..c451e25 --- /dev/null +++ b/meshchatx/src/frontend/components/propagation-nodes/PropagationNodesPage.vue @@ -0,0 +1,408 @@ + + + diff --git a/meshchatx/src/frontend/components/rncp/RNCPPage.vue b/meshchatx/src/frontend/components/rncp/RNCPPage.vue new file mode 100644 index 0000000..81a7f0b --- /dev/null +++ b/meshchatx/src/frontend/components/rncp/RNCPPage.vue @@ -0,0 +1,498 @@ + + + diff --git a/meshchatx/src/frontend/components/rnprobe/RNProbePage.vue b/meshchatx/src/frontend/components/rnprobe/RNProbePage.vue new file mode 100644 index 0000000..e4e8e20 --- /dev/null +++ b/meshchatx/src/frontend/components/rnprobe/RNProbePage.vue @@ -0,0 +1,244 @@ + + + diff --git a/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue b/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue new file mode 100644 index 0000000..92a6b38 --- /dev/null +++ b/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue @@ -0,0 +1,226 @@ + + + diff --git a/meshchatx/src/frontend/components/settings/SettingsPage.vue b/meshchatx/src/frontend/components/settings/SettingsPage.vue new file mode 100644 index 0000000..dbf29bb --- /dev/null +++ b/meshchatx/src/frontend/components/settings/SettingsPage.vue @@ -0,0 +1,880 @@ + + + + + diff --git a/meshchatx/src/frontend/components/tools/ToolsPage.vue b/meshchatx/src/frontend/components/tools/ToolsPage.vue new file mode 100644 index 0000000..62477aa --- /dev/null +++ b/meshchatx/src/frontend/components/tools/ToolsPage.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/meshchatx/src/frontend/components/translator/TranslatorPage.vue b/meshchatx/src/frontend/components/translator/TranslatorPage.vue new file mode 100644 index 0000000..51c78f1 --- /dev/null +++ b/meshchatx/src/frontend/components/translator/TranslatorPage.vue @@ -0,0 +1,528 @@ + + + diff --git a/meshchatx/src/frontend/fonts/RobotoMonoNerdFont/RobotoMonoNerdFont-Regular.ttf b/meshchatx/src/frontend/fonts/RobotoMonoNerdFont/RobotoMonoNerdFont-Regular.ttf new file mode 100644 index 0000000..11b95d3 Binary files /dev/null and b/meshchatx/src/frontend/fonts/RobotoMonoNerdFont/RobotoMonoNerdFont-Regular.ttf differ diff --git a/meshchatx/src/frontend/fonts/RobotoMonoNerdFont/font.css b/meshchatx/src/frontend/fonts/RobotoMonoNerdFont/font.css new file mode 100644 index 0000000..c623f32 --- /dev/null +++ b/meshchatx/src/frontend/fonts/RobotoMonoNerdFont/font.css @@ -0,0 +1,6 @@ +@font-face { + font-family: 'Roboto Mono Nerd Font'; + src: url('./RobotoMonoNerdFont-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; +} diff --git a/meshchatx/src/frontend/index.html b/meshchatx/src/frontend/index.html new file mode 100644 index 0000000..6ea0a25 --- /dev/null +++ b/meshchatx/src/frontend/index.html @@ -0,0 +1,33 @@ + + + + + + + + + + Reticulum MeshChat + + + +
+ + + + diff --git a/meshchatx/src/frontend/js/Codec2Loader.js b/meshchatx/src/frontend/js/Codec2Loader.js new file mode 100644 index 0000000..9318ad5 --- /dev/null +++ b/meshchatx/src/frontend/js/Codec2Loader.js @@ -0,0 +1,58 @@ +const codec2ScriptPaths = [ + "/assets/js/codec2-emscripten/c2enc.js", + "/assets/js/codec2-emscripten/c2dec.js", + "/assets/js/codec2-emscripten/sox.js", + "/assets/js/codec2-emscripten/codec2-lib.js", + "/assets/js/codec2-emscripten/wav-encoder.js", + "/assets/js/codec2-emscripten/codec2-microphone-recorder.js", +]; + +let loadPromise = null; + +function injectScript(src) { + if (typeof document === "undefined") { + return Promise.resolve(); + } + + const attrName = "data-codec2-src"; + const loadedAttr = "data-codec2-loaded"; + const existing = document.querySelector(`script[${attrName}="${src}"]`); + + if (existing) { + if (existing.getAttribute(loadedAttr) === "true") { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + existing.addEventListener("load", () => resolve(), { once: true }); + existing.addEventListener("error", () => reject(new Error(`Failed to load ${src}`)), { once: true }); + }); + } + + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.src = src; + script.async = false; + script.setAttribute(attrName, src); + script.addEventListener("load", () => { + script.setAttribute(loadedAttr, "true"); + resolve(); + }); + script.addEventListener("error", () => { + script.remove(); + reject(new Error(`Failed to load ${src}`)); + }); + document.head.appendChild(script); + }); +} + +export function ensureCodec2ScriptsLoaded() { + if (typeof window === "undefined") { + return Promise.resolve(); + } + + if (!loadPromise) { + loadPromise = codec2ScriptPaths.reduce((chain, src) => chain.then(() => injectScript(src)), Promise.resolve()); + } + + return loadPromise; +} diff --git a/meshchatx/src/frontend/js/DialogUtils.js b/meshchatx/src/frontend/js/DialogUtils.js new file mode 100644 index 0000000..c6c27c8 --- /dev/null +++ b/meshchatx/src/frontend/js/DialogUtils.js @@ -0,0 +1,35 @@ +import GlobalEmitter from "./GlobalEmitter"; + +class DialogUtils { + static alert(message, type = "info") { + if (window.electron) { + // running inside electron, use ipc alert + window.electron.alert(message); + } + + // always show toast as well (or instead of browser alert) + GlobalEmitter.emit("toast", { message, type }); + } + + static confirm(message) { + if (window.electron) { + // running inside electron, use ipc confirm + return window.electron.confirm(message); + } else { + // running inside normal browser, use browser alert + return window.confirm(message); + } + } + + static async prompt(message) { + if (window.electron) { + // running inside electron, use ipc prompt + return await window.electron.prompt(message); + } else { + // running inside normal browser, use browser prompt + return window.prompt(message); + } + } +} + +export default DialogUtils; diff --git a/meshchatx/src/frontend/js/DownloadUtils.js b/meshchatx/src/frontend/js/DownloadUtils.js new file mode 100644 index 0000000..02c3b28 --- /dev/null +++ b/meshchatx/src/frontend/js/DownloadUtils.js @@ -0,0 +1,24 @@ +class DownloadUtils { + static downloadFile(filename, blob) { + // create object url for blob + const objectUrl = URL.createObjectURL(blob); + + // create hidden link element to download blob + const link = document.createElement("a"); + link.href = objectUrl; + link.download = filename; + link.style.display = "none"; + document.body.append(link); + + // click link to download file in browser + link.click(); + + // link element is no longer needed + link.remove(); + + // revoke object url to clear memory + setTimeout(() => URL.revokeObjectURL(objectUrl), 10000); + } +} + +export default DownloadUtils; diff --git a/meshchatx/src/frontend/js/ElectronUtils.js b/meshchatx/src/frontend/js/ElectronUtils.js new file mode 100644 index 0000000..ba05e0b --- /dev/null +++ b/meshchatx/src/frontend/js/ElectronUtils.js @@ -0,0 +1,19 @@ +class ElectronUtils { + static isElectron() { + return window.electron != null; + } + + static relaunch() { + if (window.electron) { + window.electron.relaunch(); + } + } + + static showPathInFolder(path) { + if (window.electron) { + window.electron.showPathInFolder(path); + } + } +} + +export default ElectronUtils; diff --git a/meshchatx/src/frontend/js/GlobalEmitter.js b/meshchatx/src/frontend/js/GlobalEmitter.js new file mode 100644 index 0000000..2a3d4fd --- /dev/null +++ b/meshchatx/src/frontend/js/GlobalEmitter.js @@ -0,0 +1,24 @@ +import mitt from "mitt"; + +class GlobalEmitter { + constructor() { + this.emitter = mitt(); + } + + // add event listener + on(event, handler) { + this.emitter.on(event, handler); + } + + // remove event listener + off(event, handler) { + this.emitter.off(event, handler); + } + + // emit event + emit(type, event) { + this.emitter.emit(type, event); + } +} + +export default new GlobalEmitter(); diff --git a/meshchatx/src/frontend/js/GlobalState.js b/meshchatx/src/frontend/js/GlobalState.js new file mode 100644 index 0000000..fd73676 --- /dev/null +++ b/meshchatx/src/frontend/js/GlobalState.js @@ -0,0 +1,8 @@ +import { reactive } from "vue"; + +// global state +const globalState = reactive({ + unreadConversationsCount: 0, +}); + +export default globalState; diff --git a/meshchatx/src/frontend/js/MicronParser.js b/meshchatx/src/frontend/js/MicronParser.js new file mode 100644 index 0000000..80cd198 --- /dev/null +++ b/meshchatx/src/frontend/js/MicronParser.js @@ -0,0 +1,720 @@ +/** + * Micron Parser JavaScript implementation + * + * micron-parser.js is based on MicronParser.py from NomadNet: + * https://raw.githubusercontent.com/markqvist/NomadNet/refs/heads/master/nomadnet/ui/textui/MicronParser.py + * + * Documentation for the Micron markdown format can be found here: + * https://raw.githubusercontent.com/markqvist/NomadNet/refs/heads/master/nomadnet/ui/textui/Guide.py + */ +class MicronParser { + constructor(darkTheme = true) { + this.darkTheme = darkTheme; + this.DEFAULT_FG_DARK = "ddd"; + this.DEFAULT_FG_LIGHT = "222"; + this.DEFAULT_BG = "default"; + + this.SELECTED_STYLES = null; + + this.STYLES_DARK = { + plain: { fg: this.DEFAULT_FG_DARK, bg: this.DEFAULT_BG, bold: false, underline: false, italic: false }, + heading1: { fg: "222", bg: "bbb", bold: false, underline: false, italic: false }, + heading2: { fg: "111", bg: "999", bold: false, underline: false, italic: false }, + heading3: { fg: "000", bg: "777", bold: false, underline: false, italic: false }, + }; + + this.STYLES_LIGHT = { + plain: { fg: this.DEFAULT_FG_LIGHT, bg: this.DEFAULT_BG, bold: false, underline: false, italic: false }, + heading1: { fg: "000", bg: "777", bold: false, underline: false, italic: false }, + heading2: { fg: "111", bg: "aaa", bold: false, underline: false, italic: false }, + heading3: { fg: "222", bg: "ccc", bold: false, underline: false, italic: false }, + }; + + if (this.darkTheme) { + this.SELECTED_STYLES = this.STYLES_DARK; + } else { + this.SELECTED_STYLES = this.STYLES_LIGHT; + } + } + + static formatNomadnetworkUrl(url) { + return `nomadnetwork://${url}`; + } + + convertMicronToHtml(markup) { + let html = ""; + + let state = { + literal: false, + depth: 0, + fg_color: this.SELECTED_STYLES.plain.fg, + bg_color: this.DEFAULT_BG, + formatting: { + bold: false, + underline: false, + italic: false, + strikethrough: false, + }, + default_align: "left", + align: "left", + radio_groups: {}, + }; + + const lines = markup.split("\n"); + + for (let line of lines) { + const lineOutput = this.parseLine(line, state); + if (lineOutput && lineOutput.length > 0) { + for (let el of lineOutput) { + html += el.outerHTML; + } + } else { + html += "
"; + } + } + + return html; + } + + parseToHtml(markup) { + // Create a fragment to hold all the Micron output + const fragment = document.createDocumentFragment(); + + let state = { + literal: false, + depth: 0, + fg_color: this.SELECTED_STYLES.plain.fg, + bg_color: this.DEFAULT_BG, + formatting: { + bold: false, + underline: false, + italic: false, + strikethrough: false, + }, + default_align: "left", + align: "left", + radio_groups: {}, + }; + + const lines = markup.split("\n"); + + for (let line of lines) { + const lineOutput = this.parseLine(line, state); + if (lineOutput && lineOutput.length > 0) { + for (let el of lineOutput) { + fragment.appendChild(el); + } + } else { + fragment.appendChild(document.createElement("br")); + } + } + + return fragment; + } + + parseLine(line, state) { + if (line.length > 0) { + // Check literals toggle + if (line === "`=") { + state.literal = !state.literal; + return null; + } + + if (!state.literal) { + // Comments + if (line[0] === "#") { + return null; + } + + // Reset section depth + if (line[0] === "<") { + state.depth = 0; + return this.parseLine(line.slice(1), state); + } + + // Section headings + if (line[0] === ">") { + let i = 0; + while (i < line.length && line[i] === ">") { + i++; + } + state.depth = i; + let headingLine = line.slice(i); + + if (headingLine.length > 0) { + // apply heading style if it exists + let style = null; + let wanted_style = "heading" + i; + if (this.SELECTED_STYLES[wanted_style]) { + style = this.SELECTED_STYLES[wanted_style]; + } else { + style = this.SELECTED_STYLES.plain; + } + + const latched_style = this.stateToStyle(state); + this.styleToState(style, state); + + let outputParts = this.makeOutput(state, headingLine); + this.styleToState(latched_style, state); + + // wrap in a heading container + if (outputParts && outputParts.length > 0) { + const div = document.createElement("div"); + this.applyAlignment(div, state); + this.applySectionIndent(div, state); + // merge text nodes + this.appendOutput(div, outputParts, state); + return [div]; + } else { + return null; + } + } else { + return null; + } + } + + // horizontal dividers + if (line[0] === "-") { + const hr = document.createElement("hr"); + this.applySectionIndent(hr, state); + return [hr]; + } + } + + let outputParts = this.makeOutput(state, line); + if (outputParts) { + // outputParts can contain text (tuple) and special objects (fields/checkbox) + // construct a single line container + let container = document.createElement("div"); + this.applyAlignment(container, state); + this.applySectionIndent(container, state); + this.appendOutput(container, outputParts, state); + return [container]; + } else { + // Just empty line + return [document.createElement("br")]; + } + } else { + // Empty line + return [document.createElement("br")]; + } + } + + applyAlignment(el, state) { + // use CSS text-align for alignment + el.style.textAlign = state.align || "left"; + } + + applySectionIndent(el, state) { + // indent by state.depth + let indent = (state.depth - 1) * 2; + if (indent > 0) { + el.style.marginLeft = indent * 10 + "px"; + } + } + + // convert current state to a style object + stateToStyle(state) { + return { + fg: state.fg_color, + bg: state.bg_color, + bold: state.formatting.bold, + underline: state.formatting.underline, + italic: state.formatting.italic, + }; + } + + styleToState(style, state) { + if (style.fg !== undefined && style.fg !== null) state.fg_color = style.fg; + if (style.bg !== undefined && style.bg !== null) state.bg_color = style.bg; + if (style.bold !== undefined && style.bold !== null) state.formatting.bold = style.bold; + if (style.underline !== undefined && style.underline !== null) state.formatting.underline = style.underline; + if (style.italic !== undefined && style.italic !== null) state.formatting.italic = style.italic; + } + + appendOutput(container, parts) { + let currentSpan = null; + let currentStyle = null; + + const flushSpan = () => { + if (currentSpan) { + container.appendChild(currentSpan); + currentSpan = null; + currentStyle = null; + } + }; + + for (let p of parts) { + if (typeof p === "string") { + let span = document.createElement("span"); + span.textContent = p; + container.appendChild(span); + } else if (Array.isArray(p) && p.length === 2) { + // tuple: [styleSpec, text] + let [styleSpec, text] = p; + // if different style, flush currentSpan + if (!this.stylesEqual(styleSpec, currentStyle)) { + flushSpan(); + currentSpan = document.createElement("span"); + this.applyStyleToElement(currentSpan, styleSpec); + currentStyle = styleSpec; + } + currentSpan.textContent += text; + } else if (p && typeof p === "object") { + // field, checkbox, radio, link + flushSpan(); + if (p.type === "field") { + let input = document.createElement("input"); + input.type = p.masked ? "password" : "text"; + input.name = p.name; + input.setAttribute("value", p.data); + if (p.width) { + input.size = p.width; + } + this.applyStyleToElement(input, this.styleFromState(p.style)); + container.appendChild(input); + } else if (p.type === "checkbox") { + let label = document.createElement("label"); + let cb = document.createElement("input"); + cb.type = "checkbox"; + cb.name = p.name; + cb.value = p.value; + if (p.prechecked) cb.setAttribute("checked", true); + label.appendChild(cb); + label.appendChild(document.createTextNode(" " + p.label)); + this.applyStyleToElement(label, this.styleFromState(p.style)); + container.appendChild(label); + } else if (p.type === "radio") { + let label = document.createElement("label"); + let rb = document.createElement("input"); + rb.type = "radio"; + rb.name = p.name; + rb.value = p.value; + if (p.prechecked) rb.setAttribute("checked", true); + label.appendChild(rb); + label.appendChild(document.createTextNode(" " + p.label)); + this.applyStyleToElement(label, this.styleFromState(p.style)); + container.appendChild(label); + } else if (p.type === "link") { + let directURL = p.url.replace("nomadnetwork://", "").replace("lxmf://", ""); + // use p.url as is for the href + const formattedUrl = p.url; + + let a = document.createElement("a"); + a.href = formattedUrl; + a.title = formattedUrl; + + let fieldsToSubmit = []; + let requestVars = {}; + let foundAll = false; + + if (p.fields && p.fields.length > 0) { + for (const f of p.fields) { + if (f === "*") { + // submit all fields + foundAll = true; + } else if (f.includes("=")) { + // this is a request variable (key=value) + const [k, v] = f.split("="); + requestVars[k] = v; + } else { + // this is a field name to submit + fieldsToSubmit.push(f); + } + } + + let fieldStr = ""; + if (foundAll) { + // if '*' was found, submit all fields + fieldStr = "*"; + } else { + fieldStr = fieldsToSubmit.join("|"); + } + + // append request variables directly to the directURL as query parameters + const varEntries = Object.entries(requestVars); + if (varEntries.length > 0) { + const queryString = varEntries.map(([k, v]) => `${k}=${v}`).join("|"); + + directURL += directURL.includes("`") ? `|${queryString}` : `\`${queryString}`; + } + + a.setAttribute( + "onclick", + `event.preventDefault(); onNodePageUrlClick('${directURL}', '${fieldStr}', false, false)` + ); + } else { + // no fields or request variables, just handle the direct URL + a.setAttribute( + "onclick", + `event.preventDefault(); onNodePageUrlClick('${directURL}', null, false, false)` + ); + } + + a.textContent = p.label; + this.applyStyleToElement(a, this.styleFromState(p.style)); + container.appendChild(a); + } + } + } + + flushSpan(); + } + + stylesEqual(s1, s2) { + if (!s1 && !s2) return true; + if (!s1 || !s2) return false; + return ( + s1.fg === s2.fg && + s1.bg === s2.bg && + s1.bold === s2.bold && + s1.underline === s2.underline && + s1.italic === s2.italic + ); + } + + styleFromState(stateStyle) { + // stateStyle is a name of a style or a style object + // in this code, p.style is actually a style name. j,ust return that + return stateStyle; + } + + applyStyleToElement(el, style) { + if (!style) return; + // convert style fg/bg to colors + let fgColor = this.colorToCss(style.fg); + let bgColor = this.colorToCss(style.bg); + + if (fgColor && fgColor !== "default") { + el.style.color = fgColor; + } + if (bgColor && bgColor !== "default") { + el.style.backgroundColor = bgColor; + } + + if (style.bold) { + el.style.fontWeight = "bold"; + } + if (style.underline) { + el.style.textDecoration = el.style.textDecoration ? el.style.textDecoration + " underline" : "underline"; + } + if (style.italic) { + el.style.fontStyle = "italic"; + } + } + + colorToCss(c) { + if (!c || c === "default") return null; + // if 3 hex chars (like '222') => expand to #222 + if (c.length === 3 && /^[0-9a-fA-F]{3}$/.test(c)) { + return "#" + c; + } + // If 6 hex chars + if (c.length === 6 && /^[0-9a-fA-F]{6}$/.test(c)) { + return "#" + c; + } + // If grayscale 'gxx' + if (c.length === 3 && c[0] === "g") { + // treat xx as a number and map to gray + let val = parseInt(c.slice(1), 10); + if (isNaN(val)) val = 50; + // map 0-99 scale to a gray hex + let h = Math.floor(val * 2.55) + .toString(16) + .padStart(2, "0"); + return "#" + h + h + h; + } + + // fallback: just return a known CSS color or tailwind class if not known + return null; + } + + makeOutput(state, line) { + if (state.literal) { + // literal mode: output as is, except if `= line + if (line === "\\`=") { + line = "`="; + } + return [[this.stateToStyle(state), line]]; + } + + let output = []; + let part = ""; + let mode = "text"; + let escape = false; + let skip = 0; + let i = 0; + while (i < line.length) { + let c = line[i]; + if (skip > 0) { + skip--; + i++; + continue; + } + + if (mode === "formatting") { + // Handle formatting commands + switch (c) { + case "_": + state.formatting.underline = !state.formatting.underline; + break; + case "!": + state.formatting.bold = !state.formatting.bold; + break; + case "*": + state.formatting.italic = !state.formatting.italic; + break; + case "F": + // next 3 chars = fg color + if (line.length >= i + 4) { + let color = line.substr(i + 1, 3); + state.fg_color = color; + skip = 3; + } + break; + case "f": + // reset fg + state.fg_color = this.SELECTED_STYLES.plain.fg; + break; + case "B": + if (line.length >= i + 4) { + let color = line.substr(i + 1, 3); + state.bg_color = color; + skip = 3; + } + break; + case "b": + // reset bg + state.bg_color = this.DEFAULT_BG; + break; + case "`": + // reset all formatting + state.formatting.bold = false; + state.formatting.underline = false; + state.formatting.italic = false; + state.fg_color = this.SELECTED_STYLES.plain.fg; + state.bg_color = this.DEFAULT_BG; + state.align = state.default_align; + break; + case "c": + state.align = state.align === "center" ? state.default_align : "center"; + break; + case "l": + state.align = state.align === "left" ? state.default_align : "left"; + break; + case "r": + state.align = state.align === "right" ? state.default_align : "right"; + break; + case "a": + state.align = state.default_align; + break; + case "<": + // Flush current text first + if (part.length > 0) { + output.push([this.stateToStyle(state), part]); + part = ""; + } + { + let fieldData = this.parseField(line, i, state); + if (fieldData) { + output.push(fieldData.obj); + i += fieldData.skip; + continue; + } + } + break; + + case "[": + // flush current text first + if (part.length > 0) { + output.push([this.stateToStyle(state), part]); + part = ""; + } + { + let linkData = this.parseLink(line, i, state); + if (linkData) { + output.push(linkData.obj); + // mode = "text"; + i += linkData.skip; + continue; + } + } + break; + + default: + // unknown formatting char, ignore + break; + } + mode = "text"; + if (part.length > 0) { + // no flush needed, no text added + } + } else { + // mode === "text" + if (c === "\\") { + if (escape) { + // was escaped backslash + part += c; + escape = false; + } else { + escape = true; + } + } else if (c === "`") { + if (escape) { + // just a literal backtick + part += c; + escape = false; + } else { + // switch to formatting mode + if (part.length > 0) { + output.push([this.stateToStyle(state), part]); + part = ""; + } + mode = "formatting"; + } + } else { + if (escape) { + part += "\\"; + escape = false; + } + part += c; + } + } + + i++; + } + + // end of line + if (part.length > 0) { + output.push([this.stateToStyle(state), part]); + } + + return output.length > 0 ? output : null; + } + + parseField(line, startIndex, state) { + let field_start = startIndex + 1; + let backtick_pos = line.indexOf("`", field_start); + if (backtick_pos === -1) return null; + + let field_content = line.substring(field_start, backtick_pos); + let field_masked = false; + let field_width = 24; + let field_type = "field"; + let field_name = field_content; + let field_value = ""; + let field_prechecked = false; + + if (field_content.includes("|")) { + let f_components = field_content.split("|"); + let field_flags = f_components[0]; + field_name = f_components[1]; + + if (field_flags.includes("^")) { + field_type = "radio"; + field_flags = field_flags.replace("^", ""); + } else if (field_flags.includes("?")) { + field_type = "checkbox"; + field_flags = field_flags.replace("?", ""); + } else if (field_flags.includes("!")) { + field_masked = true; + field_flags = field_flags.replace("!", ""); + } + + if (field_flags.length > 0) { + let w = parseInt(field_flags, 10); + if (!isNaN(w)) { + field_width = Math.min(w, 256); + } + } + + if (f_components.length > 2) { + field_value = f_components[2]; + } + + if (f_components.length > 3) { + if (f_components[3] === "*") { + field_prechecked = true; + } + } + } + + let field_end = line.indexOf(">", backtick_pos); + if (field_end === -1) return null; + + let field_data = line.substring(backtick_pos + 1, field_end); + let style = this.stateToStyle(state); + + let obj = null; + if (field_type === "checkbox" || field_type === "radio") { + obj = { + type: field_type, + name: field_name, + value: field_value || field_data, + label: field_data, + prechecked: field_prechecked, + style: style, + }; + } else { + obj = { + type: "field", + name: field_name, + width: field_width, + masked: field_masked, + data: field_data, + style: style, + }; + } + + let skip = field_end - startIndex + 2; + return { obj: obj, skip: skip }; + } + + parseLink(line, startIndex, state) { + let endpos = line.indexOf("]", startIndex); + if (endpos === -1) return null; + + let link_data = line.substring(startIndex + 1, endpos); + let link_components = link_data.split("`"); + let link_label = ""; + let link_url = ""; + let link_fields = ""; + + if (link_components.length === 1) { + link_label = ""; + link_url = link_data; + } else if (link_components.length === 2) { + link_label = link_components[0]; + link_url = link_components[1]; + } else if (link_components.length === 3) { + link_label = link_components[0]; + link_url = link_components[1]; + link_fields = link_components[2]; + } + + if (link_url.length === 0) { + return null; + } + + if (link_label === "") { + link_label = link_url; + } + + // format the URL + link_url = MicronParser.formatNomadnetworkUrl(link_url); + + let style = this.stateToStyle(state); + let obj = { + type: "link", + url: link_url, + label: link_label, + fields: link_fields ? link_fields.split("|") : [], + style: style, + }; + + let skip = endpos - startIndex + 2; + return { obj: obj, skip: skip }; + } +} + +export default MicronParser; diff --git a/meshchatx/src/frontend/js/MicrophoneRecorder.js b/meshchatx/src/frontend/js/MicrophoneRecorder.js new file mode 100644 index 0000000..3c2d94f --- /dev/null +++ b/meshchatx/src/frontend/js/MicrophoneRecorder.js @@ -0,0 +1,64 @@ +/** + * A simple class for recording microphone input and returning the audio. + */ +class MicrophoneRecorder { + constructor() { + this.audioChunks = []; + this.microphoneMediaStream = null; + this.mediaRecorder = null; + } + + async start() { + try { + // request access to the microphone + this.microphoneMediaStream = await navigator.mediaDevices.getUserMedia({ + audio: true, + }); + + // create media recorder + this.mediaRecorder = new MediaRecorder(this.microphoneMediaStream); + + // handle received audio from media recorder + this.mediaRecorder.ondataavailable = (event) => { + this.audioChunks.push(event.data); + }; + + // start recording + this.mediaRecorder.start(); + + // successfully started recording + return true; + } catch { + return false; + } + } + + async stop() { + return new Promise((resolve, reject) => { + try { + // handle media recording stopped + this.mediaRecorder.onstop = () => { + // stop using microphone + if (this.microphoneMediaStream) { + this.microphoneMediaStream.getTracks().forEach((track) => track.stop()); + } + + // create blob from audio chunks + const blob = new Blob(this.audioChunks, { + type: this.mediaRecorder.mimeType, // likely to be "audio/webm;codecs=opus" in chromium + }); + + // resolve promise + resolve(blob); + }; + + // stop recording + this.mediaRecorder.stop(); + } catch (e) { + reject(e); + } + }); + } +} + +export default MicrophoneRecorder; diff --git a/meshchatx/src/frontend/js/NotificationUtils.js b/meshchatx/src/frontend/js/NotificationUtils.js new file mode 100644 index 0000000..76c0de9 --- /dev/null +++ b/meshchatx/src/frontend/js/NotificationUtils.js @@ -0,0 +1,25 @@ +class NotificationUtils { + static showIncomingCallNotification() { + Notification.requestPermission().then((result) => { + if (result === "granted") { + new window.Notification("Incoming Call", { + body: "Someone is calling you.", + tag: "incoming_telephone_call", // only ever show one incoming call notification at a time + }); + } + }); + } + + static showNewMessageNotification() { + Notification.requestPermission().then((result) => { + if (result === "granted") { + new window.Notification("New Message", { + body: "Someone sent you a message.", + tag: "new_message", // only ever show one new message notification at a time + }); + } + }); + } +} + +export default NotificationUtils; diff --git a/meshchatx/src/frontend/js/TileCache.js b/meshchatx/src/frontend/js/TileCache.js new file mode 100644 index 0000000..04a671d --- /dev/null +++ b/meshchatx/src/frontend/js/TileCache.js @@ -0,0 +1,68 @@ +const DB_NAME = "meshchat_map_cache"; +const DB_VERSION = 1; +const STORE_NAME = "tiles"; + +class TileCache { + constructor() { + this.db = null; + this.initPromise = this.init(); + } + + async init() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onerror = (event) => reject("IndexedDB error: " + event.target.errorCode); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); + } + }; + + request.onsuccess = (event) => { + this.db = event.target.result; + resolve(); + }; + }); + } + + async getTile(key) { + await this.initPromise; + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([STORE_NAME], "readonly"); + const store = transaction.objectStore(STORE_NAME); + const request = store.get(key); + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + async setTile(key, data) { + await this.initPromise; + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([STORE_NAME], "readwrite"); + const store = transaction.objectStore(STORE_NAME); + const request = store.put(data, key); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + async clear() { + await this.initPromise; + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([STORE_NAME], "readwrite"); + const store = transaction.objectStore(STORE_NAME); + const request = store.clear(); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } +} + +export default new TileCache(); diff --git a/meshchatx/src/frontend/js/ToastUtils.js b/meshchatx/src/frontend/js/ToastUtils.js new file mode 100644 index 0000000..600fb32 --- /dev/null +++ b/meshchatx/src/frontend/js/ToastUtils.js @@ -0,0 +1,25 @@ +import GlobalEmitter from "./GlobalEmitter"; + +class ToastUtils { + static show(message, type = "info", duration = 5000) { + GlobalEmitter.emit("toast", { message, type, duration }); + } + + static success(message, duration = 5000) { + this.show(message, "success", duration); + } + + static error(message, duration = 5000) { + this.show(message, "error", duration); + } + + static warning(message, duration = 5000) { + this.show(message, "warning", duration); + } + + static info(message, duration = 5000) { + this.show(message, "info", duration); + } +} + +export default ToastUtils; diff --git a/meshchatx/src/frontend/js/Utils.js b/meshchatx/src/frontend/js/Utils.js new file mode 100644 index 0000000..ef0240f --- /dev/null +++ b/meshchatx/src/frontend/js/Utils.js @@ -0,0 +1,185 @@ +import dayjs from "dayjs"; + +class Utils { + static formatDestinationHash(destinationHashHex) { + const bytesPerSide = 4; + const leftSide = destinationHashHex.substring(0, bytesPerSide * 2); + const rightSide = destinationHashHex.substring(destinationHashHex.length - bytesPerSide * 2); + return `<${leftSide}...${rightSide}>`; + } + + static formatBytes(bytes) { + if (bytes === 0) { + return "0 Bytes"; + } + + const k = 1024; + const decimals = 0; + const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + " " + sizes[i]; + } + + static formatNumber(num) { + if (num === 0) { + return "0"; + } + return num.toLocaleString(); + } + + static parseSeconds(secondsToFormat) { + secondsToFormat = Number(secondsToFormat); + var days = Math.floor(secondsToFormat / (3600 * 24)); + var hours = Math.floor((secondsToFormat % (3600 * 24)) / 3600); + var minutes = Math.floor((secondsToFormat % 3600) / 60); + var seconds = Math.floor(secondsToFormat % 60); + return { + days: days, + hours: hours, + minutes: minutes, + seconds: seconds, + }; + } + + static formatSeconds(seconds) { + const parsedSeconds = this.parseSeconds(seconds); + + if (parsedSeconds.days > 0) { + if (parsedSeconds.days === 1) { + return "1 day ago"; + } else { + return parsedSeconds.days + " days ago"; + } + } + + if (parsedSeconds.hours > 0) { + if (parsedSeconds.hours === 1) { + return "1 hour ago"; + } else { + return parsedSeconds.hours + " hours ago"; + } + } + + if (parsedSeconds.minutes > 0) { + if (parsedSeconds.minutes === 1) { + return "1 min ago"; + } else { + return parsedSeconds.minutes + " mins ago"; + } + } + + if (parsedSeconds.seconds <= 1) { + return "a second ago"; + } else { + return parsedSeconds.seconds + " seconds ago"; + } + } + + static formatTimeAgo(datetimeString) { + if (!datetimeString) return "unknown"; + + // ensure UTC if no timezone is provided + let dateString = datetimeString; + if (typeof dateString === "string" && !dateString.includes("Z") && !dateString.includes("+")) { + // SQLite CURRENT_TIMESTAMP format is YYYY-MM-DD HH:MM:SS + // Replace space with T and append Z for ISO format + dateString = dateString.replace(" ", "T") + "Z"; + } + + const millisecondsAgo = Date.now() - new Date(dateString).getTime(); + const secondsAgo = Math.round(millisecondsAgo / 1000); + return this.formatSeconds(secondsAgo); + } + + static formatSecondsAgo(seconds) { + const secondsAgo = Math.round(Date.now() / 1000 - seconds); + return this.formatSeconds(secondsAgo); + } + + static formatMinutesSeconds(seconds) { + const parsedSeconds = this.parseSeconds(seconds); + const paddedMinutes = parsedSeconds.minutes.toString().padStart(2, "0"); + const paddedSeconds = parsedSeconds.seconds.toString().padStart(2, "0"); + return `${paddedMinutes}:${paddedSeconds}`; + } + + static convertUnixMillisToLocalDateTimeString(unixTimestampInMilliseconds) { + return dayjs(unixTimestampInMilliseconds).format("YYYY-MM-DD hh:mm A"); + } + + static convertDateTimeToLocalDateTimeString(dateTime) { + return this.convertUnixMillisToLocalDateTimeString(dateTime.getTime()); + } + + static arrayBufferToBase64(arrayBuffer) { + var binary = ""; + var bytes = new Uint8Array(arrayBuffer); + var len = bytes.byteLength; + for (var i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); + } + + static formatBitsPerSecond(bits) { + if (bits === 0) { + return "0 bps"; + } + + const k = 1000; // Use 1000 instead of 1024 for network speeds + const decimals = 0; + const sizes = ["bps", "kbps", "Mbps", "Gbps", "Tbps", "Pbps", "Ebps", "Zbps", "Ybps"]; + + const i = Math.floor(Math.log(bits) / Math.log(k)); + + return parseFloat((bits / Math.pow(k, i)).toFixed(decimals)) + " " + sizes[i]; + } + + static formatBytesPerSecond(bytesPerSecond) { + if (bytesPerSecond === 0 || bytesPerSecond == null) { + return "0 B/s"; + } + + const k = 1024; + const decimals = 1; + const sizes = ["B/s", "KB/s", "MB/s", "GB/s", "TB/s", "PB/s", "EB/s", "ZB/s", "YB/s"]; + + const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k)); + + return parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(decimals)) + " " + sizes[i]; + } + + static formatFrequency(hz) { + if (hz === 0 || hz == null) { + return "0 Hz"; + } + + const k = 1000; + const sizes = ["Hz", "kHz", "MHz", "GHz", "THz", "PHz", "EHz", "ZHz", "YHz"]; + const i = Math.floor(Math.log(hz) / Math.log(k)); + + return parseFloat(hz / Math.pow(k, i)) + " " + sizes[i]; + } + + static decodeBase64ToUtf8String(base64) { + // support for decoding base64 as a utf8 string to support emojis and cyrillic characters etc + return decodeURIComponent( + atob(base64) + .split("") + .map(function (c) { + return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join("") + ); + } + + static isInterfaceEnabled(iface) { + const rawValue = iface.enabled ?? iface.interface_enabled; + const value = rawValue?.toString()?.toLowerCase(); + return value === "on" || value === "yes" || value === "true"; + } +} + +export default Utils; diff --git a/meshchatx/src/frontend/js/WebSocketConnection.js b/meshchatx/src/frontend/js/WebSocketConnection.js new file mode 100644 index 0000000..48bfc64 --- /dev/null +++ b/meshchatx/src/frontend/js/WebSocketConnection.js @@ -0,0 +1,69 @@ +import mitt from "mitt"; + +class WebSocketConnection { + constructor() { + this.emitter = mitt(); + this.reconnect(); + + /** + * ping websocket server every 30 seconds + * this helps to prevent the underlying tcp connection from going stale when there's no traffic for a long time + */ + setInterval(() => { + this.ping(); + }, 30000); + } + + // add event listener + on(event, handler) { + this.emitter.on(event, handler); + } + + // remove event listener + off(event, handler) { + this.emitter.off(event, handler); + } + + // emit event + emit(type, event) { + this.emitter.emit(type, event); + } + + reconnect() { + // connect to websocket + const wsUrl = location.origin.replace(/^https/, "wss").replace(/^http/, "ws") + "/ws"; + this.ws = new WebSocket(wsUrl); + + // auto reconnect when websocket closes + this.ws.addEventListener("close", () => { + setTimeout(() => { + this.reconnect(); + }, 1000); + }); + + // emit data received from websocket + this.ws.onmessage = (message) => { + this.emit("message", message); + }; + } + + send(message) { + if (this.ws != null && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(message); + } + } + + ping() { + try { + this.send( + JSON.stringify({ + type: "ping", + }) + ); + } catch { + // ignore error + } + } +} + +export default new WebSocketConnection(); diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json new file mode 100644 index 0000000..28aab31 --- /dev/null +++ b/meshchatx/src/frontend/locales/de.json @@ -0,0 +1,541 @@ +{ + "app": { + "name": "Reticulum MeshChatX", + "sync_messages": "Nachrichten synchronisieren", + "compose": "Verfassen", + "messages": "Nachrichten", + "nomad_network": "Nomad Network", + "map": "Karte", + "archives": "Archive", + "propagation_nodes": "Propagationsknoten", + "network_visualiser": "Netzwerk-Visualisierer", + "interfaces": "Schnittstellen", + "tools": "Werkzeuge", + "settings": "Einstellungen", + "about": "Über", + "my_identity": "Meine Identität", + "identity_hash": "Identitäts-Hash", + "lxmf_address": "LXMF-Adresse", + "announce": "Ankündigen", + "announce_now": "Jetzt ankündigen", + "last_announced": "Zuletzt angekündigt: {time}", + "last_announced_never": "Zuletzt angekündigt: Nie", + "display_name_placeholder": "Anzeigename", + "profile": "Profil", + "manage_identity": "Verwalten Sie Ihre Identität, Transportbeteiligung und LXMF-Standardeinstellungen.", + "theme": "Thema", + "theme_mode": "{mode}-Modus", + "transport": "Transport", + "enabled": "Aktiviert", + "disabled": "Deaktiviert", + "propagation": "Propagation", + "local_node_running": "Lokaler Knoten läuft", + "client_only": "Nur Client", + "copy": "Kopieren", + "appearance": "Erscheinungsbild", + "appearance_description": "Wechseln Sie jederzeit zwischen hellen und dunklen Voreinstellungen.", + "light_theme": "Helles Thema", + "dark_theme": "Dunkles Thema", + "live_preview": "Live-Vorschau wird sofort aktualisiert.", + "realtime": "Echtzeit", + "transport_mode": "Transport-Modus", + "transport_description": "Leiten Sie Pfade und Verkehr für nahegelegene Peers weiter.", + "enable_transport_mode": "Transport-Modus aktivieren", + "transport_toggle_description": "Ankündigungen weiterleiten, auf Pfadanfragen antworten und helfen, dass Ihr Mesh online bleibt.", + "requires_restart": "Erfordert einen Neustart nach dem Umschalten.", + "show_community_interfaces": "Community-Schnittstellen anzeigen", + "community_interfaces_description": "Community-gepflegte Voreinstellungen beim Hinzufügen neuer Schnittstellen anzeigen.", + "reliability": "Zuverlässigkeit", + "messages_description": "Steuern Sie, wie MeshChat fehlgeschlagene Zustellungen wiederholt oder eskaliert. Kontrollieren Sie das automatische Wiederholungsverhalten, die erneute Übertragung von Anhängen und Fallback-Mechanismen, um eine zuverlässige Nachrichtenzustellung über das Mesh-Netzwerk zu gewährleisten.", + "auto_resend_title": "Automatisch erneut senden, wenn Peer ankündigt", + "auto_resend_description": "Fehlgeschlagene Nachrichten werden automatisch erneut versucht, sobald das Ziel erneut sendet.", + "retry_attachments_title": "Wiederholungen mit Anhängen zulassen", + "retry_attachments_description": "Große Payloads werden ebenfalls wiederholt (nützlich, wenn beide Peers hohe Limits haben).", + "auto_fallback_title": "Automatisch auf Propagationsknoten ausweichen", + "auto_fallback_description": "Fehlgeschlagene direkte Zustellungen werden in Ihrem bevorzugten Propagationsknoten eingereiht.", + "inbound_stamp_cost": "Kosten für eingehende Nachrichtenstempel", + "inbound_stamp_description": "Erfordern Sie Proof-of-Work-Stempel für direkt an Sie gesendete Nachrichten. Höhere Werte erfordern mehr Rechenaufwand von den Sendern. Bereich: 1-254. Standard: 8.", + "browse_nodes": "Knoten durchsuchen", + "propagation_nodes_description": "Halten Sie Gespräche im Fluss, auch wenn Peers offline sind.", + "nodes_info_1": "Propagationsknoten halten Nachrichten sicher bereit, bis die Empfänger wieder synchronisieren.", + "nodes_info_2": "Knoten peeren untereinander, um verschlüsselte Payloads zu verteilen.", + "nodes_info_3": "Die meisten Knoten speichern Daten ca. 30 Tage lang und verwirfen dann nicht zugestellte Elemente.", + "run_local_node": "Einen lokalen Propagationsknoten betreiben", + "run_local_node_description": "MeshChat wird einen Knoten unter Verwendung dieses lokalen Ziel-Hashs ankündigen und warten.", + "preferred_propagation_node": "Bevorzugter Propagationsknoten", + "preferred_node_placeholder": "Ziel-Hash, z.B. a39610c89d18bb48c73e429582423c24", + "fallback_node_description": "Nachrichten weichen auf diesen Knoten aus, wenn die direkte Zustellung fehlschlägt.", + "auto_sync_interval": "Automatisches Synchronisierungsintervall", + "last_synced": "Zuletzt synchronisiert vor {time}.", + "last_synced_never": "Zuletzt synchronisiert: Nie.", + "propagation_stamp_cost": "Kosten für Propagationsknotenstempel", + "propagation_stamp_description": "Erfordern Sie Proof-of-Work-Stempel für über Ihren Knoten verbreitete Nachrichten. Höhere Werte erfordern mehr Rechenaufwand. Bereich: 13-254. Standard: 16. **Hinweis:** Eine Änderung erfordert den Neustart der App.", + "language": "Sprache", + "select_language": "Wählen Sie Ihre bevorzugte Sprache.", + "custom_fork_by": "Angepasster Fork von", + "open": "Öffnen", + "identity": "Identität", + "lxmf_address_hash": "LXMF-Adress-Hash", + "propagation_node_status": "Status des Propagationsknotens", + "last_sync": "Letzter Sync: vor {time}", + "last_sync_never": "Letzter Sync: Nie", + "syncing": "Synchronisierung...", + "synced": "Synchronisiert", + "not_synced": "Nicht synchronisiert", + "not_configured": "Nicht konfiguriert", + "toggle_source": "Quellcode umschalten", + "audio_calls": "Telefon", + "calls": "Anrufe", + "status": "Status", + "active_call": "Aktiver Anruf", + "incoming": "Eingehend", + "outgoing": "Ausgehend", + "call": "Anruf", + "calls_plural": "Anrufe", + "hop": "Hop", + "hops_plural": "Hops", + "hung_up_waiting": "Aufgelegt, warte auf Anruf...", + "view_incoming_calls": "Eingehende Anrufe anzeigen", + "hangup_all_calls": "Alle Anrufe beenden", + "clear_history": "Verlauf löschen", + "no_active_calls": "Keine aktiven Anrufe", + "incoming_call": "Eingehender Anruf...", + "outgoing_call": "Ausgehender Anruf...", + "call_active": "Aktiv", + "call_ended": "Beendet", + "propagation_node": "Propagationsknoten", + "sync_now": "Jetzt synchronisieren" + }, + "common": { + "open": "Öffnen", + "cancel": "Abbrechen", + "save": "Speichern", + "delete": "Löschen", + "edit": "Bearbeiten", + "add": "Hinzufügen", + "sync": "Synchronisieren", + "restart_app": "App neu starten", + "reveal": "Anzeigen", + "refresh": "Aktualisieren", + "vacuum": "Vakuumieren", + "auto_recover": "Automatisch wiederherstellen" + }, + "about": { + "title": "Über", + "version": "v{version}", + "rns_version": "RNS {version}", + "lxmf_version": "LXMF {version}", + "python_version": "Python {version}", + "config_path": "Konfigurationspfad", + "database_path": "Datenbankpfad", + "database_size": "Datenbankgröße", + "database_health": "Datenbank-Zustand", + "database_health_description": "Schnellprüfung, WAL-Optimierung und Wiederherstellungswerkzeuge für die MeshChatX-Datenbank.", + "running_checks": "Prüfungen werden ausgeführt...", + "integrity": "Integrität", + "journal_mode": "Journal-Modus", + "wal_autocheckpoint": "WAL Autocheckpoint", + "page_size": "Seitengröße", + "pages_free": "Seiten / Frei", + "free_space_estimate": "Geschätzter freier Speicherplatz", + "system_resources": "Systemressourcen", + "live": "Live", + "memory_rss": "Arbeitsspeicher (RSS)", + "virtual_memory": "Virtueller Speicher", + "network_stats": "Netzwerkstatistiken", + "sent": "Gesendet", + "received": "Empfangen", + "packets_sent": "Pakete gesendet", + "packets_received": "Pakete empfangen", + "reticulum_stats": "Reticulum-Statistiken", + "total_paths": "Gesamtpfade", + "announces_per_second": "Ankündigungen / Sek.", + "announces_per_minute": "Ankündigungen / Min.", + "announces_per_hour": "Ankündigungen / Std.", + "download_activity": "Download-Aktivität", + "no_downloads_yet": "Noch keine Downloads", + "runtime_status": "Laufzeitstatus", + "shared_instance": "Geteilte Instanz", + "standalone_instance": "Eigenständige Instanz", + "transport_enabled": "Transport aktiviert", + "transport_disabled": "Transport deaktiviert", + "identity_addresses": "Identität & Adressen", + "telephone_address": "Telefon-Adresse" + }, + "interfaces": { + "title": "Schnittstellen", + "manage": "Verwalten", + "description": "Suchen, filtern und exportieren Sie Ihre Reticulum-Adapter.", + "add_interface": "Schnittstelle hinzufügen", + "import": "Importieren", + "export_all": "Alle exportieren", + "search_placeholder": "Suche nach Name, Typ, Host...", + "all": "Alle", + "all_types": "Alle Typen", + "no_interfaces_found": "Keine Schnittstellen gefunden", + "no_interfaces_description": "Passen Sie Ihre Suche an oder fügen Sie eine neue Schnittstelle hinzu.", + "restart_required": "Neustart erforderlich", + "restart_description": "Reticulum MeshChat muss neu gestartet werden, damit Schnittstellenänderungen wirksam werden.", + "restart_now": "Jetzt neu starten" + }, + "map": { + "title": "Karte", + "description": "Offline-fähige Karte mit MBTiles-Unterstützung.", + "upload_mbtiles": "MBTiles hochladen", + "select_file": "MBTiles-Datei auswählen", + "offline_mode": "Offline-Modus", + "online_mode": "Online-Modus", + "attribution": "Attribution", + "bounds": "Grenzen", + "center": "Zentrum", + "zoom": "Zoom", + "uploading": "Wird hochgeladen...", + "upload_success": "Karte erfolgreich hochgeladen", + "upload_failed": "Hochladen der Karte fehlgeschlagen", + "no_map_loaded": "Keine Offline-Karte geladen. Laden Sie eine .mbtiles-Datei hoch, um den Offline-Modus zu aktivieren.", + "invalid_file": "Ungültige MBTiles-Datei. Nur Rasterkacheln werden unterstützt.", + "default_view": "Standardansicht", + "set_as_default": "Als Standardansicht festlegen", + "export_area": "Bereich exportieren", + "export_instructions": "Ziehen Sie auf der Karte, um einen Bereich auszuwählen.", + "min_zoom": "Min. Zoom", + "max_zoom": "Max. Zoom", + "tile_count": "Geschätzte Kacheln", + "start_export": "Export starten", + "exporting": "Karte wird exportiert...", + "download_ready": "Export abgeschlossen", + "download_now": "MBTiles herunterladen", + "caching_enabled": "Kachel-Caching", + "clear_cache": "Cache leeren", + "cache_cleared": "Kachel-Cache geleert", + "tile_server_url": "Kachel-Server-URL", + "tile_server_url_placeholder": "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + "tile_server_url_hint": "Verwenden Sie {z}, {x}, {y} für Zoom, Spalte, Zeile", + "tile_server_saved": "Kachel-Server-URL gespeichert", + "nominatim_api_url": "Nominatim API-URL", + "nominatim_api_url_placeholder": "https://nominatim.openstreetmap.org", + "nominatim_api_url_hint": "Basis-URL für den Nominatim-Geocoding-Service", + "nominatim_api_saved": "Nominatim API-URL gespeichert", + "search_placeholder": "Nach einem Ort suchen...", + "search_offline_error": "Suche ist nur im Online-Modus verfügbar", + "search_connection_error": "Verbindung zum Suchdienst fehlgeschlagen. Bitte überprüfen Sie Ihre Internetverbindung.", + "search_error": "Suchfehler", + "search_no_results": "Keine Ergebnisse gefunden", + "custom_tile_server_unavailable": "Benutzerdefinierter Kachelserver ist im Offline-Modus nicht erreichbar", + "custom_nominatim_unavailable": "Benutzerdefinierter Nominatim-Server ist im Offline-Modus nicht erreichbar", + "onboarding_title": "Als MBTiles exportieren!", + "onboarding_text": "Verwenden Sie das Export-Tool, um Kartenbereiche als MBTiles-Dateien für die Offline-Nutzung herunterzuladen.", + "onboarding_got_it": "Verstanden" + }, + "interface": { + "disable": "Deaktivieren", + "enable": "Aktivieren", + "edit_interface": "Schnittstelle bearbeiten", + "export_interface": "Schnittstelle exportieren", + "delete_interface": "Schnittstelle löschen", + "listen": "Abhören", + "forward": "Weiterleiten", + "port": "Port", + "frequency": "Frequenz", + "bandwidth": "Bandbreite", + "txpower": "Sendeleistung (TX)", + "spreading_factor": "SF", + "coding_rate": "Kodierungsrate", + "bitrate": "Bitrate", + "tx": "TX", + "rx": "RX", + "noise": "Rauschen", + "clients": "Clients" + }, + "messages": { + "title": "Nachrichten", + "conversations": "Gespräche", + "announces": "Ankündigungen", + "search_placeholder": "Suche in {count} Gesprächen...", + "unread": "Ungelesen", + "failed": "Fehlgeschlagen", + "attachments": "Anhänge", + "no_messages_yet": "Noch keine Nachrichten", + "loading_conversations": "Gespräche werden geladen...", + "no_conversations": "Keine Gespräche", + "discover_peers": "Entdecken Sie Peers auf dem Tab 'Ankündigungen'", + "no_search_results": "Keine Ergebnisse gefunden", + "try_another_search": "Versuchen Sie einen anderen Suchbegriff", + "no_search_results_conversations": "Ihre Suche ergab keine Treffer in den Gesprächen.", + "search_placeholder_announces": "Suche in {count} aktuellen Ankündigungen...", + "no_peers_discovered": "Keine Peers entdeckt", + "waiting_for_announce": "Warten auf Ankündigungen!", + "no_search_results_peers": "Ihre Suche ergab keine Treffer bei den Peers!", + "direct": "Direkt", + "hops": "{count} Hops", + "hops_away": "{count} Hops entfernt", + "snr": "SNR {snr}", + "stamp_cost": "Stempelkosten {cost}", + "pop_out_chat": "Chat auslagern", + "custom_display_name": "Benutzerdefinierter Anzeigename", + "send_placeholder": "Schreibe eine Nachricht...", + "no_messages_in_conversation": "Noch keine Nachrichten in diesem Gespräch.", + "say_hello": "Sag Hallo!", + "no_active_chat": "Kein aktiver Chat", + "select_peer_or_enter_address": "Wählen Sie einen Peer aus der Seitenleiste oder geben Sie unten eine Adresse ein", + "add_files": "Dateien hinzufügen", + "recording": "Aufnahme: {duration}", + "nomad_network_node": "Nomad Network Knoten", + "toggle_source": "Quellcode umschalten" + }, + "nomadnet": { + "remove_favourite": "Favorit entfernen", + "add_favourite": "Favorit hinzufügen", + "page_archives": "Seitenarchive", + "archive_current_version": "Aktuelle Version archivieren", + "no_archives_for_this_page": "Keine Archive für diese Seite", + "viewing_archived_version_from": "Archivierte Version vom {time} anzeigen", + "viewing_archived_version": "Archivierte Version wird angezeigt", + "load_live": "Live laden", + "failed_to_load_page": "Seite konnte nicht geladen werden", + "archived_version_available": "Eine archivierte Version dieser Seite ist verfügbar.", + "view_archive": "Archiv anzeigen", + "no_active_node": "Kein aktiver Knoten", + "select_node_to_browse": "Wählen Sie einen Knoten aus, um mit dem Surfen zu beginnen!", + "open_nomadnet_url": "Eine Nomadnet-URL öffnen", + "unknown_node": "Unbekannter Knoten", + "existing_download_in_progress": "Ein bestehender Download ist im Gange. Bitte warten Sie, bis dieser abgeschlossen ist, bevor Sie einen weiteren Download starten.", + "favourites": "Favoriten", + "announces": "Ankündigungen", + "search_favourites_placeholder": "Suche in {count} Favoriten...", + "rename": "Umbenennen", + "remove": "Entfernen", + "no_favourites": "Keine Favoriten", + "add_nodes_from_announces": "Fügen Sie Knoten über den Tab 'Ankündigungen' hinzu.", + "search_announces": "Ankündigungen durchsuchen", + "announced_time_ago": "Vor {time} angekündigt", + "block_node": "Knoten blockieren", + "no_announces_yet": "Noch keine Ankündigungen", + "listening_for_peers": "Höre auf Peers im Mesh.", + "block_node_confirm": "Sind Sie sicher, dass Sie {name} blockieren möchten? Seine Ankündigungen werden ignoriert und er erscheint nicht mehr im Ankündigungs-Stream.", + "node_blocked_successfully": "Knoten erfolgreich blockiert", + "failed_to_block_node": "Knoten konnte nicht blockiert werden", + "rename_favourite": "Diesen Favoriten umbenennen", + "remove_favourite_confirm": "Sind Sie sicher, dass Sie diesen Favoriten entfernen möchten?", + "enter_nomadnet_url": "Nomadnet-URL eingeben", + "archiving_page": "Seite wird archiviert...", + "page_archived_successfully": "Seite erfolgreich archiviert.", + "identify_confirm": "Sind Sie sicher, dass Sie sich gegenüber diesem NomadNetwork-Knoten identifizieren möchten? Die Seite wird nach dem Senden Ihrer Identität neu geladen." + }, + "forwarder": { + "title": "LXMF-Weiterleiter", + "description": "Nachrichten von einer Adresse zu einer anderen weiterleiten, mit transparenter Rückleitung. Wie SimpleLogin für LXMF.", + "add_rule": "Weiterleitungsregel hinzufügen", + "forward_to_hash": "Weiterleiten an Hash", + "destination_placeholder": "Ziel-LXMF-Hash...", + "source_filter": "Quellfilter (Optional)", + "source_filter_placeholder": "Nur von diesem Hash weiterleiten...", + "add_button": "Regel hinzufügen", + "active_rules": "Aktive Regeln", + "no_rules": "Keine Weiterleitungsregeln konfiguriert.", + "active": "Aktiv", + "disabled": "Deaktiviert", + "forwarding_to": "Weiterleitung an: {hash}", + "source_filter_display": "Quellfilter: {hash}", + "delete_confirm": "Sind Sie sicher, dass Sie diese Regel löschen möchten?" + }, + "archives": { + "description": "Archivierte Nomad Network Seiten suchen und anzeigen", + "search_placeholder": "Suche nach Inhalt, Hash oder Pfad...", + "loading": "Archive werden geladen...", + "no_archives_found": "Keine Archive gefunden", + "adjust_filters": "Versuchen Sie, Ihre Suchfilter anzupassen.", + "browse_to_archive": "Archivierte Seiten erscheinen hier, sobald Sie Nomad Network Seiten besuchen.", + "page": "Seite", + "pages": "Seiten", + "view": "Anzeigen", + "showing_range": "Zeige {start} bis {end} von {total} Archiven", + "page_of": "Seite {page} von {total_pages}" + }, + "tools": { + "utilities": "Dienstprogramme", + "power_tools": "Power-Tools für Betreiber", + "diagnostics_description": "Diagnose- und Firmware-Helfer werden mit MeshChat geliefert, damit Sie Peers ohne Verlassen der Konsole Fehler beheben können.", + "ping": { + "title": "Ping", + "description": "Latenztest für jeden LXMF-Ziel-Hash mit Live-Status." + }, + "rnprobe": { + "title": "RNProbe", + "description": "Ziele mit benutzerdefinierten Paketgrößen sondieren, um die Konnektivität zu testen." + }, + "rncp": { + "title": "RNCP", + "description": "Dateien über RNS mit Fortschrittsanzeige senden und empfangen." + }, + "rnstatus": { + "title": "RNStatus", + "description": "Schnittstellenstatistiken und Netzwerkstatusinformationen anzeigen." + }, + "translator": { + "title": "Übersetzer", + "description": "Text mit der LibreTranslate API oder lokalem Argos Translate übersetzen." + }, + "forwarder": { + "title": "Weiterleiter", + "description": "LXMF-Weiterleitung im SimpleLogin-Stil mit Rückpfad-Routing." + }, + "rnode_flasher": { + "title": "RNode Flasher", + "description": "RNode-Adapter flashen und aktualisieren, ohne die Kommandozeile zu berühren." + } + }, + "ping": { + "title": "Mesh-Peers anpingen", + "description": "Nur {code}-Ziele antworten auf Ping.", + "destination_hash": "Ziel-Hash", + "timeout_seconds": "Ping-Timeout (Sekunden)", + "start_ping": "Ping starten", + "stop": "Stopp", + "clear_results": "Ergebnisse löschen", + "drop_path": "Pfad verwerfen", + "status": "Status", + "running": "Läuft", + "idle": "Leerlauf", + "last_rtt": "Letzte RTT", + "last_error": "Letzter Fehler", + "console_output": "Konsolenausgabe", + "streaming_responses": "Streaming von Seq-Antworten in Echtzeit", + "no_pings_yet": "Noch keine Pings. Starten Sie einen Durchlauf, um RTT-Daten zu sammeln.", + "invalid_hash": "Ungültiger Ziel-Hash!", + "timeout_must_be_number": "Timeout muss eine Zahl sein!" + }, + "rncp": { + "file_transfer": "Dateiübertragung", + "title": "RNCP - Reticulum Network Copy", + "description": "Senden und Empfangen von Dateien über das Reticulum-Netzwerk unter Verwendung von RNS-Ressourcen.", + "send_file": "Datei senden", + "fetch_file": "Datei abrufen", + "listen": "Hören", + "destination_hash": "Ziel-Hash", + "file_path": "Dateipfad", + "timeout_seconds": "Timeout (Sekunden)", + "disable_compression": "Komprimierung deaktivieren", + "cancel": "Abbrechen", + "progress": "Fortschritt", + "invalid_hash": "Ungültiger Ziel-Hash!", + "provide_file_path": "Bitte geben Sie einen Dateipfad an!", + "file_sent_successfully": "Datei erfolgreich gesendet. Transfer-ID: {id}", + "failed_to_send": "Senden der Datei fehlgeschlagen", + "remote_file_path": "Remote-Dateipfad", + "save_path_optional": "Speicherpfad (optional)", + "save_path_placeholder": "Leer lassen für aktuelles Verzeichnis", + "allow_overwrite": "Überschreiben zulassen", + "provide_remote_file_path": "Bitte geben Sie einen Remote-Dateipfad an!", + "file_fetched_successfully": "Datei erfolgreich abgerufen. Gespeichert unter: {path}", + "failed_to_fetch": "Abrufen der Datei fehlgeschlagen", + "allowed_hashes": "Erlaubte Identitäts-Hashes (einer pro Zeile)", + "fetch_jail_path": "Abruf-Jail-Pfad (optional)", + "allow_fetch": "Abruf zulassen", + "start_listening": "Hören starten", + "stop_listening": "Hören stoppen", + "listening_on": "Hört auf:", + "provide_allowed_hash": "Bitte geben Sie mindestens einen erlaubten Identitäts-Hash an!", + "failed_to_start_listener": "Starten des Listeners fehlgeschlagen" + }, + "rnprobe": { + "network_diagnostics": "Netzwerkdiagnose", + "title": "RNProbe - Ziel-Probe", + "description": "Ziele mit benutzerdefinierten Paketgrößen abfragen, um die Konnektivität zu testen und die RTT zu messen.", + "destination_hash": "Ziel-Hash", + "full_destination_name": "Vollständiger Zielname", + "probe_size_bytes": "Probe-Größe (Bytes)", + "number_of_probes": "Anzahl der Probes", + "wait_between_probes": "Warten zwischen Probes (Sekunden)", + "start_probe": "Probe starten", + "stop": "Stopp", + "clear_results": "Ergebnisse löschen", + "summary": "Zusammenfassung", + "sent": "Gesendet", + "delivered": "Zugestellt", + "timeouts": "Timeouts", + "failed": "Fehlgeschlagen", + "probe_results": "Probe-Ergebnisse", + "probe_responses_realtime": "Probe-Antworten in Echtzeit", + "no_probes_yet": "Noch keine Probes. Starten Sie eine Probe, um die Konnektivität zu testen.", + "probe_number": "Probe #{number}", + "bytes": "Bytes", + "hops": "Hops", + "rtt": "RTT", + "rssi": "RSSI", + "snr": "SNR", + "quality": "Qualität", + "timeout": "Timeout", + "invalid_hash": "Ungültiger Ziel-Hash!", + "provide_full_name": "Bitte geben Sie einen vollständigen Zielnamen an!", + "failed_to_probe": "Probe des Ziels fehlgeschlagen" + }, + "rnstatus": { + "network_diagnostics": "Netzwerkdiagnose", + "title": "RNStatus - Netzwerkstatus", + "description": "Schnittstellenstatistiken und Netzwerkstatusinformationen anzeigen.", + "refresh": "Aktualisieren", + "include_link_stats": "Link-Statistiken einbeziehen", + "sort_by": "Sortieren nach:", + "none": "Keine", + "bitrate": "Bitrate", + "rx_bytes": "RX-Bytes", + "tx_bytes": "TX-Bytes", + "total_traffic": "Gesamtverkehr", + "announces": "Ankündigungen", + "active_links": "Aktive Links: {count}", + "no_interfaces_found": "Keine Schnittstellen gefunden. Klicken Sie auf Aktualisieren, um den Status zu laden.", + "mode": "Modus", + "rx_packets": "RX-Pakete", + "tx_packets": "TX-Pakete", + "clients": "Clients", + "peers_reachable": "erreichbar", + "noise_floor": "Rauschteppich", + "interference": "Interferenzen", + "cpu_load": "CPU-Last", + "cpu_temp": "CPU-Temp", + "memory_load": "Speicherlast", + "battery": "Batterie", + "network": "Netzwerk", + "incoming_announces": "Eingehende Ankündigungen", + "outgoing_announces": "Ausgehende Ankündigungen", + "airtime": "Airtime", + "channel_load": "Kanallast" + }, + "translator": { + "text_translation": "Textübersetzung", + "title": "Übersetzer", + "description": "Text mit der LibreTranslate-API oder dem lokalen Argos Translate übersetzen.", + "argos_translate": "Argos Translate", + "libretranslate": "LibreTranslate", + "api_server": "LibreTranslate API-Server", + "api_server_description": "Geben Sie die Basis-URL Ihres LibreTranslate-Servers ein (z. B. http://localhost:5000)", + "source_language": "Quellsprache", + "auto_detect": "Automatisch erkennen", + "target_language": "Zielsprache", + "select_target_language": "Zielsprache auswählen", + "argos_not_detected": "Argos Translate nicht erkannt", + "argos_not_detected_desc": "Um die lokale Übersetzung zu verwenden, müssen Sie das Paket Argos Translate mit einer der folgenden Methoden installieren:", + "method_pip_venv": "Methode 1: pip (venv)", + "method_pipx": "Methode 2: pipx", + "note_restart_required": "Hinweis: Nach der Installation müssen Sie die Anwendung möglicherweise neu starten und Sprachpakete über die Argos Translate CLI installieren.", + "no_language_packages": "Keine Sprachpakete erkannt", + "no_language_packages_desc": "Argos Translate ist installiert, aber es sind keine Sprachpakete verfügbar. Installieren Sie Sprachpakete mit einem der folgenden Befehle:", + "install_all_languages": "Alle Sprachen installieren", + "install_specific_pair": "Bestimmtes Sprachpaar installieren (Beispiel: Englisch nach Deutsch)", + "after_install_note": "Klicken Sie nach der Installation der Sprachpakete auf „Sprachen aktualisieren“, um die verfügbaren Sprachen neu zu laden.", + "text_to_translate": "Zu übersetzender Text", + "enter_text_placeholder": "Text zum Übersetzen eingeben...", + "translate": "Übersetzen", + "swap": "Tauschen", + "clear": "Löschen", + "translation": "Übersetzung", + "source": "Quelle", + "detected": "Erkannt", + "available_languages": "Verfügbare Sprachen", + "languages_loaded_from": "Sprachen werden von der LibreTranslate-API oder Argos Translate-Paketen geladen.", + "refresh_languages": "Sprachen aktualisieren", + "failed_to_load_languages": "Sprachen konnten nicht geladen werden. Stellen Sie sicher, dass LibreTranslate ausgeführt wird oder Argos Translate installiert ist.", + "copied_to_clipboard": "In die Zwischenablage kopiert" + } +} diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json new file mode 100644 index 0000000..ae1cbd1 --- /dev/null +++ b/meshchatx/src/frontend/locales/en.json @@ -0,0 +1,541 @@ +{ + "app": { + "name": "Reticulum MeshChatX", + "sync_messages": "Sync Messages", + "compose": "Compose", + "messages": "Messages", + "nomad_network": "Nomad Network", + "map": "Map", + "archives": "Archives", + "propagation_nodes": "Propagation Nodes", + "network_visualiser": "Network Visualiser", + "interfaces": "Interfaces", + "tools": "Tools", + "settings": "Settings", + "about": "About", + "my_identity": "My Identity", + "identity_hash": "Identity Hash", + "lxmf_address": "LXMF Address", + "announce": "Announce", + "announce_now": "Announce Now", + "last_announced": "Last announced: {time}", + "last_announced_never": "Last announced: Never", + "display_name_placeholder": "Display Name", + "profile": "Profile", + "manage_identity": "Manage your identity, transport participation and LXMF defaults.", + "theme": "Theme", + "theme_mode": "{mode} mode", + "transport": "Transport", + "enabled": "Enabled", + "disabled": "Disabled", + "propagation": "Propagation", + "local_node_running": "Local node running", + "client_only": "Client-only", + "copy": "Copy", + "appearance": "Appearance", + "appearance_description": "Switch between light and dark presets anytime.", + "light_theme": "Light Theme", + "dark_theme": "Dark Theme", + "live_preview": "Live preview updates instantly.", + "realtime": "Realtime", + "transport_mode": "Transport Mode", + "transport_description": "Relay paths and traffic for nearby peers.", + "enable_transport_mode": "Enable Transport Mode", + "transport_toggle_description": "Route announces, respond to path requests and help your mesh stay online.", + "requires_restart": "Requires restart after toggling.", + "show_community_interfaces": "Show Community Interfaces", + "community_interfaces_description": "Surface community-maintained presets while adding new interfaces.", + "reliability": "Reliability", + "messages_description": "Configure how MeshChat handles message delivery failures. Control automatic retry behavior, attachment retransmission, and fallback mechanisms to ensure reliable message delivery across the mesh network.", + "auto_resend_title": "Auto resend when peer announces", + "auto_resend_description": "Failed messages automatically retry once the destination broadcasts again.", + "retry_attachments_title": "Allow retries with attachments", + "retry_attachments_description": "Large payloads will also be retried (useful when both peers have high limits).", + "auto_fallback_title": "Auto fall back to propagation node", + "auto_fallback_description": "Failed direct deliveries are queued on your preferred propagation node.", + "inbound_stamp_cost": "Inbound Message Stamp Cost", + "inbound_stamp_description": "Require proof-of-work stamps for direct delivery messages sent to you. Higher values require more computational work from senders. Range: 1-254. Default: 8.", + "browse_nodes": "Browse Nodes", + "propagation_nodes_description": "Keep conversations flowing even when peers are offline.", + "nodes_info_1": "Propagation nodes hold messages securely until recipients sync again.", + "nodes_info_2": "Nodes peer with each other to distribute encrypted payloads.", + "nodes_info_3": "Most nodes retain data ~30 days, then discard undelivered items.", + "run_local_node": "Run a local propagation node", + "run_local_node_description": "MeshChat will announce and maintain a node using this local destination hash.", + "preferred_propagation_node": "Preferred Propagation Node", + "preferred_node_placeholder": "Destination hash, e.g. a39610c89d18bb48c73e429582423c24", + "fallback_node_description": "Messages fallback to this node whenever direct delivery fails.", + "auto_sync_interval": "Auto Sync Interval", + "last_synced": "Last synced {time} ago.", + "last_synced_never": "Last synced: never.", + "propagation_stamp_cost": "Propagation Node Stamp Cost", + "propagation_stamp_description": "Require proof-of-work stamps for messages propagated through your node. Higher values require more computational work. Range: 13-254. Default: 16. **Note:** Changing this requires restarting the app.", + "language": "Language", + "select_language": "Select your preferred language.", + "custom_fork_by": "Custom fork by", + "open": "Open", + "identity": "Identity", + "lxmf_address_hash": "LXMF Address Hash", + "propagation_node_status": "Propagation Node Status", + "last_sync": "Last Sync: {time} ago", + "last_sync_never": "Last Sync: never", + "syncing": "Syncing...", + "synced": "Synced", + "not_synced": "Not Synced", + "not_configured": "Not Configured", + "toggle_source": "Toggle Source Code", + "audio_calls": "Telephone", + "calls": "Calls", + "status": "Status", + "active_call": "Active Call", + "incoming": "Incoming", + "outgoing": "Outgoing", + "call": "Call", + "calls_plural": "Calls", + "hop": "hop", + "hops_plural": "hops", + "hung_up_waiting": "Hung up, waiting for call...", + "view_incoming_calls": "View Incoming Calls", + "hangup_all_calls": "Hangup all Calls", + "clear_history": "Clear History", + "no_active_calls": "No active calls", + "incoming_call": "Incoming call...", + "outgoing_call": "Outgoing call...", + "call_active": "Active", + "call_ended": "Ended", + "propagation_node": "Propagation Node", + "sync_now": "Sync Now" + }, + "common": { + "open": "Open", + "cancel": "Cancel", + "save": "Save", + "delete": "Delete", + "edit": "Edit", + "add": "Add", + "sync": "Sync", + "restart_app": "Restart App", + "reveal": "Reveal", + "refresh": "Refresh", + "vacuum": "Vacuum", + "auto_recover": "Auto Recover" + }, + "about": { + "title": "About", + "version": "v{version}", + "rns_version": "RNS {version}", + "lxmf_version": "LXMF {version}", + "python_version": "Python {version}", + "config_path": "Config path", + "database_path": "Database path", + "database_size": "Database size", + "database_health": "Database Health", + "database_health_description": "Quick check, WAL tuning, and recovery tools for the MeshChatX database.", + "running_checks": "Running checks...", + "integrity": "Integrity", + "journal_mode": "Journal mode", + "wal_autocheckpoint": "WAL autocheckpoint", + "page_size": "Page size", + "pages_free": "Pages / Free", + "free_space_estimate": "Free space estimate", + "system_resources": "System Resources", + "live": "Live", + "memory_rss": "Memory (RSS)", + "virtual_memory": "Virtual Memory", + "network_stats": "Network Stats", + "sent": "Sent", + "received": "Received", + "packets_sent": "Packets Sent", + "packets_received": "Packets Received", + "reticulum_stats": "Reticulum Stats", + "total_paths": "Total Paths", + "announces_per_second": "Announces / sec", + "announces_per_minute": "Announces / min", + "announces_per_hour": "Announces / hr", + "download_activity": "Download Activity", + "no_downloads_yet": "No downloads yet", + "runtime_status": "Runtime Status", + "shared_instance": "Shared Instance", + "standalone_instance": "Standalone Instance", + "transport_enabled": "Transport Enabled", + "transport_disabled": "Transport Disabled", + "identity_addresses": "Identity & Addresses", + "telephone_address": "Telephone Address" + }, + "interfaces": { + "title": "Interfaces", + "manage": "Manage", + "description": "Search, filter and export your Reticulum adapters.", + "add_interface": "Add Interface", + "import": "Import", + "export_all": "Export all", + "search_placeholder": "Search by name, type, host...", + "all": "All", + "all_types": "All types", + "no_interfaces_found": "No interfaces found", + "no_interfaces_description": "Adjust your search or add a new interface.", + "restart_required": "Restart required", + "restart_description": "Reticulum MeshChat must be restarted for any interface changes to take effect.", + "restart_now": "Restart now" + }, + "map": { + "title": "Map", + "description": "Offline-capable map with MBTiles support.", + "upload_mbtiles": "Upload MBTiles", + "select_file": "Select MBTiles file", + "offline_mode": "Offline Mode", + "online_mode": "Online Mode", + "attribution": "Attribution", + "bounds": "Bounds", + "center": "Center", + "zoom": "Zoom", + "uploading": "Uploading...", + "upload_success": "Map uploaded successfully", + "upload_failed": "Failed to upload map", + "no_map_loaded": "No offline map loaded. Upload an .mbtiles file to enable offline mode.", + "invalid_file": "Invalid MBTiles file. Only raster tiles are supported.", + "default_view": "Default View", + "set_as_default": "Set as Default View", + "export_area": "Export Area", + "export_instructions": "Drag on the map to select an area.", + "min_zoom": "Min Zoom", + "max_zoom": "Max Zoom", + "tile_count": "Estimated Tiles", + "start_export": "Start Export", + "exporting": "Exporting Map...", + "download_ready": "Export Complete", + "download_now": "Download MBTiles", + "caching_enabled": "Tile Caching", + "clear_cache": "Clear Cache", + "cache_cleared": "Tile cache cleared", + "tile_server_url": "Tile Server URL", + "tile_server_url_placeholder": "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + "tile_server_url_hint": "Use {z}, {x}, {y} for zoom, column, row", + "tile_server_saved": "Tile server URL saved", + "nominatim_api_url": "Nominatim API URL", + "nominatim_api_url_placeholder": "https://nominatim.openstreetmap.org", + "nominatim_api_url_hint": "Base URL for Nominatim geocoding service", + "nominatim_api_saved": "Nominatim API URL saved", + "search_placeholder": "Search for a location...", + "search_offline_error": "Search is only available in online mode", + "search_connection_error": "Failed to connect to search service. Please check your internet connection.", + "search_error": "Search error", + "search_no_results": "No results found", + "custom_tile_server_unavailable": "Custom tile server is not accessible in offline mode", + "custom_nominatim_unavailable": "Custom Nominatim server is not accessible in offline mode", + "onboarding_title": "Export to MBTiles!", + "onboarding_text": "Use the export tool to download map areas as MBTiles files for offline use.", + "onboarding_got_it": "Got it" + }, + "interface": { + "disable": "Disable", + "enable": "Enable", + "edit_interface": "Edit Interface", + "export_interface": "Export Interface", + "delete_interface": "Delete Interface", + "listen": "Listen", + "forward": "Forward", + "port": "Port", + "frequency": "Frequency", + "bandwidth": "Bandwidth", + "txpower": "TX Power", + "spreading_factor": "SF", + "coding_rate": "Coding Rate", + "bitrate": "Bitrate", + "tx": "TX", + "rx": "RX", + "noise": "Noise", + "clients": "Clients" + }, + "messages": { + "title": "Messages", + "conversations": "Conversations", + "announces": "Announces", + "search_placeholder": "Search {count} conversations...", + "unread": "Unread", + "failed": "Failed", + "attachments": "Attachments", + "no_messages_yet": "No messages yet", + "loading_conversations": "Loading conversations...", + "no_conversations": "No Conversations", + "discover_peers": "Discover peers on the Announces tab", + "no_search_results": "No Results Found", + "try_another_search": "Try a different search term", + "no_search_results_conversations": "Your search didn't match any conversations.", + "search_placeholder_announces": "Search {count} recent announces...", + "no_peers_discovered": "No Peers Discovered", + "waiting_for_announce": "Waiting for someone to announce!", + "no_search_results_peers": "Your search didn't match any Peers!", + "direct": "Direct", + "hops": "{count} hops", + "hops_away": "{count} hops away", + "snr": "SNR {snr}", + "stamp_cost": "Stamp Cost {cost}", + "pop_out_chat": "Pop out chat", + "custom_display_name": "Custom Display Name", + "send_placeholder": "Type a message...", + "no_messages_in_conversation": "No messages in this conversation yet.", + "say_hello": "Say hello!", + "no_active_chat": "No Active Chat", + "select_peer_or_enter_address": "Select a peer from the sidebar or enter an address below", + "add_files": "Add Files", + "recording": "Recording: {duration}", + "nomad_network_node": "Nomad Network Node", + "toggle_source": "Toggle Source Code" + }, + "nomadnet": { + "remove_favourite": "Remove Favourite", + "add_favourite": "Add Favourite", + "page_archives": "Page Archives", + "archive_current_version": "Archive Current Version", + "no_archives_for_this_page": "No archives for this page", + "viewing_archived_version_from": "Viewing archived version from {time}", + "viewing_archived_version": "Viewing archived version", + "load_live": "Load Live", + "failed_to_load_page": "Failed to load page", + "archived_version_available": "An archived version of this page is available.", + "view_archive": "View Archive", + "no_active_node": "No Active Node", + "select_node_to_browse": "Select a Node to start browsing!", + "open_nomadnet_url": "Open a Nomadnet URL", + "unknown_node": "Unknown Node", + "existing_download_in_progress": "An existing download is in progress. Please wait for it to finish before starting another download.", + "favourites": "Favourites", + "announces": "Announces", + "search_favourites_placeholder": "Search {count} favourites...", + "rename": "Rename", + "remove": "Remove", + "no_favourites": "No favourites", + "add_nodes_from_announces": "Add nodes from the announces tab.", + "search_announces": "Search announces", + "announced_time_ago": "Announced {time} ago", + "block_node": "Block Node", + "no_announces_yet": "No announces yet", + "listening_for_peers": "Listening for peers on the mesh.", + "block_node_confirm": "Are you sure you want to block {name}? Their announces will be ignored and they won't appear in the announce stream.", + "node_blocked_successfully": "Node blocked successfully", + "failed_to_block_node": "Failed to block node", + "rename_favourite": "Rename this favourite", + "remove_favourite_confirm": "Are you sure you want to remove this favourite?", + "enter_nomadnet_url": "Enter a Nomadnet URL", + "archiving_page": "Archiving page...", + "page_archived_successfully": "Page archived successfully.", + "identify_confirm": "Are you sure you want to identify yourself to this NomadNetwork Node? The page will reload after your identity has been sent." + }, + "forwarder": { + "title": "LXMF Forwarder", + "description": "Forward messages from one address to another, with transparent return routing. Like SimpleLogin for LXMF.", + "add_rule": "Add Forwarding Rule", + "forward_to_hash": "Forward to Hash", + "destination_placeholder": "Destination LXMF hash...", + "source_filter": "Source Filter (Optional)", + "source_filter_placeholder": "Only forward from this hash...", + "add_button": "Add Rule", + "active_rules": "Active Rules", + "no_rules": "No forwarding rules configured.", + "active": "Active", + "disabled": "Disabled", + "forwarding_to": "Forwarding to: {hash}", + "source_filter_display": "Source filter: {hash}", + "delete_confirm": "Are you sure you want to delete this rule?" + }, + "archives": { + "description": "Search and view archived Nomad Network pages", + "search_placeholder": "Search content, hash or path...", + "loading": "Loading archives...", + "no_archives_found": "No archives found", + "adjust_filters": "Try adjusting your search filters.", + "browse_to_archive": "Archived pages will appear here once you browse Nomad Network sites.", + "page": "Page", + "pages": "Pages", + "view": "View", + "showing_range": "Showing {start} to {end} of {total} archives", + "page_of": "Page {page} of {total_pages}" + }, + "tools": { + "utilities": "Utilities", + "power_tools": "Power tools for operators", + "diagnostics_description": "Diagnostics and firmware helpers ship with MeshChat so you can troubleshoot peers without leaving the console.", + "ping": { + "title": "Ping", + "description": "Latency test for any LXMF destination hash with live status." + }, + "rnprobe": { + "title": "RNProbe", + "description": "Probe destinations with custom packet sizes to test connectivity." + }, + "rncp": { + "title": "RNCP", + "description": "Send and receive files over RNS with progress tracking." + }, + "rnstatus": { + "title": "RNStatus", + "description": "View interface statistics and network status information." + }, + "translator": { + "title": "Translator", + "description": "Translate text using LibreTranslate API or local Argos Translate." + }, + "forwarder": { + "title": "Forwarder", + "description": "SimpleLogin-style LXMF forwarding with return path routing." + }, + "rnode_flasher": { + "title": "RNode Flasher", + "description": "Flash and update RNode adapters without touching the command line." + } + }, + "ping": { + "title": "Ping Mesh Peers", + "description": "Only {code} destinations respond to ping.", + "destination_hash": "Destination Hash", + "timeout_seconds": "Ping Timeout (seconds)", + "start_ping": "Start Ping", + "stop": "Stop", + "clear_results": "Clear Results", + "drop_path": "Drop Path", + "status": "Status", + "running": "Running", + "idle": "Idle", + "last_rtt": "Last RTT", + "last_error": "Last Error", + "console_output": "Console Output", + "streaming_responses": "Streaming seq responses in real time", + "no_pings_yet": "No pings yet. Start a run to collect RTT data.", + "invalid_hash": "Invalid destination hash!", + "timeout_must_be_number": "Timeout must be a number!" + }, + "rncp": { + "file_transfer": "File Transfer", + "title": "RNCP - Reticulum Network Copy", + "description": "Send and receive files over the Reticulum network using RNS resources.", + "send_file": "Send File", + "fetch_file": "Fetch File", + "listen": "Listen", + "destination_hash": "Destination Hash", + "file_path": "File Path", + "timeout_seconds": "Timeout (seconds)", + "disable_compression": "Disable compression", + "cancel": "Cancel", + "progress": "Progress", + "invalid_hash": "Invalid destination hash!", + "provide_file_path": "Please provide a file path!", + "file_sent_successfully": "File sent successfully. Transfer ID: {id}", + "failed_to_send": "Failed to send file", + "remote_file_path": "Remote File Path", + "save_path_optional": "Save Path (optional)", + "save_path_placeholder": "Leave empty for current directory", + "allow_overwrite": "Allow overwrite", + "provide_remote_file_path": "Please provide a remote file path!", + "file_fetched_successfully": "File fetched successfully. Saved to: {path}", + "failed_to_fetch": "Failed to fetch file", + "allowed_hashes": "Allowed Identity Hashes (one per line)", + "fetch_jail_path": "Fetch Jail Path (optional)", + "allow_fetch": "Allow fetch", + "start_listening": "Start Listening", + "stop_listening": "Stop Listening", + "listening_on": "Listening on:", + "provide_allowed_hash": "Please provide at least one allowed identity hash!", + "failed_to_start_listener": "Failed to start listener" + }, + "rnprobe": { + "network_diagnostics": "Network Diagnostics", + "title": "RNProbe - Destination Probe", + "description": "Probe destinations with custom packet sizes to test connectivity and measure RTT.", + "destination_hash": "Destination Hash", + "full_destination_name": "Full Destination Name", + "probe_size_bytes": "Probe Size (bytes)", + "number_of_probes": "Number of Probes", + "wait_between_probes": "Wait Between Probes (seconds)", + "start_probe": "Start Probe", + "stop": "Stop", + "clear_results": "Clear Results", + "summary": "Summary", + "sent": "Sent", + "delivered": "Delivered", + "timeouts": "Timeouts", + "failed": "Failed", + "probe_results": "Probe Results", + "probe_responses_realtime": "Probe responses in real time", + "no_probes_yet": "No probes yet. Start a probe to test connectivity.", + "probe_number": "Probe #{number}", + "bytes": "bytes", + "hops": "Hops", + "rtt": "RTT", + "rssi": "RSSI", + "snr": "SNR", + "quality": "Quality", + "timeout": "Timeout", + "invalid_hash": "Invalid destination hash!", + "provide_full_name": "Please provide a full destination name!", + "failed_to_probe": "Failed to probe destination" + }, + "rnstatus": { + "network_diagnostics": "Network Diagnostics", + "title": "RNStatus - Network Status", + "description": "View interface statistics and network status information.", + "refresh": "Refresh", + "include_link_stats": "Include Link Stats", + "sort_by": "Sort by:", + "none": "None", + "bitrate": "Bitrate", + "rx_bytes": "RX Bytes", + "tx_bytes": "TX Bytes", + "total_traffic": "Total Traffic", + "announces": "Announces", + "active_links": "Active Links: {count}", + "no_interfaces_found": "No interfaces found. Click refresh to load status.", + "mode": "Mode", + "rx_packets": "RX Packets", + "tx_packets": "TX Packets", + "clients": "Clients", + "peers_reachable": "reachable", + "noise_floor": "Noise Floor", + "interference": "Interference", + "cpu_load": "CPU Load", + "cpu_temp": "CPU Temp", + "memory_load": "Memory Load", + "battery": "Battery", + "network": "Network", + "incoming_announces": "Incoming Announces", + "outgoing_announces": "Outgoing Announces", + "airtime": "Airtime", + "channel_load": "Channel Load" + }, + "translator": { + "text_translation": "Text Translation", + "title": "Translator", + "description": "Translate text using LibreTranslate API or local Argos Translate.", + "argos_translate": "Argos Translate", + "libretranslate": "LibreTranslate", + "api_server": "LibreTranslate API Server", + "api_server_description": "Enter the base URL of your LibreTranslate server (e.g., http://localhost:5000)", + "source_language": "Source Language", + "auto_detect": "Auto-detect", + "target_language": "Target Language", + "select_target_language": "Select target language", + "argos_not_detected": "Argos Translate not detected", + "argos_not_detected_desc": "To use local translation, you must install the Argos Translate package using one of the following methods:", + "method_pip_venv": "Method 1: pip (venv)", + "method_pipx": "Method 2: pipx", + "note_restart_required": "Note: After installation, you may need to restart the application and install language packages via the Argos Translate CLI.", + "no_language_packages": "No language packages detected", + "no_language_packages_desc": "Argos Translate is installed but no language packages are available. Install language packages using one of the following commands:", + "install_all_languages": "Install all languages", + "install_specific_pair": "Install specific language pair (example: English to German)", + "after_install_note": "After installing language packages, click \"Refresh Languages\" to reload available languages.", + "text_to_translate": "Text to Translate", + "enter_text_placeholder": "Enter text to translate...", + "translate": "Translate", + "swap": "Swap", + "clear": "Clear", + "translation": "Translation", + "source": "Source", + "detected": "Detected", + "available_languages": "Available Languages", + "languages_loaded_from": "Languages are loaded from LibreTranslate API or Argos Translate packages.", + "refresh_languages": "Refresh Languages", + "failed_to_load_languages": "Failed to load languages. Make sure LibreTranslate is running or Argos Translate is installed.", + "copied_to_clipboard": "Copied to clipboard" + } +} diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json new file mode 100644 index 0000000..2ab1ba1 --- /dev/null +++ b/meshchatx/src/frontend/locales/ru.json @@ -0,0 +1,541 @@ +{ + "app": { + "name": "Reticulum MeshChatX", + "sync_messages": "Синхронизировать сообщения", + "compose": "Написать", + "messages": "Сообщения", + "nomad_network": "Nomad Network", + "map": "Карта", + "archives": "Архивы", + "propagation_nodes": "Узлы ретрансляции", + "network_visualiser": "Визуализатор сети", + "interfaces": "Интерфейсы", + "tools": "Инструменты", + "settings": "Настройки", + "about": "О программе", + "my_identity": "Моя личность", + "identity_hash": "Хеш личности", + "lxmf_address": "Адрес LXMF", + "announce": "Анонсировать", + "announce_now": "Анонсировать сейчас", + "last_announced": "Последний анонс: {time}", + "last_announced_never": "Последний анонс: Никогда", + "display_name_placeholder": "Отображаемое имя", + "profile": "Профиль", + "manage_identity": "Управляйте своей личностью, участием в транспорте и настройками LXMF по умолчанию.", + "theme": "Тема", + "theme_mode": "Режим {mode}", + "transport": "Транспорт", + "enabled": "Включено", + "disabled": "Выключено", + "propagation": "Ретрансляция", + "local_node_running": "Локальный узел запущен", + "client_only": "Только клиент", + "copy": "Копировать", + "appearance": "Внешний вид", + "appearance_description": "Переключайтесь между светлой и темной темами в любое время.", + "light_theme": "Светлая тема", + "dark_theme": "Темная тема", + "live_preview": "Предпросмотр обновляется мгновенно.", + "realtime": "Реальное время", + "transport_mode": "Режим транспорта", + "transport_description": "Ретранслируйте пути и трафик для ближайших узлов.", + "enable_transport_mode": "Включить режим транспорта", + "transport_toggle_description": "Маршрутизировать анонсы, отвечать на запросы путей и помогать вашей сети оставаться онлайн.", + "requires_restart": "Требуется перезапуск после переключения.", + "show_community_interfaces": "Показывать интерфейсы сообщества", + "community_interfaces_description": "Показывать предустановки, поддерживаемые сообществом, при добавлении новых интерфейсов.", + "reliability": "Надежность", + "messages_description": "Настройте, как MeshChat повторяет или эскалирует неудачные доставки. Контролируйте автоматический повтор, пересылку вложений и механизмы отката для обеспечения надежной доставки сообщений в сети mesh.", + "auto_resend_title": "Автоповтор при анонсе узла", + "auto_resend_description": "Неудачные сообщения будут автоматически отправлены повторно, как только получатель снова выйдет в сеть.", + "retry_attachments_title": "Разрешить повтор для вложений", + "retry_attachments_description": "Крупные вложения также будут отправлены повторно (полезно, если у обоих узлов высокие лимиты).", + "auto_fallback_title": "Автопереключение на узел ретрансляции", + "auto_fallback_description": "Неудачные прямые доставки будут поставлены в очередь на ваш предпочтительный узел ретрансляции.", + "inbound_stamp_cost": "Стоимость входящего штампа", + "inbound_stamp_description": "Требовать штампы Proof-of-Work для сообщений, отправляемых вам напрямую. Более высокие значения требуют больше вычислительной работы от отправителей. Диапазон: 1-254. По умолчанию: 8.", + "browse_nodes": "Список узлов", + "propagation_nodes_description": "Поддерживайте общение, даже когда узлы находятся в офлайне.", + "nodes_info_1": "Узлы ретрансляции надежно хранят сообщения до тех пор, пока получатели снова не синхронизируются.", + "nodes_info_2": "Узлы соединяются друг с другом для распространения зашифрованных данных.", + "nodes_info_3": "Большинство узлов хранят данные около 30 дней, после чего недоставленные сообщения удаляются.", + "run_local_node": "Запустить локальный узел ретрансляции", + "run_local_node_description": "MeshChat будет анонсировать и поддерживать узел, используя этот локальный хеш назначения.", + "preferred_propagation_node": "Предпочтительный узел ретрансляции", + "preferred_node_placeholder": "Хеш назначения, например a39610c89d18bb48c73e429582423c24", + "fallback_node_description": "Сообщения будут отправляться на этот узел, если прямая доставка не удалась.", + "auto_sync_interval": "Интервал автосинхронизации", + "last_synced": "Последняя синхронизация {time} назад.", + "last_synced_never": "Последняя синхронизация: никогда.", + "propagation_stamp_cost": "Стоимость штампа узла ретрансляции", + "propagation_stamp_description": "Требовать штампы Proof-of-Work для сообщений, передаваемых через ваш узел. Более высокие значения требуют больше вычислительной работы. Диапазон: 13-254. По умолчанию: 16. **Примечание:** изменение требует перезапуска приложения.", + "language": "Язык", + "select_language": "Выберите предпочтительный язык.", + "custom_fork_by": "Модифицированная версия от", + "open": "Открыть", + "identity": "Личность", + "lxmf_address_hash": "Хеш адреса LXMF", + "propagation_node_status": "Статус узла ретрансляции", + "last_sync": "Синхронизация: {time} назад", + "last_sync_never": "Синхронизация: никогда", + "syncing": "Синхронизация...", + "synced": "Синхронизировано", + "not_synced": "Не синхронизировано", + "not_configured": "Не настроено", + "toggle_source": "Исходный код", + "audio_calls": "Телефон", + "calls": "Звонки", + "status": "Статус", + "active_call": "Активный звонок", + "incoming": "Входящий", + "outgoing": "Исходящий", + "call": "Звонок", + "calls_plural": "Звонки", + "hop": "прыжок", + "hops_plural": "прыжков", + "hung_up_waiting": "Отключено, ожидание звонка...", + "view_incoming_calls": "Просмотр входящих звонков", + "hangup_all_calls": "Завершить все звонки", + "clear_history": "Очистить историю", + "no_active_calls": "Нет активных звонков", + "incoming_call": "Входящий звонок...", + "outgoing_call": "Исходящий звонок...", + "call_active": "Активен", + "call_ended": "Завершен", + "propagation_node": "Узел ретрансляции", + "sync_now": "Синхронизировать сейчас" + }, + "common": { + "open": "Открыть", + "cancel": "Отмена", + "save": "Сохранить", + "delete": "Удалить", + "edit": "Изменить", + "add": "Добавить", + "sync": "Синхронизировать", + "restart_app": "Перезапустить приложение", + "reveal": "Показать", + "refresh": "Обновить", + "vacuum": "Сжать базу", + "auto_recover": "Автовосстановление" + }, + "about": { + "title": "О программе", + "version": "v{version}", + "rns_version": "RNS {version}", + "lxmf_version": "LXMF {version}", + "python_version": "Python {version}", + "config_path": "Путь к конфигу", + "database_path": "Путь к базе данных", + "database_size": "Размер базы данных", + "database_health": "Состояние базы данных", + "database_health_description": "Быстрая проверка, настройка WAL и инструменты восстановления базы данных MeshChatX.", + "running_checks": "Выполнение проверок...", + "integrity": "Целостность", + "journal_mode": "Режим журнала", + "wal_autocheckpoint": "WAL авточекпоинт", + "page_size": "Размер страницы", + "pages_free": "Страницы / Свободно", + "free_space_estimate": "Оценка свободного места", + "system_resources": "Системные ресурсы", + "live": "Live", + "memory_rss": "Память (RSS)", + "virtual_memory": "Виртуальная память", + "network_stats": "Статистика сети", + "sent": "Отправлено", + "received": "Получено", + "packets_sent": "Пакетов отправлено", + "packets_received": "Пакетов получено", + "reticulum_stats": "Статистика Reticulum", + "total_paths": "Всего путей", + "announces_per_second": "Анонсов / сек", + "announces_per_minute": "Анонсов / мин", + "announces_per_hour": "Анонсов / час", + "download_activity": "Активность загрузки", + "no_downloads_yet": "Загрузок пока нет", + "runtime_status": "Статус выполнения", + "shared_instance": "Общий экземпляр", + "standalone_instance": "Автономный экземпляр", + "transport_enabled": "Транспорт включен", + "transport_disabled": "Транспорт выключен", + "identity_addresses": "Личность и адреса", + "telephone_address": "Адрес телефона" + }, + "interfaces": { + "title": "Интерфейсы", + "manage": "Управление", + "description": "Поиск, фильтрация и экспорт ваших адаптеров Reticulum.", + "add_interface": "Добавить интерфейс", + "import": "Импорт", + "export_all": "Экспорт всех", + "search_placeholder": "Поиск по имени, типу, хосту...", + "all": "Все", + "all_types": "Все типы", + "no_interfaces_found": "Интерфейсы не найдены", + "no_interfaces_description": "Измените параметры поиска или добавьте новый интерфейс.", + "restart_required": "Требуется перезапуск", + "restart_description": "Reticulum MeshChat необходимо перезапустить, чтобы изменения вступили в силу.", + "restart_now": "Перезапустить сейчас" + }, + "map": { + "title": "Карта", + "description": "Офлайн-карта с поддержкой MBTiles.", + "upload_mbtiles": "Загрузить MBTiles", + "select_file": "Выбрать файл MBTiles", + "offline_mode": "Офлайн-режим", + "online_mode": "Онлайн-режим", + "attribution": "Атрибуция", + "bounds": "Границы", + "center": "Центр", + "zoom": "Масштаб", + "uploading": "Загрузка...", + "upload_success": "Карта успешно загружена", + "upload_failed": "Ошибка загрузки карты", + "no_map_loaded": "Офлайн-карта не загружена. Загрузите файл .mbtiles для включения офлайн-режима.", + "invalid_file": "Неверный файл MBTiles. Поддерживаются только растровые карты.", + "default_view": "Вид по умолчанию", + "set_as_default": "Установить как вид по умолчанию", + "export_area": "Экспорт области", + "export_instructions": "Выделите область на карте для экспорта.", + "min_zoom": "Мин. масштаб", + "max_zoom": "Макс. масштаб", + "tile_count": "Оценка тайлов", + "start_export": "Начать экспорт", + "exporting": "Экспорт карты...", + "download_ready": "Экспорт завершен", + "download_now": "Скачать MBTiles", + "caching_enabled": "Кэширование тайлов", + "clear_cache": "Очистить кэш", + "cache_cleared": "Кэш тайлов очищен", + "tile_server_url": "URL сервера тайлов", + "tile_server_url_placeholder": "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + "tile_server_url_hint": "Используйте {z}, {x}, {y} для масштаба, столбца, строки", + "tile_server_saved": "URL сервера тайлов сохранен", + "nominatim_api_url": "URL API Nominatim", + "nominatim_api_url_placeholder": "https://nominatim.openstreetmap.org", + "nominatim_api_url_hint": "Базовый URL для сервиса геокодирования Nominatim", + "nominatim_api_saved": "URL API Nominatim сохранен", + "search_placeholder": "Поиск места...", + "search_offline_error": "Поиск доступен только в онлайн-режиме", + "search_connection_error": "Не удалось подключиться к сервису поиска. Проверьте подключение к интернету.", + "search_error": "Ошибка поиска", + "search_no_results": "Результаты не найдены", + "custom_tile_server_unavailable": "Пользовательский сервер тайлов недоступен в офлайн-режиме", + "custom_nominatim_unavailable": "Пользовательский сервер Nominatim недоступен в офлайн-режиме", + "onboarding_title": "Экспорт в MBTiles!", + "onboarding_text": "Используйте инструмент экспорта для загрузки областей карты в виде файлов MBTiles для офлайн-использования.", + "onboarding_got_it": "Понятно" + }, + "interface": { + "disable": "Выключить", + "enable": "Включить", + "edit_interface": "Изменить интерфейс", + "export_interface": "Экспорт интерфейса", + "delete_interface": "Удалить интерфейс", + "listen": "Слушать", + "forward": "Пересылать", + "port": "Порт", + "frequency": "Частота", + "bandwidth": "Полоса", + "txpower": "Мощность TX", + "spreading_factor": "SF", + "coding_rate": "Скорость кодирования", + "bitrate": "Битрейт", + "tx": "TX", + "rx": "RX", + "noise": "Шум", + "clients": "Клиенты" + }, + "messages": { + "title": "Сообщения", + "conversations": "Чаты", + "announces": "Анонсы", + "search_placeholder": "Поиск по {count} чатам...", + "unread": "Непрочитанные", + "failed": "Ошибки", + "attachments": "Вложения", + "no_messages_yet": "Сообщений пока нет", + "loading_conversations": "Загрузка чатов...", + "no_conversations": "Чаты не найдены", + "discover_peers": "Ищите собеседников во вкладке 'Анонсы'", + "no_search_results": "Ничего не найдено", + "try_another_search": "Попробуйте другой поисковый запрос", + "no_search_results_conversations": "По вашему запросу чатов не найдено.", + "search_placeholder_announces": "Поиск по {count} недавним анонсам...", + "no_peers_discovered": "Собеседники не обнаружены", + "waiting_for_announce": "Ожидание анонсов!", + "no_search_results_peers": "По вашему запросу собеседников не найдено!", + "direct": "Прямая связь", + "hops": "{count} прыжков", + "hops_away": "в {count} прыжках", + "snr": "SNR {snr}", + "stamp_cost": "Стоимость штампа {cost}", + "pop_out_chat": "Открыть в отдельном окне", + "custom_display_name": "Свое имя для контакта", + "send_placeholder": "Введите сообщение...", + "no_messages_in_conversation": "В этом чате пока нет сообщений.", + "say_hello": "Скажите привет!", + "no_active_chat": "Нет активного чата", + "select_peer_or_enter_address": "Выберите собеседника из списка или введите адрес ниже", + "add_files": "Добавить файлы", + "recording": "Запись: {duration}", + "nomad_network_node": "Узел Nomad Network", + "toggle_source": "Исходный код" + }, + "nomadnet": { + "remove_favourite": "Удалить из избранного", + "add_favourite": "Добавить в избранное", + "page_archives": "Архивы страницы", + "archive_current_version": "Архивировать текущую версию", + "no_archives_for_this_page": "Нет архивов для этой страницы", + "viewing_archived_version_from": "Просмотр архивной версии от {time}", + "viewing_archived_version": "Просмотр архивной версии", + "load_live": "Загрузить текущую", + "failed_to_load_page": "Ошибка загрузки страницы", + "archived_version_available": "Доступна архивная версия этой страницы.", + "view_archive": "Просмотреть архив", + "no_active_node": "Нет активного узла", + "select_node_to_browse": "Выберите узел, чтобы начать просмотр!", + "open_nomadnet_url": "Открыть URL Nomadnet", + "unknown_node": "Неизвестный узел", + "existing_download_in_progress": "Уже выполняется загрузка. Дождитесь ее завершения перед началом новой.", + "favourites": "Избранное", + "announces": "Анонсы", + "search_favourites_placeholder": "Поиск по {count} избранным...", + "rename": "Переименовать", + "remove": "Удалить", + "no_favourites": "Нет избранного", + "add_nodes_from_announces": "Добавляйте узлы из вкладки анонсов.", + "search_announces": "Поиск анонсов", + "announced_time_ago": "Анонсировано {time} назад", + "block_node": "Заблокировать узел", + "no_announces_yet": "Анонсов пока нет", + "listening_for_peers": "Ожидание узлов в сети.", + "block_node_confirm": "Вы уверены, что хотите заблокировать {name}? Его анонсы будут игнорироваться, и он не будет отображаться в списке.", + "node_blocked_successfully": "Узел успешно заблокирован", + "failed_to_block_node": "Не удалось заблокировать узел", + "rename_favourite": "Переименовать в избранном", + "remove_favourite_confirm": "Вы уверены, что хотите удалить этот узел из избранного?", + "enter_nomadnet_url": "Введите URL Nomadnet", + "archiving_page": "Архивация страницы...", + "page_archived_successfully": "Страница успешно архивирована.", + "identify_confirm": "Вы уверены, что хотите идентифицировать себя для этого узла NomadNetwork? Страница будет перезагружена после отправки вашей личности." + }, + "forwarder": { + "title": "LXMF Форвардер", + "description": "Пересылка сообщений с одного адреса на другой с прозрачной обратной маршрутизацией. Похоже на SimpleLogin для LXMF.", + "add_rule": "Добавить правило пересылки", + "forward_to_hash": "Пересылать на хеш", + "destination_placeholder": "Хеш назначения LXMF...", + "source_filter": "Фильтр источника (опционально)", + "source_filter_placeholder": "Пересылать только с этого хеша...", + "add_button": "Добавить правило", + "active_rules": "Активные правила", + "no_rules": "Правила пересылки не настроены.", + "active": "Активно", + "disabled": "Выключено", + "forwarding_to": "Пересылка на: {hash}", + "source_filter_display": "Фильтр источника: {hash}", + "delete_confirm": "Вы уверены, что хотите удалить это правило?" + }, + "archives": { + "description": "Поиск и просмотр архивных страниц Nomad Network", + "search_placeholder": "Поиск по содержимому, хешу или пути...", + "loading": "Загрузка архивов...", + "no_archives_found": "Архивы не найдены", + "adjust_filters": "Попробуйте изменить параметры поиска.", + "browse_to_archive": "Архивные страницы появятся здесь после посещения сайтов Nomad Network.", + "page": "Страница", + "pages": "Страницы", + "view": "Просмотр", + "showing_range": "Показано с {start} по {end} из {total} архивов", + "page_of": "Страница {page} из {total_pages}" + }, + "tools": { + "utilities": "Утилиты", + "power_tools": "Инструменты для операторов", + "diagnostics_description": "Средства диагностики и прошивки поставляются вместе с MeshChat для устранения неполадок без использования консоли.", + "ping": { + "title": "Ping", + "description": "Тест задержки для любого LXMF хеша с отображением статуса." + }, + "rnprobe": { + "title": "RNProbe", + "description": "Зондирование узлов пакетами произвольного размера для проверки связи." + }, + "rncp": { + "title": "RNCP", + "description": "Отправка и получение файлов через RNS с отслеживанием прогресса." + }, + "rnstatus": { + "title": "RNStatus", + "description": "Статистика интерфейсов и информация о состоянии сети." + }, + "translator": { + "title": "Translator", + "description": "Перевод текста через LibreTranslate API или локальный Argos Translate." + }, + "forwarder": { + "title": "Forwarder", + "description": "Пересылка LXMF в стиле SimpleLogin с маршрутизацией обратного пути." + }, + "rnode_flasher": { + "title": "RNode Flasher", + "description": "Прошивка и обновление адаптеров RNode без использования командной строки." + } + }, + "ping": { + "title": "Пинг узлов сети", + "description": "Только пункты назначения {code} отвечают на пинг.", + "destination_hash": "Хеш назначения", + "timeout_seconds": "Тайм-аут пинга (секунды)", + "start_ping": "Запустить пинг", + "stop": "Стоп", + "clear_results": "Очистить результаты", + "drop_path": "Сбросить путь", + "status": "Статус", + "running": "Запущено", + "idle": "Ожидание", + "last_rtt": "Последний RTT", + "last_error": "Последняя ошибка", + "console_output": "Вывод консоли", + "streaming_responses": "Потоковая передача seq-ответов в реальном времени", + "no_pings_yet": "Пингов пока нет. Запустите проверку для сбора данных RTT.", + "invalid_hash": "Неверный хеш назначения!", + "timeout_must_be_number": "Тайм-аут должен быть числом!" + }, + "rncp": { + "file_transfer": "Передача файлов", + "title": "RNCP - Reticulum Network Copy", + "description": "Отправка и получение файлов через сеть Reticulum с использованием ресурсов RNS.", + "send_file": "Отправить файл", + "fetch_file": "Получить файл", + "listen": "Прослушивание", + "destination_hash": "Хеш назначения", + "file_path": "Путь к файлу", + "timeout_seconds": "Тайм-аут (секунды)", + "disable_compression": "Отключить сжатие", + "cancel": "Отмена", + "progress": "Прогресс", + "invalid_hash": "Неверный хеш назначения!", + "provide_file_path": "Пожалуйста, укажите путь к файлу!", + "file_sent_successfully": "Файл успешно отправлен. ID передачи: {id}", + "failed_to_send": "Не удалось отправить файл", + "remote_file_path": "Удаленный путь к файлу", + "save_path_optional": "Путь сохранения (опционально)", + "save_path_placeholder": "Оставьте пустым для текущей директории", + "allow_overwrite": "Разрешить перезапись", + "provide_remote_file_path": "Пожалуйста, укажите удаленный путь к файлу!", + "file_fetched_successfully": "Файл успешно получен. Сохранен в: {path}", + "failed_to_fetch": "Не удалось получить файл", + "allowed_hashes": "Разрешенные хеши идентичности (по одному в строке)", + "fetch_jail_path": "Путь изоляции (jail) для получения (опционально)", + "allow_fetch": "Разрешить получение", + "start_listening": "Начать прослушивание", + "stop_listening": "Остановить прослушивание", + "listening_on": "Прослушивание на:", + "provide_allowed_hash": "Пожалуйста, укажите хотя бы один разрешенный хеш идентичности!", + "failed_to_start_listener": "Не удалось запустить прослушиватель" + }, + "rnprobe": { + "network_diagnostics": "Диагностика сети", + "title": "RNProbe - Проверка назначения", + "description": "Проверка пунктов назначения с настраиваемым размером пакетов для тестирования связности и измерения RTT.", + "destination_hash": "Хеш назначения", + "full_destination_name": "Полное имя назначения", + "probe_size_bytes": "Размер пакета (байты)", + "number_of_probes": "Количество проверок", + "wait_between_probes": "Ожидание между проверками (секунды)", + "start_probe": "Начать проверку", + "stop": "Стоп", + "clear_results": "Очистить результаты", + "summary": "Итог", + "sent": "Отправлено", + "delivered": "Доставлено", + "timeouts": "Тайм-ауты", + "failed": "Ошибка", + "probe_results": "Результаты проверки", + "probe_responses_realtime": "Ответы проверки в реальном времени", + "no_probes_yet": "Проверок пока нет. Запустите проверку для тестирования связности.", + "probe_number": "Проверка №{number}", + "bytes": "байт", + "hops": "Хопы", + "rtt": "RTT", + "rssi": "RSSI", + "snr": "SNR", + "quality": "Качество", + "timeout": "Тайм-аут", + "invalid_hash": "Неверный хеш назначения!", + "provide_full_name": "Пожалуйста, укажите полное имя назначения!", + "failed_to_probe": "Не удалось проверить назначение" + }, + "rnstatus": { + "network_diagnostics": "Диагностика сети", + "title": "RNStatus - Статус сети", + "description": "Просмотр статистики интерфейсов и информации о статусе сети.", + "refresh": "Обновить", + "include_link_stats": "Включить статистику ссылок", + "sort_by": "Сортировать по:", + "none": "Нет", + "bitrate": "Битрейт", + "rx_bytes": "RX байт", + "tx_bytes": "TX байт", + "total_traffic": "Весь трафик", + "announces": "Анонсы", + "active_links": "Активные ссылки: {count}", + "no_interfaces_found": "Интерфейсы не найдены. Нажмите «Обновить» для загрузки статуса.", + "mode": "Режим", + "rx_packets": "RX пакетов", + "tx_packets": "TX пакетов", + "clients": "Клиенты", + "peers_reachable": "доступно", + "noise_floor": "Уровень шума", + "interference": "Помехи", + "cpu_load": "Загрузка ЦП", + "cpu_temp": "Темп. ЦП", + "memory_load": "Загрузка памяти", + "battery": "Батарея", + "network": "Сеть", + "incoming_announces": "Входящие анонсы", + "outgoing_announces": "Исходящие анонсы", + "airtime": "Эфирное время", + "channel_load": "Загрузка канала" + }, + "translator": { + "text_translation": "Перевод текста", + "title": "Переводчик", + "description": "Перевод текста с использованием LibreTranslate API или локального Argos Translate.", + "argos_translate": "Argos Translate", + "libretranslate": "LibreTranslate", + "api_server": "Сервер API LibreTranslate", + "api_server_description": "Введите базовый URL вашего сервера LibreTranslate (например, http://localhost:5000)", + "source_language": "Исходный язык", + "auto_detect": "Автоопределение", + "target_language": "Целевой язык", + "select_target_language": "Выберите целевой язык", + "argos_not_detected": "Argos Translate не обнаружен", + "argos_not_detected_desc": "Для использования локального перевода необходимо установить пакет Argos Translate одним из следующих способов:", + "method_pip_venv": "Способ 1: pip (venv)", + "method_pipx": "Способ 2: pipx", + "note_restart_required": "Примечание: После установки может потребоваться перезапуск приложения и установка языковых пакетов через CLI Argos Translate.", + "no_language_packages": "Языковые пакеты не обнаружены", + "no_language_packages_desc": "Argos Translate установлен, но языковые пакеты недоступны. Установите языковые пакеты с помощью одной из следующих команд:", + "install_all_languages": "Установить все языки", + "install_specific_pair": "Установить конкретную языковую пару (пример: с английского на русский)", + "after_install_note": "После установки языковых пакетов нажмите «Обновить языки», чтобы перезагрузить список доступных языков.", + "text_to_translate": "Текст для перевода", + "enter_text_placeholder": "Введите текст для перевода...", + "translate": "Перевести", + "swap": "Поменять местами", + "clear": "Очистить", + "translation": "Перевод", + "source": "Источник", + "detected": "Определено", + "available_languages": "Доступные языки", + "languages_loaded_from": "Языки загружаются из LibreTranslate API или пакетов Argos Translate.", + "refresh_languages": "Обновить языки", + "failed_to_load_languages": "Не удалось загрузить языки. Убедитесь, что LibreTranslate запущен или Argos Translate установлен.", + "copied_to_clipboard": "Скопировано в буфер обмена" + } +} diff --git a/meshchatx/src/frontend/main.js b/meshchatx/src/frontend/main.js new file mode 100644 index 0000000..db36da1 --- /dev/null +++ b/meshchatx/src/frontend/main.js @@ -0,0 +1,212 @@ +import axios from "axios"; +import { createApp, defineAsyncComponent } from "vue"; +import { createRouter, createWebHashHistory } from "vue-router"; +import { createI18n } from "vue-i18n"; +import vClickOutside from "click-outside-vue3"; +import "./style.css"; +import "./fonts/RobotoMonoNerdFont/font.css"; +import { ensureCodec2ScriptsLoaded } from "./js/Codec2Loader"; + +import App from "./components/App.vue"; +import en from "./locales/en.json"; +import de from "./locales/de.json"; +import ru from "./locales/ru.json"; + +// init i18n +const i18n = createI18n({ + legacy: false, + locale: "en", + fallbackLocale: "en", + messages: { + en, + de, + ru, + }, +}); + +// init vuetify +import { createVuetify } from "vuetify"; +const vuetify = createVuetify(); + +// provide axios globally +window.axios = axios; + +const router = createRouter({ + history: createWebHashHistory(), + routes: [ + { + name: "auth", + path: "/auth", + component: defineAsyncComponent(() => import("./components/auth/AuthPage.vue")), + }, + { + path: "/", + redirect: "/messages", + }, + { + name: "about", + path: "/about", + component: defineAsyncComponent(() => import("./components/about/AboutPage.vue")), + }, + { + name: "interfaces", + path: "/interfaces", + component: defineAsyncComponent(() => import("./components/interfaces/InterfacesPage.vue")), + }, + { + name: "interfaces.add", + path: "/interfaces/add", + component: defineAsyncComponent(() => import("./components/interfaces/AddInterfacePage.vue")), + }, + { + name: "interfaces.edit", + path: "/interfaces/edit", + component: defineAsyncComponent(() => import("./components/interfaces/AddInterfacePage.vue")), + props: { + interface_name: String, + }, + }, + { + name: "messages", + path: "/messages/:destinationHash?", + props: true, + component: defineAsyncComponent(() => import("./components/messages/MessagesPage.vue")), + }, + { + name: "map", + path: "/map", + component: defineAsyncComponent(() => import("./components/map/MapPage.vue")), + }, + { + name: "messages-popout", + path: "/popout/messages/:destinationHash?", + props: true, + meta: { popoutType: "conversation", isPopout: true }, + component: defineAsyncComponent(() => import("./components/messages/MessagesPage.vue")), + }, + { + name: "network-visualiser", + path: "/network-visualiser", + component: defineAsyncComponent(() => import("./components/network-visualiser/NetworkVisualiserPage.vue")), + }, + { + name: "nomadnetwork", + path: "/nomadnetwork/:destinationHash?", + props: true, + component: defineAsyncComponent(() => import("./components/nomadnetwork/NomadNetworkPage.vue")), + }, + { + name: "archives", + path: "/archives", + component: defineAsyncComponent(() => import("./components/archives/ArchivesPage.vue")), + }, + { + name: "nomadnetwork-popout", + path: "/popout/nomadnetwork/:destinationHash?", + props: true, + meta: { popoutType: "nomad", isPopout: true }, + component: defineAsyncComponent(() => import("./components/nomadnetwork/NomadNetworkPage.vue")), + }, + { + name: "propagation-nodes", + path: "/propagation-nodes", + component: defineAsyncComponent(() => import("./components/propagation-nodes/PropagationNodesPage.vue")), + }, + { + name: "ping", + path: "/ping", + component: defineAsyncComponent(() => import("./components/ping/PingPage.vue")), + }, + { + name: "rncp", + path: "/rncp", + component: defineAsyncComponent(() => import("./components/rncp/RNCPPage.vue")), + }, + { + name: "rnstatus", + path: "/rnstatus", + component: defineAsyncComponent(() => import("./components/rnstatus/RNStatusPage.vue")), + }, + { + name: "rnprobe", + path: "/rnprobe", + component: defineAsyncComponent(() => import("./components/rnprobe/RNProbePage.vue")), + }, + { + name: "translator", + path: "/translator", + component: defineAsyncComponent(() => import("./components/translator/TranslatorPage.vue")), + }, + { + name: "forwarder", + path: "/forwarder", + component: defineAsyncComponent(() => import("./components/forwarder/ForwarderPage.vue")), + }, + { + name: "profile.icon", + path: "/profile/icon", + component: defineAsyncComponent(() => import("./components/profile/ProfileIconPage.vue")), + }, + { + name: "settings", + path: "/settings", + component: defineAsyncComponent(() => import("./components/settings/SettingsPage.vue")), + }, + { + name: "blocked", + path: "/blocked", + component: defineAsyncComponent(() => import("./components/blocked/BlockedPage.vue")), + }, + { + name: "tools", + path: "/tools", + component: defineAsyncComponent(() => import("./components/tools/ToolsPage.vue")), + }, + { + name: "call", + path: "/call", + component: defineAsyncComponent(() => import("./components/call/CallPage.vue")), + }, + ], +}); + +router.beforeEach(async (to, from, next) => { + try { + const response = await window.axios.get("/api/v1/auth/status"); + const status = response.data; + + if (!status.auth_enabled) { + next(); + return; + } + + if (status.authenticated) { + if (to.name === "auth") { + next("/"); + } else { + next(); + } + return; + } + + if (to.name === "auth") { + next(); + return; + } + + next("/auth"); + } catch (e) { + if (e.response?.status === 401 || e.response?.status === 403) { + next("/auth"); + } else { + next(); + } + } +}); + +async function bootstrap() { + await ensureCodec2ScriptsLoaded(); + createApp(App).use(router).use(vuetify).use(i18n).use(vClickOutside).mount("#app"); +} + +bootstrap(); diff --git a/meshchatx/src/frontend/style.css b/meshchatx/src/frontend/style.css new file mode 100644 index 0000000..4905878 --- /dev/null +++ b/meshchatx/src/frontend/style.css @@ -0,0 +1,93 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + scrollbar-width: thin; + scrollbar-color: #94a3b8 #e2e8f0; +} + +*::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +*::-webkit-scrollbar-track { + background: #e2e8f0; +} + +*::-webkit-scrollbar-thumb { + background-color: #94a3b8; + border-radius: 999px; + border: 2px solid #e2e8f0; +} + +.dark * { + scrollbar-color: #52525b #18181b; +} + +.dark *::-webkit-scrollbar-track { + background: #18181b; +} + +.dark *::-webkit-scrollbar-thumb { + background-color: #52525b; + border-color: #18181b; +} + +.glass-card { + @apply bg-white/95 dark:bg-zinc-900/85 backdrop-blur border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-xl px-4 py-4; +} + +.input-field { + @apply bg-gray-50/90 dark:bg-zinc-900/80 border border-gray-200 dark:border-zinc-700 text-sm rounded-2xl focus:ring-2 focus:ring-blue-400 focus:border-blue-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 block w-full p-2.5 text-gray-900 dark:text-gray-100 transition; +} + +.primary-chip { + @apply inline-flex items-center gap-x-2 rounded-full bg-blue-600/90 px-3 py-1.5 text-xs font-semibold text-white shadow hover:bg-blue-500 transition; +} + +.secondary-chip { + @apply inline-flex items-center gap-x-2 rounded-full border border-gray-300 dark:border-zinc-700 px-3 py-1.5 text-xs font-semibold text-gray-700 dark:text-gray-100 bg-white/80 dark:bg-zinc-900/70 hover:border-blue-400 dark:hover:border-blue-500 transition; +} + +.glass-label { + @apply mb-1 text-sm font-semibold text-gray-800 dark:text-gray-200; +} + +.monospace-field { + @apply font-mono tracking-tight text-gray-900 dark:text-white; +} + +.metric-row { + @apply grid gap-3 sm:grid-cols-2 text-gray-800 dark:text-gray-100; +} + +.metric-value { + @apply text-lg font-semibold text-gray-900 dark:text-white; +} + +.address-card { + @apply relative border border-gray-200 dark:border-zinc-800 rounded-2xl bg-white/80 dark:bg-zinc-900/70 p-4 space-y-2; +} +.address-card__label { + @apply text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400; +} +.address-card__value { + @apply text-sm text-gray-900 dark:text-white break-words pr-16; +} +.address-card__action { + @apply absolute top-3 right-3 inline-flex items-center gap-1 rounded-full border border-gray-200 dark:border-zinc-700 px-3 py-1 text-xs font-semibold text-gray-700 dark:text-gray-100 bg-white/70 dark:bg-zinc-900/60 hover:border-blue-400 dark:hover:border-blue-500 transition; +} + +.file-input { + @apply block w-full text-xs cursor-pointer rounded-lg border border-gray-300 dark:border-zinc-700 bg-gray-50 dark:bg-zinc-900 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-400 dark:focus:ring-blue-500; +} + +.file-input::file-selector-button { + @apply mr-2 border-0 rounded-md bg-blue-600 text-white px-3 py-1 text-xs font-semibold cursor-pointer hover:bg-blue-500 transition; +} + +.dark .file-input::file-selector-button { + @apply bg-blue-500 hover:bg-blue-400 text-white; +} diff --git a/meshchatx/src/version.py b/meshchatx/src/version.py new file mode 100644 index 0000000..b5a13a7 --- /dev/null +++ b/meshchatx/src/version.py @@ -0,0 +1,6 @@ +""" +Auto-generated helper so Python tooling and the Electron build +share the same version string. +""" + +__version__ = '2.50.0' diff --git a/package.json b/package.json new file mode 100644 index 0000000..b768917 --- /dev/null +++ b/package.json @@ -0,0 +1,155 @@ +{ + "name": "reticulum-meshchatx", + "version": "2.50.0", + "description": "A simple mesh network communications app powered by the Reticulum Network Stack", + "author": "Sudo-Ivan", + "main": "electron/main.js", + "scripts": { + "dev": "vite dev", + "watch": "pnpm run build-frontend -- --watch", + "build-frontend": "vite build", + "build-backend": "node scripts/build-backend.js", + "build": "pnpm run build-frontend && pnpm run build-backend", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", + "electron-postinstall": "electron-builder install-app-deps", + "electron": "pnpm run electron-postinstall && pnpm run build && electron .", + "dist": "pnpm run electron-postinstall && pnpm run build && electron-builder --publish=never", + "dist-prebuilt": "pnpm run electron-postinstall && pnpm run build-backend && electron-builder --publish=never", + "dist:mac-arm64": "pnpm run electron-postinstall && pnpm run build && electron-builder --mac --arm64 --publish=never", + "dist:mac-universal": "pnpm run electron-postinstall && pnpm run build && electron-builder --mac --universal --publish=never" + }, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "packageManager": "pnpm@10.0.0", + "devDependencies": { + "@eslint/js": "^9.39.2", + "@rushstack/eslint-patch": "^1.15.0", + "@vue/eslint-config-prettier": "^10.2.0", + "electron": "^39.2.7", + "electron-builder": "^24.13.3", + "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-security": "^3.0.1", + "eslint-plugin-vue": "^10.6.2", + "globals": "^16.5.0", + "prettier": "^3.7.4", + "terser": "^5.44.1" + }, + "build": { + "appId": "com.sudoivan.reticulummeshchat", + "productName": "Reticulum MeshChatX", + "asar": true, + "asarUnpack": [ + "build/exe/**/*" + ], + "files": [ + "electron/**/*" + ], + "directories": { + "buildResources": "electron/build" + }, + "mac": { + "target": { + "target": "dmg", + "arch": [ + "universal" + ] + }, + "identity": null, + "artifactName": "ReticulumMeshChat-v${version}-mac-${arch}.${ext}", + "x64ArchFiles": "Contents/Resources/app/electron/build/exe/**", + "extendInfo": { + "NSMicrophoneUsageDescription": "Microphone access is only needed for Audio Calls", + "com.apple.security.device.audio-input": true + }, + "extraFiles": [ + { + "from": "build/exe", + "to": "Resources/app/electron/build/exe", + "filter": [ + "**/*" + ] + } + ] + }, + "win": { + "artifactName": "ReticulumMeshChat-v${version}-${os}.${ext}", + "target": [ + { + "target": "portable" + }, + { + "target": "nsis" + } + ], + "extraFiles": [ + { + "from": "build/exe", + "to": "Resources/app/electron/build/exe", + "filter": [ + "**/*" + ] + } + ] + }, + "linux": { + "artifactName": "ReticulumMeshChat-v${version}-${os}.${ext}", + "target": [ + "AppImage", + "deb" + ], + "maintainer": "Sudo-Ivan", + "category": "Network", + "extraFiles": [ + { + "from": "build/exe", + "to": "resources/app/electron/build/exe", + "filter": [ + "**/*" + ] + } + ] + }, + "dmg": { + "writeUpdateInfo": false + }, + "portable": { + "artifactName": "ReticulumMeshChat-v${version}-${os}-portable.${ext}" + }, + "nsis": { + "artifactName": "ReticulumMeshChat-v${version}-${os}-installer.${ext}", + "oneClick": false, + "allowToChangeInstallationDirectory": true + } + }, + "dependencies": { + "@mdi/js": "^7.4.47", + "@tailwindcss/forms": "^0.5.11", + "@vitejs/plugin-vue": "^5.2.4", + "autoprefixer": "^10.4.23", + "axios": "^1.13.2", + "click-outside-vue3": "^4.0.1", + "compressorjs": "^1.2.1", + "dayjs": "^1.11.19", + "electron-prompt": "^1.7.0", + "micron-parser": "^1.0.2", + "mitt": "^3.0.1", + "ol": "^10.7.0", + "postcss": "^8.5.6", + "protobufjs": "^7.5.4", + "tailwindcss": "^3.4.19", + "vis-data": "^7.1.10", + "vis-network": "^9.1.13", + "vite": "^6.4.1", + "vite-plugin-vuetify": "^2.1.2", + "vue": "^3.5.26", + "vue-i18n": "^11.2.8", + "vue-router": "^4.6.4", + "vuetify": "^3.11.6" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..7b56fcd --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4814 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@mdi/js': + specifier: ^7.4.47 + version: 7.4.47 + '@tailwindcss/forms': + specifier: ^0.5.11 + version: 0.5.11(tailwindcss@3.4.19) + '@vitejs/plugin-vue': + specifier: ^5.2.4 + version: 5.2.4(vite@6.4.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1))(vue@3.5.26(typescript@5.9.3)) + autoprefixer: + specifier: ^10.4.23 + version: 10.4.23(postcss@8.5.6) + axios: + specifier: ^1.13.2 + version: 1.13.2 + click-outside-vue3: + specifier: ^4.0.1 + version: 4.0.1 + compressorjs: + specifier: ^1.2.1 + version: 1.2.1 + dayjs: + specifier: ^1.11.19 + version: 1.11.19 + electron-prompt: + specifier: ^1.7.0 + version: 1.7.0 + micron-parser: + specifier: ^1.0.2 + version: 1.0.2 + mitt: + specifier: ^3.0.1 + version: 3.0.1 + ol: + specifier: ^10.7.0 + version: 10.7.0 + postcss: + specifier: ^8.5.6 + version: 8.5.6 + protobufjs: + specifier: ^7.5.4 + version: 7.5.4 + tailwindcss: + specifier: ^3.4.19 + version: 3.4.19 + vis-data: + specifier: ^7.1.10 + version: 7.1.10(uuid@11.1.0)(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@2.0.0)) + vis-network: + specifier: ^9.1.13 + version: 9.1.13(@egjs/hammerjs@2.0.17)(component-emitter@2.0.0)(keycharm@0.4.0)(uuid@11.1.0)(vis-data@7.1.10(uuid@11.1.0)(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@2.0.0)))(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@2.0.0)) + vite: + specifier: ^6.4.1 + version: 6.4.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1) + vite-plugin-vuetify: + specifier: ^2.1.2 + version: 2.1.2(vite@6.4.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1))(vue@3.5.26(typescript@5.9.3))(vuetify@3.11.6) + vue: + specifier: ^3.5.26 + version: 3.5.26(typescript@5.9.3) + vue-i18n: + specifier: ^11.2.8 + version: 11.2.8(vue@3.5.26(typescript@5.9.3)) + vue-router: + specifier: ^4.6.4 + version: 4.6.4(vue@3.5.26(typescript@5.9.3)) + vuetify: + specifier: ^3.11.6 + version: 3.11.6(typescript@5.9.3)(vite-plugin-vuetify@2.1.2)(vue@3.5.26(typescript@5.9.3)) + devDependencies: + '@eslint/js': + specifier: ^9.39.2 + version: 9.39.2 + '@rushstack/eslint-patch': + specifier: ^1.15.0 + version: 1.15.0 + '@vue/eslint-config-prettier': + specifier: ^10.2.0 + version: 10.2.0(eslint@9.39.2(jiti@1.21.7))(prettier@3.7.4) + electron: + specifier: ^39.2.7 + version: 39.2.7 + electron-builder: + specifier: ^24.13.3 + version: 24.13.3(electron-builder-squirrel-windows@24.13.3) + eslint: + specifier: ^9.39.2 + version: 9.39.2(jiti@1.21.7) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-prettier: + specifier: ^5.5.4 + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7))(prettier@3.7.4) + eslint-plugin-security: + specifier: ^3.0.1 + version: 3.0.1 + eslint-plugin-vue: + specifier: ^10.6.2 + version: 10.6.2(eslint@9.39.2(jiti@1.21.7))(vue-eslint-parser@10.2.0(eslint@9.39.2(jiti@1.21.7))) + globals: + specifier: ^16.5.0 + version: 16.5.0 + prettier: + specifier: ^3.7.4 + version: 3.7.4 + terser: + specifier: ^5.44.1 + version: 5.44.1 + +packages: + + 7zip-bin@5.2.0: + resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@develar/schema-utils@2.6.5': + resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} + engines: {node: '>= 8.9.0'} + + '@egjs/hammerjs@2.0.17': + resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} + engines: {node: '>=0.8.0'} + + '@electron/asar@3.4.1': + resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} + engines: {node: '>=10.12.0'} + hasBin: true + + '@electron/get@2.0.3': + resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} + engines: {node: '>=12'} + + '@electron/notarize@2.2.1': + resolution: {integrity: sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==} + engines: {node: '>= 10.0.0'} + + '@electron/osx-sign@1.0.5': + resolution: {integrity: sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==} + engines: {node: '>=12.0.0'} + hasBin: true + + '@electron/universal@1.5.1': + resolution: {integrity: sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==} + engines: {node: '>=8.6'} + + '@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] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@intlify/core-base@11.2.8': + resolution: {integrity: sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@11.2.8': + resolution: {integrity: sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==} + engines: {node: '>= 16'} + + '@intlify/shared@11.2.8': + resolution: {integrity: sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw==} + engines: {node: '>= 16'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@malept/cross-spawn-promise@1.1.1': + resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==} + engines: {node: '>= 10'} + + '@malept/flatpak-bundler@0.4.0': + resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} + engines: {node: '>= 10.0.0'} + + '@mdi/js@7.4.47': + resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==} + + '@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'} + + '@petamoriken/float16@3.9.3': + resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@rollup/rollup-android-arm-eabi@4.54.0': + resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.54.0': + resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.54.0': + resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.54.0': + resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.54.0': + resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.54.0': + resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.54.0': + resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.54.0': + resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.54.0': + resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.54.0': + resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.54.0': + resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.54.0': + resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.54.0': + resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.54.0': + resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.54.0': + resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.54.0': + resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.54.0': + resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.54.0': + resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==} + cpu: [x64] + os: [win32] + + '@rushstack/eslint-patch@1.15.0': + resolution: {integrity: sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@tailwindcss/forms@0.5.11': + resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + + '@types/hammerjs@2.0.46': + resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} + + '@types/http-cache-semantics@4.0.4': + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@22.19.3': + resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} + + '@types/node@25.0.3': + resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} + + '@types/plist@3.0.5': + resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + + '@types/rbush@4.0.0': + resolution: {integrity: sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/verror@1.10.11': + resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.26': + resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==} + + '@vue/compiler-dom@3.5.26': + resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==} + + '@vue/compiler-sfc@3.5.26': + resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==} + + '@vue/compiler-ssr@3.5.26': + resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/eslint-config-prettier@10.2.0': + resolution: {integrity: sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==} + peerDependencies: + eslint: '>= 8.21.0' + prettier: '>= 3.0.0' + + '@vue/reactivity@3.5.26': + resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==} + + '@vue/runtime-core@3.5.26': + resolution: {integrity: sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==} + + '@vue/runtime-dom@3.5.26': + resolution: {integrity: sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==} + + '@vue/server-renderer@3.5.26': + resolution: {integrity: sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==} + peerDependencies: + vue: 3.5.26 + + '@vue/shared@3.5.26': + resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==} + + '@vuetify/loader-shared@2.1.1': + resolution: {integrity: sha512-jSZTzTYaoiv8iwonFCVZQ0YYX/M+Uyl4ng+C4egMJT0Hcmh9gIxJL89qfZICDeo3g0IhqrvipW2FFKKRDMtVcA==} + peerDependencies: + vue: ^3.0.0 + vuetify: ^3.0.0 + + '@xmldom/xmldom@0.8.11': + resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} + engines: {node: '>=10.0.0'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + 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'} + + app-builder-bin@4.0.0: + resolution: {integrity: sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==} + + app-builder-lib@24.13.3: + resolution: {integrity: sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==} + engines: {node: '>=14.0.0'} + peerDependencies: + dmg-builder: 24.13.3 + electron-builder-squirrel-windows: 24.13.3 + + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + autoprefixer@10.4.23: + resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird-lst@1.0.9: + resolution: {integrity: sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + blueimp-canvas-to-blob@3.29.0: + resolution: {integrity: sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal@1.0.1: + resolution: {integrity: sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==} + engines: {node: '>=0.4'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + builder-util-runtime@9.2.4: + resolution: {integrity: sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==} + engines: {node: '>=12.0.0'} + + builder-util@24.13.1: + resolution: {integrity: sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001762: + resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chromium-pickle-js@0.2.0: + resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + click-outside-vue3@4.0.1: + resolution: {integrity: sha512-sbplNecrup5oGqA3o4bo8XmvHRT6q9fvw21Z67aDbTqB9M6LF7CuYLTlLvNtOgKU6W3zst5H5zJuEh4auqA34g==} + engines: {node: '>=6'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + compare-version@0.1.2: + resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} + engines: {node: '>=0.10.0'} + + component-emitter@2.0.0: + resolution: {integrity: sha512-4m5s3Me2xxlVKG9PkZpQqHQR7bgpnN7joDMJ4yvVkVXngjoITG76IaZmzmywSeRTeTpc6N6r3H3+KyUurV8OYw==} + engines: {node: '>=18'} + + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + + compressorjs@1.2.1: + resolution: {integrity: sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + config-file-ts@0.2.6: + resolution: {integrity: sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==} + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + + crc@3.8.0: + resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + 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==} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dir-compare@3.3.0: + resolution: {integrity: sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dmg-builder@24.13.3: + resolution: {integrity: sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==} + + dmg-license@1.0.11: + resolution: {integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==} + engines: {node: '>=8'} + os: [darwin] + hasBin: true + + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + + dotenv-expand@5.1.0: + resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} + + dotenv@9.0.2: + resolution: {integrity: sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==} + engines: {node: '>=10'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + earcut@3.0.2: + resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-builder-squirrel-windows@24.13.3: + resolution: {integrity: sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==} + + electron-builder@24.13.3: + resolution: {integrity: sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==} + engines: {node: '>=14.0.0'} + hasBin: true + + electron-prompt@1.7.0: + resolution: {integrity: sha512-IfqJYEgcRO6NuyPROo8AtdkAiZ6N9I1lQEf4dJAkPuhV5YgOHdmLqZJf6OXumZJfzrjpzCM5jHeYOrhGdgbnEA==} + + electron-publish@24.13.1: + resolution: {integrity: sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==} + + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + + electron@39.2.7: + resolution: {integrity: sha512-KU0uFS6LSTh4aOIC3miolcbizOFP7N1M46VTYVfqIgFiuA2ilfNaOHLDS9tCMvwwHRowAsvqBrh9NgMXcTOHCQ==} + engines: {node: '>= 12.20.55'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + entities@7.0.0: + resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + 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'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.4: + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-security@3.0.1: + resolution: {integrity: sha512-XjVGBhtDZJfyuhIxnQ/WMm385RbX3DBu7H1J7HNNhmB2tnGxMeqVSnYv79oAj992ayvIBZghsymwkYFS6cGH4Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-plugin-vue@10.6.2: + resolution: {integrity: sha512-nA5yUs/B1KmKzvC42fyD0+l9Yd+LtEpVhWRbXuDj0e+ZURcTtyRbMDWUeJmTAh2wC6jC83raS63anNM2YT3NPw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 + vue-eslint-parser: ^10.0.0 + peerDependenciesMeta: + '@stylistic/eslint-plugin': + optional: true + '@typescript-eslint/parser': + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + extsprintf@1.4.1: + resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} + engines: {'0': node >=0.6.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + 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==} + + geotiff@2.1.3: + resolution: {integrity: sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==} + engines: {node: '>=10.19'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + 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'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + iconv-corefoundation@1.1.7: + resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} + engines: {node: ^8.11.2 || >=10} + os: [darwin] + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-blob@2.1.0: + resolution: {integrity: sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==} + engines: {node: '>=6'} + + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isbinaryfile@5.0.7: + resolution: {integrity: sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==} + engines: {node: '>= 18.0.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + keycharm@0.4.0: + resolution: {integrity: sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + lazy-val@1.0.5: + resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + lerc@3.0.0: + resolution: {integrity: sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + 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==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + micron-parser@1.0.2: + resolution: {integrity: sha512-lYrEolylOUXeSISYrPRW/ZZAH1dpZRyTJ0VzQIA4cWJy0yNCXUUs+ujuAwV2OYlAPH8tCE1Z22+zg04Ilp/JWg==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + 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.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-addon-api@1.7.2: + resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + 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'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + ol@10.7.0: + resolution: {integrity: sha512-122U5gamPqNgLpLOkogFJhgpywvd/5en2kETIDW+Ubfi9lPnZ0G9HWRdG+CX0oP8od2d6u6ky3eewIYYlrVczw==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-headers@2.0.6: + resolution: {integrity: sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pbf@4.0.1: + resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==} + hasBin: true + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@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'} + + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + + 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.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + engines: {node: '>=14'} + hasBin: true + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + + protocol-buffers-schema@3.6.0: + resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + quick-lru@6.1.2: + resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==} + engines: {node: '>=12'} + + quickselect@3.0.0: + resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} + + rbush@4.0.1: + resolution: {integrity: sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + read-config-file@6.3.2: + resolution: {integrity: sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==} + engines: {node: '>=12.0.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-protobuf-schema@2.1.0: + resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + + rollup@4.54.0: + resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex@2.1.1: + resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sanitize-filename@1.6.3: + resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} + + sax@1.4.3: + resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} + + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + stat-mode@1.0.0: + resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} + engines: {node: '>= 6'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + temp-file@3.4.0: + resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + + terser@5.44.1: + resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} + engines: {node: '>=10'} + 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==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + upath@2.0.1: + resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} + engines: {node: '>=4'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + verror@1.10.1: + resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} + engines: {node: '>=0.6.0'} + + vis-data@7.1.10: + resolution: {integrity: sha512-23juM9tdCaHTX5vyIQ7XBzsfZU0Hny+gSTwniLrfFcmw9DOm7pi3+h9iEBsoZMp5rX6KNqWwc1MF0fkAmWVuoQ==} + peerDependencies: + uuid: ^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + vis-util: ^5.0.1 + + vis-network@9.1.13: + resolution: {integrity: sha512-HLeHd5KZS92qzO1kC59qMh1/FWAZxMUEwUWBwDMoj6RKj/Ajkrgy/heEYo0Zc8SZNQ2J+u6omvK2+a28GX1QuQ==} + peerDependencies: + '@egjs/hammerjs': ^2.0.0 + component-emitter: ^1.3.0 || ^2.0.0 + keycharm: ^0.2.0 || ^0.3.0 || ^0.4.0 + uuid: ^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + vis-data: ^6.3.0 || ^7.0.0 + vis-util: ^5.0.1 + + vis-util@5.0.7: + resolution: {integrity: sha512-E3L03G3+trvc/X4LXvBfih3YIHcKS2WrP0XTdZefr6W6Qi/2nNCqZfe4JFfJU6DcQLm6Gxqj2Pfl+02859oL5A==} + engines: {node: '>=8'} + peerDependencies: + '@egjs/hammerjs': ^2.0.0 + component-emitter: ^1.3.0 || ^2.0.0 + + vite-plugin-vuetify@2.1.2: + resolution: {integrity: sha512-I/wd6QS+DO6lHmuGoi1UTyvvBTQ2KDzQZ9oowJQEJ6OcjWfJnscYXx2ptm6S7fJSASuZT8jGRBL3LV4oS3LpaA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: '>=5' + vue: ^3.0.0 + vuetify: ^3.0.0 + + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + 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 + + vue-eslint-parser@10.2.0: + resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + vue-i18n@11.2.8: + resolution: {integrity: sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue@3.5.26: + resolution: {integrity: sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + vuetify@3.11.6: + resolution: {integrity: sha512-vaWvEpDSeldRoib1tCKNVBp60paTD2/n0a0TmrVLF9BN4CJJn6/A4VKG2Sg+DE8Yc+SNOtFtipChxSlQxjcUvw==} + peerDependencies: + typescript: '>=4.7' + vite-plugin-vuetify: '>=2.1.0' + vue: ^3.5.0 + webpack-plugin-vuetify: '>=3.1.0' + peerDependenciesMeta: + typescript: + optional: true + vite-plugin-vuetify: + optional: true + webpack-plugin-vuetify: + optional: true + + web-worker@1.5.0: + resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xml-utils@1.10.2: + resolution: {integrity: sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + + zstddec@0.1.0: + resolution: {integrity: sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==} + +snapshots: + + 7zip-bin@5.2.0: {} + + '@alloc/quick-lru@5.2.0': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@develar/schema-utils@2.6.5': + dependencies: + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + '@egjs/hammerjs@2.0.17': + dependencies: + '@types/hammerjs': 2.0.46 + + '@electron/asar@3.4.1': + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.2 + + '@electron/get@2.0.3': + dependencies: + debug: 4.4.3 + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@electron/notarize@2.2.1': + dependencies: + debug: 4.4.3 + fs-extra: 9.1.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@electron/osx-sign@1.0.5': + dependencies: + compare-version: 0.1.2 + debug: 4.4.3 + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/universal@1.5.1': + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 1.1.1 + debug: 4.4.3 + dir-compare: 3.3.0 + fs-extra: 9.1.0 + minimatch: 3.1.2 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@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 + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@1.21.7))': + dependencies: + eslint: 9.39.2(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.2': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@intlify/core-base@11.2.8': + dependencies: + '@intlify/message-compiler': 11.2.8 + '@intlify/shared': 11.2.8 + + '@intlify/message-compiler@11.2.8': + dependencies: + '@intlify/shared': 11.2.8 + source-map-js: 1.2.1 + + '@intlify/shared@11.2.8': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@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 + + '@malept/cross-spawn-promise@1.1.1': + dependencies: + cross-spawn: 7.0.6 + + '@malept/flatpak-bundler@0.4.0': + dependencies: + debug: 4.4.3 + fs-extra: 9.1.0 + lodash: 4.17.21 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - supports-color + + '@mdi/js@7.4.47': {} + + '@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 + + '@petamoriken/float16@3.9.3': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.2.9': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@rollup/rollup-android-arm-eabi@4.54.0': + optional: true + + '@rollup/rollup-android-arm64@4.54.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.54.0': + optional: true + + '@rollup/rollup-darwin-x64@4.54.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.54.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.54.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.54.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.54.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.54.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.54.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.54.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.54.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.54.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.54.0': + optional: true + + '@rushstack/eslint-patch@1.15.0': {} + + '@sindresorhus/is@4.6.0': {} + + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + + '@tailwindcss/forms@0.5.11(tailwindcss@3.4.19)': + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 3.4.19 + + '@tootallnate/once@2.0.0': {} + + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.0.4 + '@types/keyv': 3.1.4 + '@types/node': 22.19.3 + '@types/responselike': 1.0.3 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree@1.0.8': {} + + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 25.0.3 + + '@types/hammerjs@2.0.46': {} + + '@types/http-cache-semantics@4.0.4': {} + + '@types/json-schema@7.0.15': {} + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 22.19.3 + + '@types/ms@2.1.0': {} + + '@types/node@22.19.3': + dependencies: + undici-types: 6.21.0 + + '@types/node@25.0.3': + dependencies: + undici-types: 7.16.0 + + '@types/plist@3.0.5': + dependencies: + '@types/node': 25.0.3 + xmlbuilder: 15.1.1 + optional: true + + '@types/rbush@4.0.0': {} + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 22.19.3 + + '@types/trusted-types@2.0.7': + optional: true + + '@types/verror@1.10.11': + optional: true + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.19.3 + optional: true + + '@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1))(vue@3.5.26(typescript@5.9.3))': + dependencies: + vite: 6.4.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1) + vue: 3.5.26(typescript@5.9.3) + + '@vue/compiler-core@3.5.26': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.26 + entities: 7.0.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.26': + dependencies: + '@vue/compiler-core': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/compiler-sfc@3.5.26': + dependencies: + '@babel/parser': 7.28.5 + '@vue/compiler-core': 3.5.26 + '@vue/compiler-dom': 3.5.26 + '@vue/compiler-ssr': 3.5.26 + '@vue/shared': 3.5.26 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.26': + dependencies: + '@vue/compiler-dom': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/devtools-api@6.6.4': {} + + '@vue/eslint-config-prettier@10.2.0(eslint@9.39.2(jiti@1.21.7))(prettier@3.7.4)': + dependencies: + eslint: 9.39.2(jiti@1.21.7) + eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-prettier: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7))(prettier@3.7.4) + prettier: 3.7.4 + transitivePeerDependencies: + - '@types/eslint' + + '@vue/reactivity@3.5.26': + dependencies: + '@vue/shared': 3.5.26 + + '@vue/runtime-core@3.5.26': + dependencies: + '@vue/reactivity': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/runtime-dom@3.5.26': + dependencies: + '@vue/reactivity': 3.5.26 + '@vue/runtime-core': 3.5.26 + '@vue/shared': 3.5.26 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.26(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.26 + '@vue/shared': 3.5.26 + vue: 3.5.26(typescript@5.9.3) + + '@vue/shared@3.5.26': {} + + '@vuetify/loader-shared@2.1.1(vue@3.5.26(typescript@5.9.3))(vuetify@3.11.6)': + dependencies: + upath: 2.0.1 + vue: 3.5.26(typescript@5.9.3) + vuetify: 3.11.6(typescript@5.9.3)(vite-plugin-vuetify@2.1.2)(vue@3.5.26(typescript@5.9.3)) + + '@xmldom/xmldom@0.8.11': {} + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + app-builder-bin@4.0.0: {} + + app-builder-lib@24.13.3(dmg-builder@24.13.3)(electron-builder-squirrel-windows@24.13.3): + dependencies: + '@develar/schema-utils': 2.6.5 + '@electron/notarize': 2.2.1 + '@electron/osx-sign': 1.0.5 + '@electron/universal': 1.5.1 + '@malept/flatpak-bundler': 0.4.0 + '@types/fs-extra': 9.0.13 + async-exit-hook: 2.0.1 + bluebird-lst: 1.0.9 + builder-util: 24.13.1 + builder-util-runtime: 9.2.4 + chromium-pickle-js: 0.2.0 + debug: 4.4.3 + dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3) + ejs: 3.1.10 + electron-builder-squirrel-windows: 24.13.3(dmg-builder@24.13.3) + electron-publish: 24.13.1 + form-data: 4.0.5 + fs-extra: 10.1.0 + hosted-git-info: 4.1.0 + is-ci: 3.0.1 + isbinaryfile: 5.0.7 + js-yaml: 4.1.1 + lazy-val: 1.0.5 + minimatch: 5.1.6 + read-config-file: 6.3.2 + sanitize-filename: 1.6.3 + semver: 7.7.3 + tar: 6.2.1 + temp-file: 3.4.0 + transitivePeerDependencies: + - supports-color + + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + assert-plus@1.0.0: + optional: true + + astral-regex@2.0.0: + optional: true + + async-exit-hook@2.0.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + autoprefixer@10.4.23(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001762 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.9.11: {} + + binary-extensions@2.3.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bluebird-lst@1.0.9: + dependencies: + bluebird: 3.7.2 + + bluebird@3.7.2: {} + + blueimp-canvas-to-blob@3.29.0: {} + + boolbase@1.0.0: {} + + boolean@3.2.0: + optional: true + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001762 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + buffer-crc32@0.2.13: {} + + buffer-equal@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builder-util-runtime@9.2.4: + dependencies: + debug: 4.4.3 + sax: 1.4.3 + transitivePeerDependencies: + - supports-color + + builder-util@24.13.1: + dependencies: + 7zip-bin: 5.2.0 + '@types/debug': 4.1.12 + app-builder-bin: 4.0.0 + bluebird-lst: 1.0.9 + builder-util-runtime: 9.2.4 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + fs-extra: 10.1.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-ci: 3.0.1 + js-yaml: 4.1.1 + source-map-support: 0.5.21 + stat-mode: 1.0.0 + temp-file: 3.4.0 + transitivePeerDependencies: + - supports-color + + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001762: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + 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 + + chownr@2.0.0: {} + + chromium-pickle-js@0.2.0: {} + + ci-info@3.9.0: {} + + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + optional: true + + click-outside-vue3@4.0.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@2.20.3: {} + + commander@4.1.1: {} + + commander@5.1.0: {} + + compare-version@0.1.2: {} + + component-emitter@2.0.0: {} + + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + compressorjs@1.2.1: + dependencies: + blueimp-canvas-to-blob: 3.29.0 + is-blob: 2.1.0 + + concat-map@0.0.1: {} + + config-file-ts@0.2.6: + dependencies: + glob: 10.5.0 + typescript: 5.9.3 + + core-util-is@1.0.2: + optional: true + + core-util-is@1.0.3: {} + + crc-32@1.2.2: {} + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + + crc@3.8.0: + dependencies: + buffer: 5.7.1 + optional: true + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + dayjs@1.11.19: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-is@0.1.4: {} + + defer-to-connect@2.0.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + optional: true + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + optional: true + + delayed-stream@1.0.0: {} + + detect-node@2.1.0: + optional: true + + didyoumean@1.2.2: {} + + dir-compare@3.3.0: + dependencies: + buffer-equal: 1.0.1 + minimatch: 3.1.2 + + dlv@1.1.3: {} + + dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3): + dependencies: + app-builder-lib: 24.13.3(dmg-builder@24.13.3)(electron-builder-squirrel-windows@24.13.3) + builder-util: 24.13.1 + builder-util-runtime: 9.2.4 + fs-extra: 10.1.0 + iconv-lite: 0.6.3 + js-yaml: 4.1.1 + optionalDependencies: + dmg-license: 1.0.11 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + + dmg-license@1.0.11: + dependencies: + '@types/plist': 3.0.5 + '@types/verror': 1.10.11 + ajv: 6.12.6 + crc: 3.8.0 + iconv-corefoundation: 1.1.7 + plist: 3.1.0 + smart-buffer: 4.2.0 + verror: 1.10.1 + optional: true + + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dotenv-expand@5.1.0: {} + + dotenv@9.0.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + earcut@3.0.2: {} + + eastasianwidth@0.2.0: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3): + dependencies: + app-builder-lib: 24.13.3(dmg-builder@24.13.3)(electron-builder-squirrel-windows@24.13.3) + archiver: 5.3.2 + builder-util: 24.13.1 + fs-extra: 10.1.0 + transitivePeerDependencies: + - dmg-builder + - supports-color + + electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3): + dependencies: + app-builder-lib: 24.13.3(dmg-builder@24.13.3)(electron-builder-squirrel-windows@24.13.3) + builder-util: 24.13.1 + builder-util-runtime: 9.2.4 + chalk: 4.1.2 + dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3) + fs-extra: 10.1.0 + is-ci: 3.0.1 + lazy-val: 1.0.5 + read-config-file: 6.3.2 + simple-update-notifier: 2.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + + electron-prompt@1.7.0: {} + + electron-publish@24.13.1: + dependencies: + '@types/fs-extra': 9.0.13 + builder-util: 24.13.1 + builder-util-runtime: 9.2.4 + chalk: 4.1.2 + fs-extra: 10.1.0 + lazy-val: 1.0.5 + mime: 2.6.0 + transitivePeerDependencies: + - supports-color + + electron-to-chromium@1.5.267: {} + + electron@39.2.7: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 22.19.3 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + entities@7.0.0: {} + + env-paths@2.2.1: {} + + err-code@2.0.3: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es6-error@4.1.1: + optional: true + + 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: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@1.21.7)): + dependencies: + eslint: 9.39.2(jiti@1.21.7) + + eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7))(prettier@3.7.4): + dependencies: + eslint: 9.39.2(jiti@1.21.7) + prettier: 3.7.4 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.11 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@1.21.7)) + + eslint-plugin-security@3.0.1: + dependencies: + safe-regex: 2.1.1 + + eslint-plugin-vue@10.6.2(eslint@9.39.2(jiti@1.21.7))(vue-eslint-parser@10.2.0(eslint@9.39.2(jiti@1.21.7))): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7)) + eslint: 9.39.2(jiti@1.21.7) + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 7.1.1 + semver: 7.7.3 + vue-eslint-parser: 10.2.0(eslint@9.39.2(jiti@1.21.7)) + xml-name-validator: 4.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.2(jiti@1.21.7): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 1.21.7 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + extsprintf@1.4.1: + optional: true + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.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 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fraction.js@5.3.4: {} + + fs-constants@1.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + geotiff@2.1.3: + dependencies: + '@petamoriken/float16': 3.9.3 + lerc: 3.0.0 + pako: 2.1.0 + parse-headers: 2.0.6 + quick-lru: 6.1.2 + web-worker: 1.5.0 + xml-utils: 1.10.2 + zstddec: 0.1.0 + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.7.3 + serialize-error: 7.0.1 + optional: true + + globals@14.0.0: {} + + globals@16.5.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + optional: true + + gopd@1.2.0: {} + + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + optional: true + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + http-cache-semantics@4.2.0: {} + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-corefoundation@1.1.7: + dependencies: + cli-truncate: 2.1.0 + node-addon-api: 1.7.2 + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-blob@2.1.0: {} + + is-ci@3.0.1: + dependencies: + ci-info: 3.9.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isarray@1.0.0: {} + + isbinaryfile@4.0.10: {} + + isbinaryfile@5.0.7: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.4 + picocolors: 1.1.1 + + jiti@1.21.7: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-stringify-safe@5.0.1: + optional: true + + json5@2.2.3: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keycharm@0.4.0: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + lazy-val@1.0.5: {} + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + lerc@3.0.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.defaults@4.2.0: {} + + lodash.difference@4.5.0: {} + + lodash.flatten@4.4.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.merge@4.6.2: {} + + lodash.union@4.6.0: {} + + lodash@4.17.21: {} + + long@5.3.2: {} + + lowercase-keys@2.0.0: {} + + lru-cache@10.4.3: {} + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + micron-parser@1.0.2: + dependencies: + dompurify: 3.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@2.6.0: {} + + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + + mini-svg-data-uri@1.4.4: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mitt@3.0.1: {} + + mkdirp@1.0.4: {} + + 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.11: {} + + natural-compare@1.4.0: {} + + node-addon-api@1.7.2: + optional: true + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + normalize-url@6.1.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-keys@1.1.1: + optional: true + + ol@10.7.0: + dependencies: + '@types/rbush': 4.0.0 + earcut: 3.0.2 + geotiff: 2.1.3 + pbf: 4.0.1 + rbush: 4.0.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-cancelable@2.1.1: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + pako@2.1.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-headers@2.0.6: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + pbf@4.0.1: + dependencies: + resolve-protobuf-schema: 2.1.0 + + pend@1.2.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.11 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.7.4: {} + + process-nextick-args@2.0.1: {} + + progress@2.0.3: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 25.0.3 + long: 5.3.2 + + protocol-buffers-schema@3.6.0: {} + + proxy-from-env@1.1.0: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + quick-lru@5.1.1: {} + + quick-lru@6.1.2: {} + + quickselect@3.0.0: {} + + rbush@4.0.1: + dependencies: + quickselect: 3.0.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + read-config-file@6.3.2: + dependencies: + config-file-ts: 0.2.6 + dotenv: 9.0.2 + dotenv-expand: 5.1.0 + js-yaml: 4.1.1 + json5: 2.2.3 + lazy-val: 1.0.5 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + regexp-tree@0.1.27: {} + + require-directory@2.1.1: {} + + resolve-alpn@1.2.1: {} + + resolve-from@4.0.0: {} + + resolve-protobuf-schema@2.1.0: + dependencies: + protocol-buffers-schema: 3.6.0 + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + retry@0.12.0: {} + + reusify@1.1.0: {} + + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + + rollup@4.54.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.54.0 + '@rollup/rollup-android-arm64': 4.54.0 + '@rollup/rollup-darwin-arm64': 4.54.0 + '@rollup/rollup-darwin-x64': 4.54.0 + '@rollup/rollup-freebsd-arm64': 4.54.0 + '@rollup/rollup-freebsd-x64': 4.54.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.54.0 + '@rollup/rollup-linux-arm-musleabihf': 4.54.0 + '@rollup/rollup-linux-arm64-gnu': 4.54.0 + '@rollup/rollup-linux-arm64-musl': 4.54.0 + '@rollup/rollup-linux-loong64-gnu': 4.54.0 + '@rollup/rollup-linux-ppc64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-musl': 4.54.0 + '@rollup/rollup-linux-s390x-gnu': 4.54.0 + '@rollup/rollup-linux-x64-gnu': 4.54.0 + '@rollup/rollup-linux-x64-musl': 4.54.0 + '@rollup/rollup-openharmony-arm64': 4.54.0 + '@rollup/rollup-win32-arm64-msvc': 4.54.0 + '@rollup/rollup-win32-ia32-msvc': 4.54.0 + '@rollup/rollup-win32-x64-gnu': 4.54.0 + '@rollup/rollup-win32-x64-msvc': 4.54.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex@2.1.1: + dependencies: + regexp-tree: 0.1.27 + + safer-buffer@2.1.2: {} + + sanitize-filename@1.6.3: + dependencies: + truncate-utf8-bytes: 1.0.2 + + sax@1.4.3: {} + + semver-compare@1.0.0: + optional: true + + semver@6.3.1: {} + + semver@7.7.3: {} + + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.3 + + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + optional: true + + smart-buffer@4.2.0: + optional: true + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + sprintf-js@1.1.3: + optional: true + + stat-mode@1.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-json-comments@3.1.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.15 + ts-interface-checker: 0.1.13 + + sumchecker@3.0.1: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + + 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.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + temp-file@3.4.0: + dependencies: + async-exit-hook: 2.0.1 + fs-extra: 10.1.0 + + terser@5.44.1: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.5 + + tmp@0.2.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + truncate-utf8-bytes@1.0.2: + dependencies: + utf8-byte-length: 1.0.5 + + ts-interface-checker@0.1.13: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.13.1: + optional: true + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + undici-types@7.16.0: {} + + universalify@0.1.2: {} + + universalify@2.0.1: {} + + upath@2.0.1: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + utf8-byte-length@1.0.5: {} + + util-deprecate@1.0.2: {} + + uuid@11.1.0: {} + + verror@1.10.1: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.4.1 + optional: true + + vis-data@7.1.10(uuid@11.1.0)(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@2.0.0)): + dependencies: + uuid: 11.1.0 + vis-util: 5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@2.0.0) + + vis-network@9.1.13(@egjs/hammerjs@2.0.17)(component-emitter@2.0.0)(keycharm@0.4.0)(uuid@11.1.0)(vis-data@7.1.10(uuid@11.1.0)(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@2.0.0)))(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@2.0.0)): + dependencies: + '@egjs/hammerjs': 2.0.17 + component-emitter: 2.0.0 + keycharm: 0.4.0 + uuid: 11.1.0 + vis-data: 7.1.10(uuid@11.1.0)(vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@2.0.0)) + vis-util: 5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@2.0.0) + + vis-util@5.0.7(@egjs/hammerjs@2.0.17)(component-emitter@2.0.0): + dependencies: + '@egjs/hammerjs': 2.0.17 + component-emitter: 2.0.0 + + vite-plugin-vuetify@2.1.2(vite@6.4.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1))(vue@3.5.26(typescript@5.9.3))(vuetify@3.11.6): + dependencies: + '@vuetify/loader-shared': 2.1.1(vue@3.5.26(typescript@5.9.3))(vuetify@3.11.6) + debug: 4.4.3 + upath: 2.0.1 + vite: 6.4.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1) + vue: 3.5.26(typescript@5.9.3) + vuetify: 3.11.6(typescript@5.9.3)(vite-plugin-vuetify@2.1.2)(vue@3.5.26(typescript@5.9.3)) + transitivePeerDependencies: + - supports-color + + vite@6.4.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.54.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.0.3 + fsevents: 2.3.3 + jiti: 1.21.7 + terser: 5.44.1 + + vue-eslint-parser@10.2.0(eslint@9.39.2(jiti@1.21.7)): + dependencies: + debug: 4.4.3 + eslint: 9.39.2(jiti@1.21.7) + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + + vue-i18n@11.2.8(vue@3.5.26(typescript@5.9.3)): + dependencies: + '@intlify/core-base': 11.2.8 + '@intlify/shared': 11.2.8 + '@vue/devtools-api': 6.6.4 + vue: 3.5.26(typescript@5.9.3) + + vue-router@4.6.4(vue@3.5.26(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.26(typescript@5.9.3) + + vue@3.5.26(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.26 + '@vue/compiler-sfc': 3.5.26 + '@vue/runtime-dom': 3.5.26 + '@vue/server-renderer': 3.5.26(vue@3.5.26(typescript@5.9.3)) + '@vue/shared': 3.5.26 + optionalDependencies: + typescript: 5.9.3 + + vuetify@3.11.6(typescript@5.9.3)(vite-plugin-vuetify@2.1.2)(vue@3.5.26(typescript@5.9.3)): + dependencies: + vue: 3.5.26(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + vite-plugin-vuetify: 2.1.2(vite@6.4.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1))(vue@3.5.26(typescript@5.9.3))(vuetify@3.11.6) + + web-worker@1.5.0: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + xml-name-validator@4.0.0: {} + + xml-utils@1.10.2: {} + + xmlbuilder@15.1.1: {} + + y18n@5.0.8: {} + + yallist@4.0.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yocto-queue@0.1.0: {} + + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + + zstddec@0.1.0: {} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..91a80d7 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1978 @@ +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, +] + +[[package]] +name = "aiohttp" +version = "3.13.2" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohttp-3.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155"}, + {file = "aiohttp-3.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c"}, + {file = "aiohttp-3.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636"}, + {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da"}, + {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725"}, + {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5"}, + {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3"}, + {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802"}, + {file = "aiohttp-3.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a"}, + {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204"}, + {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22"}, + {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d"}, + {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f"}, + {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f"}, + {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6"}, + {file = "aiohttp-3.13.2-cp310-cp310-win32.whl", hash = "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251"}, + {file = "aiohttp-3.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514"}, + {file = "aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0"}, + {file = "aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb"}, + {file = "aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9"}, + {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613"}, + {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead"}, + {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780"}, + {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a"}, + {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592"}, + {file = "aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab"}, + {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30"}, + {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40"}, + {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948"}, + {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf"}, + {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782"}, + {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8"}, + {file = "aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec"}, + {file = "aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c"}, + {file = "aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b"}, + {file = "aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc"}, + {file = "aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7"}, + {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb"}, + {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3"}, + {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f"}, + {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6"}, + {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e"}, + {file = "aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7"}, + {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d"}, + {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b"}, + {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8"}, + {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16"}, + {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169"}, + {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248"}, + {file = "aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e"}, + {file = "aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45"}, + {file = "aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be"}, + {file = "aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742"}, + {file = "aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293"}, + {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811"}, + {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a"}, + {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4"}, + {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a"}, + {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e"}, + {file = "aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb"}, + {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded"}, + {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b"}, + {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8"}, + {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04"}, + {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476"}, + {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23"}, + {file = "aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254"}, + {file = "aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a"}, + {file = "aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b"}, + {file = "aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61"}, + {file = "aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4"}, + {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b"}, + {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694"}, + {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906"}, + {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9"}, + {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011"}, + {file = "aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6"}, + {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213"}, + {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49"}, + {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae"}, + {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa"}, + {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4"}, + {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a"}, + {file = "aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940"}, + {file = "aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4"}, + {file = "aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673"}, + {file = "aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd"}, + {file = "aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3"}, + {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf"}, + {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e"}, + {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5"}, + {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad"}, + {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e"}, + {file = "aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61"}, + {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661"}, + {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98"}, + {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693"}, + {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a"}, + {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be"}, + {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c"}, + {file = "aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734"}, + {file = "aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f"}, + {file = "aiohttp-3.13.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7fbdf5ad6084f1940ce88933de34b62358d0f4a0b6ec097362dcd3e5a65a4989"}, + {file = "aiohttp-3.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7c3a50345635a02db61792c85bb86daffac05330f6473d524f1a4e3ef9d0046d"}, + {file = "aiohttp-3.13.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e87dff73f46e969af38ab3f7cb75316a7c944e2e574ff7c933bc01b10def7f5"}, + {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2adebd4577724dcae085665f294cc57c8701ddd4d26140504db622b8d566d7aa"}, + {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e036a3a645fe92309ec34b918394bb377950cbb43039a97edae6c08db64b23e2"}, + {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:23ad365e30108c422d0b4428cf271156dd56790f6dd50d770b8e360e6c5ab2e6"}, + {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1f9b2c2d4b9d958b1f9ae0c984ec1dd6b6689e15c75045be8ccb4011426268ca"}, + {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a92cf4b9bea33e15ecbaa5c59921be0f23222608143d025c989924f7e3e0c07"}, + {file = "aiohttp-3.13.2-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:070599407f4954021509193404c4ac53153525a19531051661440644728ba9a7"}, + {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:29562998ec66f988d49fb83c9b01694fa927186b781463f376c5845c121e4e0b"}, + {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4dd3db9d0f4ebca1d887d76f7cdbcd1116ac0d05a9221b9dad82c64a62578c4d"}, + {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d7bc4b7f9c4921eba72677cd9fedd2308f4a4ca3e12fab58935295ad9ea98700"}, + {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dacd50501cd017f8cccb328da0c90823511d70d24a323196826d923aad865901"}, + {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:8b2f1414f6a1e0683f212ec80e813f4abef94c739fd090b66c9adf9d2a05feac"}, + {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04c3971421576ed24c191f610052bcb2f059e395bc2489dd99e397f9bc466329"}, + {file = "aiohttp-3.13.2-cp39-cp39-win32.whl", hash = "sha256:9f377d0a924e5cc94dc620bc6366fc3e889586a7f18b748901cf016c916e2084"}, + {file = "aiohttp-3.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:9c705601e16c03466cb72011bd1af55d68fa65b045356d8f96c216e5f6db0fa5"}, + {file = "aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiohttp-session" +version = "2.12.1" +description = "sessions for aiohttp.web" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "aiohttp_session-2.12.1-py3-none-any.whl", hash = "sha256:654df46c3c9b73294312795f558c3bca4a85bfd3b01a8b744d984ae3958dce5f"}, + {file = "aiohttp_session-2.12.1.tar.gz", hash = "sha256:15e6e0288e9bcccd4b1d0c28aae9c20e19a252b12d0cb682223ca9c83180e899"}, +] + +[package.dependencies] +aiohttp = ">=3.10" + +[package.extras] +aiomcache = ["aiomcache (>=0.5.2)"] +aioredis = ["redis (>=4.3.1)"] +pycrypto = ["cryptography"] +pynacl = ["pynacl"] +secure = ["cryptography"] + +[[package]] +name = "aiosignal" +version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} + +[[package]] +name = "attrs" +version = "25.4.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, + {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, +] + +[[package]] +name = "audioop-lts" +version = "0.2.2" +description = "LTS Port of Python audioop" +optional = false +python-versions = ">=3.13" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800"}, + {file = "audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303"}, + {file = "audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449"}, + {file = "audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636"}, + {file = "audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e"}, + {file = "audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd"}, + {file = "audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0"}, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be"}, + {file = "bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2"}, + {file = "bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f"}, + {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86"}, + {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23"}, + {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2"}, + {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83"}, + {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746"}, + {file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e"}, + {file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d"}, + {file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba"}, + {file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41"}, + {file = "bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861"}, + {file = "bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e"}, + {file = "bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5"}, + {file = "bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef"}, + {file = "bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4"}, + {file = "bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf"}, + {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da"}, + {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9"}, + {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f"}, + {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493"}, + {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b"}, + {file = "bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c"}, + {file = "bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4"}, + {file = "bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e"}, + {file = "bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d"}, + {file = "bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993"}, + {file = "bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b"}, + {file = "bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb"}, + {file = "bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef"}, + {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd"}, + {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd"}, + {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464"}, + {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75"}, + {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff"}, + {file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4"}, + {file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb"}, + {file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c"}, + {file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb"}, + {file = "bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538"}, + {file = "bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9"}, + {file = "bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980"}, + {file = "bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a"}, + {file = "bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191"}, + {file = "bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254"}, + {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db"}, + {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac"}, + {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822"}, + {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8"}, + {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a"}, + {file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1"}, + {file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42"}, + {file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10"}, + {file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172"}, + {file = "bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683"}, + {file = "bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2"}, + {file = "bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927"}, + {file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534"}, + {file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4"}, + {file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911"}, + {file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4"}, + {file = "bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "cabarchive" +version = "0.2.4" +description = "A pure-python library for creating and extracting cab files" +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "cabarchive-0.2.4-py3-none-any.whl", hash = "sha256:4afabd224eb2e40af8e907379fb8eec6b0adfb71c2aef4457ec3a4d77383c059"}, + {file = "cabarchive-0.2.4.tar.gz", hash = "sha256:04f60089473114cf26eab2b7e1d09611c5bfaf8edd3202dacef66bb5c71e48cf"}, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, + {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["main"] +files = [ + {file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"}, + {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"}, + {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"}, + {file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"}, + {file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"}, + {file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"}, + {file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"}, + {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"}, + {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de"}, + {file = "cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914"}, + {file = "cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db"}, + {file = "cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21"}, + {file = "cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506"}, + {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963"}, + {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4"}, + {file = "cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df"}, + {file = "cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f"}, + {file = "cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372"}, + {file = "cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32"}, + {file = "cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c"}, + {file = "cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "cx-freeze" +version = "8.5.2" +description = "Create standalone executables from Python scripts" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "cx_freeze-8.5.2-py3-none-any.whl", hash = "sha256:b12811adbc9546e7a5d61fce27f78e13142ffd50cde5e8c92be36655f06c9900"}, + {file = "cx_freeze-8.5.2.tar.gz", hash = "sha256:e3c72bd5fae1e49f67a4cc747526c8ef48182bf2f8d90e85c840cc4bbba4d1c5"}, +] + +[package.dependencies] +dmgbuild = {version = ">=1.6.1", markers = "sys_platform == \"darwin\""} +filelock = {version = ">=3.20.1", markers = "sys_platform == \"linux\""} +freeze-core = ">=0.4.0" +lief = {version = ">=0.15.1,<=0.17.1", markers = "sys_platform == \"win32\" and (platform_machine != \"ARM64\" or python_version <= \"3.13\")"} +packaging = ">=24" +patchelf = {version = ">=0.16.1,<0.18", markers = "sys_platform == \"linux\" and (platform_machine == \"x86_64\" or platform_machine == \"i686\" or platform_machine == \"aarch64\" or platform_machine == \"armv7l\" or platform_machine == \"ppc64le\" or platform_machine == \"s390x\")"} +python-msilib = {version = ">=0.4.1", markers = "sys_platform == \"win32\" and python_version >= \"3.13\""} +setuptools = ">=78.1.1,<81" + +[package.extras] +dev = ["bump-my-version (==1.2.5)", "pre-commit (==4.5.1)"] +doc = ["furo (==2025.9.25) ; python_version >= \"3.11\"", "myst-parser (==4.0.1) ; python_version >= \"3.11\"", "sphinx (==8.2.3) ; python_version >= \"3.11\"", "sphinx-issues (==5.0.1) ; python_version >= \"3.11\"", "sphinx-new-tab-link (==0.8.1) ; python_version >= \"3.11\"", "sphinx-tabs (==3.4.7) ; python_version >= \"3.11\""] +tests = ["coverage (==7.13.0)", "filelock (>=3.20.1)", "pluggy (==1.6.0)", "pytest (==9.0.2)", "pytest-mock (==3.15.1)", "pytest-timeout (==2.4.0)", "pytest-xdist (==3.8.0)"] + +[[package]] +name = "dmgbuild" +version = "1.6.6" +description = "macOS command line utility to build disk images" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "sys_platform == \"darwin\"" +files = [ + {file = "dmgbuild-1.6.6-py3-none-any.whl", hash = "sha256:701e83b06155c189194408fab0cb39a6b85a0be8f4d052f6ac69a7507d578026"}, + {file = "dmgbuild-1.6.6.tar.gz", hash = "sha256:72ce448feec6aa24dba5e777c44a0397b7ae1b4d86fb4dd537e44cf1653c03ec"}, +] + +[package.dependencies] +ds_store = ">=1.1.0" +mac_alias = ">=2.0.1" + +[package.extras] +badge-icons = ["pyobjc-framework-Quartz (>=3.0.4)"] + +[[package]] +name = "ds-store" +version = "1.3.2" +description = "Manipulate Finder .DS_Store files from Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "sys_platform == \"darwin\"" +files = [ + {file = "ds_store-1.3.2-py3-none-any.whl", hash = "sha256:3b37332d9f8c18ff04c385d2933b66a1d84950071364f94bfc5f5a3ab1fb361e"}, + {file = "ds_store-1.3.2.tar.gz", hash = "sha256:e4da49df901123481a85b9250945f2aac054a0f0c29352cd2cc858c536cdd364"}, +] + +[package.dependencies] +mac_alias = ">=2.2.2" + +[[package]] +name = "filelock" +version = "3.20.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a"}, + {file = "filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c"}, +] + +[[package]] +name = "freeze-core" +version = "0.4.2" +description = "Core dependency for cx_Freeze" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "freeze_core-0.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e59695d4b3543dc89fc9c8464ca076a5c8083962452da3ecc51cd2ad6777ab7f"}, + {file = "freeze_core-0.4.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e6a1e733be02b986ef373e183066b0bdececde241fee88e28a5af96cce6b4a3"}, + {file = "freeze_core-0.4.2-cp310-cp310-win32.whl", hash = "sha256:eb0513828dae5f4622f9a354ad17c5cc3aec81bf53c4bece3d414fe54482599e"}, + {file = "freeze_core-0.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:e36417771af59d6a6bc7677085c1d1892b6874264ce50a878c76bc47e7da3ec8"}, + {file = "freeze_core-0.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3663eb7f994c3976bda99dc72aa8bd8536ff2eea7e1458cbfd5eeae928b0e0f9"}, + {file = "freeze_core-0.4.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cdee0ac751117fd4b70b1de2053a8c3d217a57aed4d55e67ded70f87bc83e4e7"}, + {file = "freeze_core-0.4.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9655614fdfd85005987f6d37f761f203417581bd30716a612e018fc5e711b877"}, + {file = "freeze_core-0.4.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:282937d91e056ecec3eac794111f943075d08c3dfec3c8efc88d6529b7ff153d"}, + {file = "freeze_core-0.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eeda39821eed2a98f74611badc2ea390c124542cbe8ff912e7f536d20e11ea73"}, + {file = "freeze_core-0.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f96df0f8e8839817bc58b7b942d0f3af54f4b7730bfcf86f2db524126f389f97"}, + {file = "freeze_core-0.4.2-cp311-cp311-win32.whl", hash = "sha256:a2923128672f97058a1fe15cf4dee0ac674467a178f71c092a5d9a94a4d0be8b"}, + {file = "freeze_core-0.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:43860e0214d9fc5e41f0c17f931773e80f870a791ea647940017bb83d81c5e66"}, + {file = "freeze_core-0.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:4a2a26acba4f3693a495a14f8481b15c6dbef99d0b5b1fcef96234b4947d2030"}, + {file = "freeze_core-0.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1ba4915dbf799dade21a96eb9c521c9b0f9a2496ebe510fa54b6e6bd421fb174"}, + {file = "freeze_core-0.4.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db330723a6b899b32dfa038efc8cfc4f9936a01561549f75dfaeb83f13eb74a2"}, + {file = "freeze_core-0.4.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d02a5ea6e87242ccf228304f0483df61873b1432e75c74e1de53013d084ead9e"}, + {file = "freeze_core-0.4.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0d2f1a9aea27825f5311f23a9b5ed52a7b0d8d5a6bb1231318252089339ae1c4"}, + {file = "freeze_core-0.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e5e35148ed4776d572a7b6678c2bbd6e1eb21782f0da65d6276aa52b29b392b"}, + {file = "freeze_core-0.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be464f358ba802fad1d7907767a4875a520f8a2d655026e2cc405b22f86c54eb"}, + {file = "freeze_core-0.4.2-cp312-cp312-win32.whl", hash = "sha256:439f3ebc79adf0ca575c20364cd266e75a222e47ae4840db3c3bdbc8d296c8dc"}, + {file = "freeze_core-0.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:ced01393db4592d5eb86be4db5d7e57f70baf5f90968718c1b52960b09e04660"}, + {file = "freeze_core-0.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:9d978b30f92f475c863e11485dac4a810c552dd8d5376a1e3d69b4068b18ace4"}, + {file = "freeze_core-0.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e822d7dadf6c4135072c86bdca2c5eea4772f32438eed497457d5c5be869b989"}, + {file = "freeze_core-0.4.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc471a64d13ab203402b58b41b0dd63b654dfca6741d9e9582e08275a2a582bd"}, + {file = "freeze_core-0.4.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:96b5ba541739d7e2821db1d3eb3bbc61364be5da017ba2e3461d85b5529d7ca4"}, + {file = "freeze_core-0.4.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a8a56975e6d4fe17ac9237a6578cb7e07c124698aae7c99345fa5615e7462ad"}, + {file = "freeze_core-0.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dbec55ae2a5e7f6dbba4f62089542c1239c76adedf08ad38209397d0027c5695"}, + {file = "freeze_core-0.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cad2c3e9815267ecb3510acbc87ba15913b6dc01e9f8e09e81599389b31c7a2c"}, + {file = "freeze_core-0.4.2-cp313-cp313-win32.whl", hash = "sha256:e5398a9523efbfe1d8350ccd9587b5f3a1612ccd9a26fa35ad159399d4857fb1"}, + {file = "freeze_core-0.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:d125534089278c790864ed723d301290450bb80f47aab0b17254a6f085d70a01"}, + {file = "freeze_core-0.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:82a8e980b2e0f723adaf6fbe0ccba8b3a86976689f7cdeb03609a65be45e22ad"}, + {file = "freeze_core-0.4.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:bafd9dc93d35babc33681c48e1597f73c44cb12fdf599d0a87c967a00c1dfc50"}, + {file = "freeze_core-0.4.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f8389f3aa06d800252a725b0f13a38dcaa88b3164428c0d023066ba796353fe9"}, + {file = "freeze_core-0.4.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4877158870eb6ec22eb7e8d639c066ffe9ebf87d61b429094e77564bcc33f2f0"}, + {file = "freeze_core-0.4.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:438336cdae8d742aed7c33e2e957b2be6458bdebe5dbd66e9ae7912bcaffa7d3"}, + {file = "freeze_core-0.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3eec7e9eb34effbaf99bf405761f6f871d8e854a7128d21211f659960c8084f1"}, + {file = "freeze_core-0.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9e998ebd1a448a0384ea4f1c13f3b360baffc86776ffb745dce0bc8f189dc9f5"}, + {file = "freeze_core-0.4.2-cp313-cp313t-win32.whl", hash = "sha256:afad0fd1431114f3ea6e1592647f9a4bbfefa37e15606bb606b9cafe6d038ba4"}, + {file = "freeze_core-0.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e7db75a7c889d4113dbf0c2ce060f79e2fe22ce9149df7c1ea9332f20d5da76d"}, + {file = "freeze_core-0.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:497acb3e7d4d0bc9ab6374e9f01388d4b5a9f26e1d5fd730b515cdaa25532949"}, + {file = "freeze_core-0.4.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:cf9ec119cf4c91e9a1137826b35648ede032c3795577db1814d922e76f4ccfe8"}, + {file = "freeze_core-0.4.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23483b68a6c2ca5afba5909764647a4abb249c2b9475a581e15c5e9a6a0581e1"}, + {file = "freeze_core-0.4.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b1ff94052f0863449bdf538d431d718c21cc3be1c8055174a511d65b21255233"}, + {file = "freeze_core-0.4.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:23b096e1a3becea53f7f9cf466e9b5da0d723283c9353fafb81bef2401d7bf22"}, + {file = "freeze_core-0.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:93e88c8e465674891975f4d0c7395f9926ab4484955fbdea0b30f496a6ac5309"}, + {file = "freeze_core-0.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1080a654bad3b9658cc45cbb5528e48386e1e75569bf940405b1d1657a4b9cb0"}, + {file = "freeze_core-0.4.2-cp314-cp314-win32.whl", hash = "sha256:a5ae6eca00c8d2ca607c549e1e4dec5de15a8dadb73e03c35c58409ce4302360"}, + {file = "freeze_core-0.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:a5b67313ac10828ef067f187e87d03518fa262a4a9c9d022087b9d6e953cc8be"}, + {file = "freeze_core-0.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:c03aa08c7f2ee035655848e256d0ef99920e8038550c60a84c6dcd1f857ff1f4"}, + {file = "freeze_core-0.4.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c4b43b52c4fc8659b9b3e29772a2b5bcc3ae5e650d30efb95e6a844294d84ce5"}, + {file = "freeze_core-0.4.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3b965d79b592b5a31fb6f3de71ca058bbc58aad7304ad65390a18cb8cf174487"}, + {file = "freeze_core-0.4.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0ae7cce7fcdcd8faa96792dab1af855e566b5ad464509887b8b7a579ac36980e"}, + {file = "freeze_core-0.4.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5e42a3fc4fae92b872503d0ee44cca9513f6dbe4edffc3a8d7dfaacaecb07e91"}, + {file = "freeze_core-0.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:438f5a1167e22f9a246414fea0aff5f5b18520c365fd30f97bc1900d25d467bb"}, + {file = "freeze_core-0.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c756aa1bc954619ab5493830861ddbc71ded6672346f9dacaf7350ffce53fe1"}, + {file = "freeze_core-0.4.2-cp314-cp314t-win32.whl", hash = "sha256:3915f4bad2f23786ef4f78e4a3069b20e63c42d3ef2a6193f25cf0fe9f1ed82f"}, + {file = "freeze_core-0.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:815eefedf318cc569fe127c592c92ec8e176f29210a40abe1bf18595fe04f97e"}, + {file = "freeze_core-0.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:429d2f82e899e568d45a9cc942765b6af5426134fcd8a5c27b375d8969cfb836"}, + {file = "freeze_core-0.4.2.tar.gz", hash = "sha256:3e1942b0908b9399b164f66712f8b222f38512950e61d16a5064d9795f0b0ac7"}, +] + +[package.dependencies] +cabarchive = {version = ">=0.2.4", markers = "sys_platform == \"win32\""} +filelock = ">=3.15.3" +striprtf = {version = ">=0.0.26", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["bump-my-version (==1.2.4)", "cibuildwheel (==3.3.0)", "pre-commit (==4.5.0)"] +tests = ["coverage (==7.12.0)", "pytest (==9.0.1)"] + +[[package]] +name = "frozenlist" +version = "1.8.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, + {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, + {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, + {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, + {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, + {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, + {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, + {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, + {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, +] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "lief" +version = "0.17.1" +description = "Library to instrument executable formats" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "sys_platform == \"win32\" and (platform_machine != \"ARM64\" or python_version <= \"3.13\")" +files = [ + {file = "lief-0.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55f6122716c50fb1eabc73b5f36baa35667a6d900214a84aada3c493af6de5cc"}, + {file = "lief-0.17.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:64637d926607919082fadbeca784e09f59e8fc03ae527cc06f82bfd644c4a12a"}, + {file = "lief-0.17.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d2b7dfb2523b57824abf7d8f951899a00042bf618e73fee8eb2d451fc4744f3b"}, + {file = "lief-0.17.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:f22ba9d271d174f973036ce99d07954b96020517a1ced479e6b5686059b8a682"}, + {file = "lief-0.17.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e4150376dccc792cf9f1bdd54149e3a8191a2e7e458ac434352e47a93890c157"}, + {file = "lief-0.17.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1058bdd99be989075585daa7517b1da58e2c2a98e7cb3c0f95123d3f2240ccb5"}, + {file = "lief-0.17.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:96b301eac113e211d58c12a58357c50a5a403f076c4788458b58b23bcdfe9f6b"}, + {file = "lief-0.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4cb22f259cb44832fb279769818cbec50814b80c0df0a9eb08ffe25f1087076a"}, + {file = "lief-0.17.1-cp310-cp310-win32.whl", hash = "sha256:dea013fb64dbf84523f19adf1a8ceac8d69582d72c297320af8ff2123204620c"}, + {file = "lief-0.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:d6d60f6c866d287e37af155aeb356142fe106f603528e7ea2b984529eb1af4e6"}, + {file = "lief-0.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:632c6ff6b0a7a87349eed334f16e9bb81ce8b455bfa35371965e606c88813ee8"}, + {file = "lief-0.17.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:8195890069b3baee3aeaff22f9cc26b7ed3aba711f16d0e7824eb8c0eaca7183"}, + {file = "lief-0.17.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d3eb2a7e77d956a8d88caf6def2974d751c9c7ff0a82e981e147446945a7256e"}, + {file = "lief-0.17.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:99eb7b3e2d947e455b23b128ec6e6e85d160a5e6efaa8370250f2d6324b7470a"}, + {file = "lief-0.17.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:702f612fdf9627fd7bd9a0af4f3ea5d437479a03d4f5872c1f250038a228466a"}, + {file = "lief-0.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5419f3ad5011716a46d55637961fa6f39188dccd43f00f1043328e1a34df391"}, + {file = "lief-0.17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d3db7bded3d9f4742a1f98fdee7dea14702d943ef7afd817714801610cc284f1"}, + {file = "lief-0.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d3337314ee663ef41f78e2b0ff6f6563819b68a75630d8c13f1867f0af9c6e5b"}, + {file = "lief-0.17.1-cp311-cp311-win32.whl", hash = "sha256:2dd4fe914111aaccc3ad7385caf4265e9ba4766ba6646f0b5556c9a8b4b2168f"}, + {file = "lief-0.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ceb9801b3c6366cf7ef7c808afad7b3f94f41338f1ef33ee4e3f916121d78354"}, + {file = "lief-0.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:d734be459521ec0162d92357104069e734cbaa5569f25fb74a19463bf9a8ef25"}, + {file = "lief-0.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48a8d96e0132323cca4d065f2767db5c30cc80ebcf13ced387db2b1f743a0d1c"}, + {file = "lief-0.17.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:a89bc93808e870341067252b349bdc98135f006456b0bead99f5544ccb627ff7"}, + {file = "lief-0.17.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:629e92ec77ce7d1c6842f89ee155c61b70aefc02477b9d579da0a5c274aaf84c"}, + {file = "lief-0.17.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:594bb50ab54c3ad280c57bb9b34f8f13137952db36709a2cd925558eaad268d4"}, + {file = "lief-0.17.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b204c1f0dfeb820c7982fe40924cd147bc11b1c598d27acddb1ba64a5c653f1b"}, + {file = "lief-0.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d9c07ca8e8c2b0d72281fb5e52568b95e893a7d6fb0a84c4af3c58cc10b73892"}, + {file = "lief-0.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9657541b8e7a43cd0fc52300cacc1a5e1d2023eec4a1929554493932e630e7b1"}, + {file = "lief-0.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:479bc9464e8c4af1eaeb0faa77d0dd02d57b6d7bd146a73d494443b618ff5043"}, + {file = "lief-0.17.1-cp312-cp312-win32.whl", hash = "sha256:86c130a0ec2fc3ebb0a230491fd176aa2f26dc4f87a261932046fe3f6c4fd8a6"}, + {file = "lief-0.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:72cc7c8b2f9f2e1ad19d6b771dc9865f4e3656fd99a057e9b01c02207c9c7e25"}, + {file = "lief-0.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:9f0b267b0771af1ef9de64e437391282e57009e68d509146ae76de1588da0cbf"}, + {file = "lief-0.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dfde4ee44c34affe1fbe25177553deff0d38152d5e98fcf5409ea7f2133e4643"}, + {file = "lief-0.17.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:68fff6f4cb32b0cd674cb292e8c9e00bd60d5ccf978da345772c591247ebacd3"}, + {file = "lief-0.17.1-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:311a9e785c7681a09ea4b288ce12fe667d353ede1a65e44bda580f209064d7ab"}, + {file = "lief-0.17.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:ed967b1155fa55f6d4f7ce6e250c17ae6ab59bf1dc21088f665c6d1b0582eecf"}, + {file = "lief-0.17.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:72d52cceb92886e14c79b0fa62c4ae1fbe70888377b32a708d7f38c3c27447fa"}, + {file = "lief-0.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:69eb6fa86c785d17eabbfc780bf319b0a989303f74772f351be2ac4a65f442c4"}, + {file = "lief-0.17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a8421dfd29056675516b1c7f8e84a4e5d0c12fc71f832263feecbade44b70610"}, + {file = "lief-0.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9dff0644ed434d679411e31d9b000792f28bdec9f12e699b9adc239edc4d81e"}, + {file = "lief-0.17.1-cp313-cp313-win32.whl", hash = "sha256:571b830523037efbfb9fcd58707c715ae417e2514e5abb3b03dc67b4c50f47fe"}, + {file = "lief-0.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:7ada5acf4d83b5252c5fa46da3ff20c396bcdfa7aeb800c5dd28f8faa6827184"}, + {file = "lief-0.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:52fb1bff33cc8e2d3da8a2b426e9f8e5bdb334e351e70bc21b87db374fd4a837"}, + {file = "lief-0.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:94505d03c63f68a2f740b72e87ae4fc80eff7c5e63c7d09582535888ef676d5c"}, + {file = "lief-0.17.1-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:9198f88d3e168dda0c2891f6c343fc2936f8245139ea09683d2726a1b920bdb5"}, + {file = "lief-0.17.1-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:54de530262d7dad925523102bfdbc2ead26aa5ba8819e539ac7373a4ba8868c5"}, + {file = "lief-0.17.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:203019139646cd2be6a43e0f216b1463af2f6cb00c4c8006ccf6c62cb2aa2eda"}, + {file = "lief-0.17.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:50864f0c57a5702849c662d01c80b1ec030a92867beffb977d264f46fd658c06"}, + {file = "lief-0.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:616b8eb51f9205a1bc90945487040522573306bfd7a4d0b8f838b61ff060b1a2"}, + {file = "lief-0.17.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:770c9fc5852050f3ff5f08d9a702d228114aa27a8385f1d72e2497b73fdecb27"}, + {file = "lief-0.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f73391aaf3848b81ed130928cba06a239d225de590be1e067701e05fd0b33aae"}, + {file = "lief-0.17.1-cp314-cp314-win32.whl", hash = "sha256:f610d5937df9de86962733673f1a2b5255931c50b004e614728e83cbb0620057"}, + {file = "lief-0.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3dadb33cad8cf01d78a5eb12fb660ed1a06619f0baade38b606fd151e87436e2"}, + {file = "lief-0.17.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6495fbd5649eb804192a79010416e11dfdadbe9b12b42031617c677b28dbc5d5"}, + {file = "lief-0.17.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:da2ca340c7d3e4419da8b59d6f9d819bd9dc83d13640fb6f85b16195dad73fed"}, + {file = "lief-0.17.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:aafbdca896904aa7817d71a9856cabebbe79603fd3eaad63ea666012de6f2e0d"}, + {file = "lief-0.17.1-cp38-cp38-manylinux_2_28_i686.whl", hash = "sha256:0891738e9cd1a834392648b09470419cf4b1ad7b64a354c2a24344d335966f14"}, + {file = "lief-0.17.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:2880fb8b3b55923c05392a7c4131192b4037604d696b8994e3c45d94b3bbe627"}, + {file = "lief-0.17.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:cf3dd76768d775509ab75c547b1ad802a8469e8d8ac5f69f2053103ca4e4676b"}, + {file = "lief-0.17.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:50cb074a87b90f13f76e2b110dcf26196f255d059432e2656168d5ec875e83f4"}, + {file = "lief-0.17.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:137d3644d8e4e1b67208c3a3c7beace6f91e7ab9340c1c4a60304f2a7ec6e1c8"}, + {file = "lief-0.17.1-cp38-cp38-win32.whl", hash = "sha256:687f50d95ea8154c7781ab5dac69d923b0f30fde40d057a765bba7f1c8df3fc4"}, + {file = "lief-0.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:a77daf35375dc8e616a9568f6e0af3808afd1b2dd9c92f09c03f990f621a097a"}, + {file = "lief-0.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b366f8401ae35c3fe8f9996c9ba36b597d8dab0385001891c1a77e85767e917"}, + {file = "lief-0.17.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:52090039ed64a7dde9a6ec215fb416a8b7925631ccd8738b9873b447a6359e49"}, + {file = "lief-0.17.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d20c82e2e1cce3a420fcf0510c47733e0213740470eb1b18e2ecf626267df12a"}, + {file = "lief-0.17.1-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:7feb655f93c79ac9eae1e3ea86af36b6aca32eaa3f43a28417dda8626d673ac0"}, + {file = "lief-0.17.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ec89830b43b50e57936764fe08432af3e36b9c5ac0c74d4f2f59612a16fb65b1"}, + {file = "lief-0.17.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8f40226f01eb9fae1d79ad6d46e7e48efbedc2fb5995115a52e1a2b791c0ce49"}, + {file = "lief-0.17.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3bf83773f93676fe81f3c97fe74639865cddca7a4d0d9aecbe1a5f1be7fdca89"}, + {file = "lief-0.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f179a02e2f4dc340867cdf988fb7f5d7e97b93453a416857525ec711e49be68e"}, + {file = "lief-0.17.1-cp39-cp39-win32.whl", hash = "sha256:800e39e16923935ad1afe2c1888574bba04d34d21f035c67af090ab95ac9e40a"}, + {file = "lief-0.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:3011c95c7b682b19717af5913bd6d33a924840bc0de3c6b6ad7db17290c46361"}, +] + +[[package]] +name = "lxmf" +version = "0.9.3" +description = "Lightweight Extensible Message Format for Reticulum" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "lxmf-0.9.3-py3-none-any.whl", hash = "sha256:6da8ea4f47b4d3af49281d04359f656dd57685dac78e90e8f8ba21f90edd9928"}, + {file = "lxmf-0.9.3.tar.gz", hash = "sha256:e5a67b12ff85e5b5d5977218fd08a1d0cb13efb4e8227c75b7b34a43c6db7328"}, +] + +[package.dependencies] +rns = ">=1.0.1" + +[[package]] +name = "lxst" +version = "0.4.5" +description = "Lightweight Extensible Signal Transport for Reticulum" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "lxst-0.4.5-py3-none-any.whl", hash = "sha256:24b39ce21db538b168b80567ecc8d7dbf002e5a86c328aa8571304126d57c79a"}, +] + +[package.dependencies] +audioop-lts = {version = ">=0.2.1", markers = "python_version >= \"3.13\""} +cffi = ">=2.0.0" +lxmf = ">=0.9.3" +numpy = ">=2.3.4" +pycodec2 = ">=4.1.0" +rns = ">=1.0.4" + +[[package]] +name = "mac-alias" +version = "2.2.3" +description = "Generate/parse macOS Alias records from Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "sys_platform == \"darwin\"" +files = [ + {file = "mac_alias-2.2.3-py3-none-any.whl", hash = "sha256:7362b521d2132ef92f606a37abfed5fcd849ceb2f28b6f9743e014b02af92f0d"}, + {file = "mac_alias-2.2.3.tar.gz", hash = "sha256:1c7fa367687d66979f2ce4d1a8b2716cf1c9fb811741cab3cf3ca356555c2beb"}, +] + +[[package]] +name = "multidict" +version = "6.7.0" +description = "multidict implementation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349"}, + {file = "multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e"}, + {file = "multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3"}, + {file = "multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046"}, + {file = "multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32"}, + {file = "multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73"}, + {file = "multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc"}, + {file = "multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62"}, + {file = "multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84"}, + {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0"}, + {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e"}, + {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4"}, + {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648"}, + {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111"}, + {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36"}, + {file = "multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85"}, + {file = "multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7"}, + {file = "multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0"}, + {file = "multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc"}, + {file = "multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721"}, + {file = "multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6"}, + {file = "multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c"}, + {file = "multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7"}, + {file = "multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7"}, + {file = "multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9"}, + {file = "multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8"}, + {file = "multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd"}, + {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb"}, + {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6"}, + {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2"}, + {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff"}, + {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b"}, + {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34"}, + {file = "multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff"}, + {file = "multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81"}, + {file = "multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912"}, + {file = "multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184"}, + {file = "multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45"}, + {file = "multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa"}, + {file = "multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7"}, + {file = "multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e"}, + {file = "multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546"}, + {file = "multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4"}, + {file = "multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1"}, + {file = "multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d"}, + {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304"}, + {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12"}, + {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62"}, + {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0"}, + {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a"}, + {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8"}, + {file = "multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4"}, + {file = "multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b"}, + {file = "multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec"}, + {file = "multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6"}, + {file = "multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159"}, + {file = "multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca"}, + {file = "multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8"}, + {file = "multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60"}, + {file = "multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4"}, + {file = "multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f"}, + {file = "multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf"}, + {file = "multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32"}, + {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036"}, + {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec"}, + {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e"}, + {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64"}, + {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd"}, + {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288"}, + {file = "multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17"}, + {file = "multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390"}, + {file = "multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e"}, + {file = "multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00"}, + {file = "multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb"}, + {file = "multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b"}, + {file = "multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c"}, + {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1"}, + {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b"}, + {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5"}, + {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad"}, + {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c"}, + {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5"}, + {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10"}, + {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754"}, + {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c"}, + {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762"}, + {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6"}, + {file = "multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d"}, + {file = "multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6"}, + {file = "multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792"}, + {file = "multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842"}, + {file = "multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b"}, + {file = "multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38"}, + {file = "multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128"}, + {file = "multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34"}, + {file = "multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99"}, + {file = "multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202"}, + {file = "multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1"}, + {file = "multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3"}, + {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d"}, + {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6"}, + {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7"}, + {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb"}, + {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f"}, + {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f"}, + {file = "multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885"}, + {file = "multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c"}, + {file = "multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000"}, + {file = "multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63"}, + {file = "multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718"}, + {file = "multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2"}, + {file = "multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e"}, + {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064"}, + {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e"}, + {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd"}, + {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a"}, + {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96"}, + {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e"}, + {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599"}, + {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394"}, + {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38"}, + {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9"}, + {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0"}, + {file = "multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13"}, + {file = "multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd"}, + {file = "multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827"}, + {file = "multidict-6.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c"}, + {file = "multidict-6.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40"}, + {file = "multidict-6.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851"}, + {file = "multidict-6.7.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687"}, + {file = "multidict-6.7.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5"}, + {file = "multidict-6.7.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb"}, + {file = "multidict-6.7.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6"}, + {file = "multidict-6.7.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e"}, + {file = "multidict-6.7.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e"}, + {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32"}, + {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c"}, + {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84"}, + {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329"}, + {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e"}, + {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4"}, + {file = "multidict-6.7.0-cp39-cp39-win32.whl", hash = "sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91"}, + {file = "multidict-6.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f"}, + {file = "multidict-6.7.0-cp39-cp39-win_arm64.whl", hash = "sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546"}, + {file = "multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3"}, + {file = "multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5"}, +] + +[[package]] +name = "numpy" +version = "2.4.0" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "numpy-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:316b2f2584682318539f0bcaca5a496ce9ca78c88066579ebd11fd06f8e4741e"}, + {file = "numpy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2718c1de8504121714234b6f8241d0019450353276c88b9453c9c3d92e101db"}, + {file = "numpy-2.4.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:21555da4ec4a0c942520ead42c3b0dc9477441e085c42b0fbdd6a084869a6f6b"}, + {file = "numpy-2.4.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:413aa561266a4be2d06cd2b9665e89d9f54c543f418773076a76adcf2af08bc7"}, + {file = "numpy-2.4.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0feafc9e03128074689183031181fac0897ff169692d8492066e949041096548"}, + {file = "numpy-2.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8fdfed3deaf1928fb7667d96e0567cdf58c2b370ea2ee7e586aa383ec2cb346"}, + {file = "numpy-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06a922a469cae9a57100864caf4f8a97a1026513793969f8ba5b63137a35d25"}, + {file = "numpy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:927ccf5cd17c48f801f4ed43a7e5673a2724bd2171460be3e3894e6e332ef83a"}, + {file = "numpy-2.4.0-cp311-cp311-win32.whl", hash = "sha256:882567b7ae57c1b1a0250208cc21a7976d8cbcc49d5a322e607e6f09c9e0bd53"}, + {file = "numpy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b986403023c8f3bf8f487c2e6186afda156174d31c175f747d8934dfddf3479"}, + {file = "numpy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:3f3096405acc48887458bbf9f6814d43785ac7ba2a57ea6442b581dedbc60ce6"}, + {file = "numpy-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8b6bb8369abefb8bd1801b054ad50e02b3275c8614dc6e5b0373c305291037"}, + {file = "numpy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e284ca13d5a8367e43734148622caf0b261b275673823593e3e3634a6490f83"}, + {file = "numpy-2.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:49ff32b09f5aa0cd30a20c2b39db3e669c845589f2b7fc910365210887e39344"}, + {file = "numpy-2.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:36cbfb13c152b1c7c184ddac43765db8ad672567e7bafff2cc755a09917ed2e6"}, + {file = "numpy-2.4.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ddc8f4914466e6fc954c76527aa91aa763682a4f6d73249ef20b418fe6effb"}, + {file = "numpy-2.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc578891de1db95b2a35001b695451767b580bb45753717498213c5ff3c41d63"}, + {file = "numpy-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98e81648e0b36e325ab67e46b5400a7a6d4a22b8a7c8e8bbfe20e7db7906bf95"}, + {file = "numpy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d57b5046c120561ba8fa8e4030fbb8b822f3063910fa901ffadf16e2b7128ad6"}, + {file = "numpy-2.4.0-cp312-cp312-win32.whl", hash = "sha256:92190db305a6f48734d3982f2c60fa30d6b5ee9bff10f2887b930d7b40119f4c"}, + {file = "numpy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:680060061adb2d74ce352628cb798cfdec399068aa7f07ba9fb818b2b3305f98"}, + {file = "numpy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:39699233bc72dd482da1415dcb06076e32f60eddc796a796c5fb6c5efce94667"}, + {file = "numpy-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a152d86a3ae00ba5f47b3acf3b827509fd0b6cb7d3259665e63dafbad22a75ea"}, + {file = "numpy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39b19251dec4de8ff8496cd0806cbe27bf0684f765abb1f4809554de93785f2d"}, + {file = "numpy-2.4.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:009bd0ea12d3c784b6639a8457537016ce5172109e585338e11334f6a7bb88ee"}, + {file = "numpy-2.4.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5fe44e277225fd3dff6882d86d3d447205d43532c3627313d17e754fb3905a0e"}, + {file = "numpy-2.4.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f935c4493eda9069851058fa0d9e39dbf6286be690066509305e52912714dbb2"}, + {file = "numpy-2.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cfa5f29a695cb7438965e6c3e8d06e0416060cf0d709c1b1c1653a939bf5c2a"}, + {file = "numpy-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba0cb30acd3ef11c94dc27fbfba68940652492bc107075e7ffe23057f9425681"}, + {file = "numpy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60e8c196cd82cbbd4f130b5290007e13e6de3eca79f0d4d38014769d96a7c475"}, + {file = "numpy-2.4.0-cp313-cp313-win32.whl", hash = "sha256:5f48cb3e88fbc294dc90e215d86fbaf1c852c63dbdb6c3a3e63f45c4b57f7344"}, + {file = "numpy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:a899699294f28f7be8992853c0c60741f16ff199205e2e6cdca155762cbaa59d"}, + {file = "numpy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9198f447e1dc5647d07c9a6bbe2063cc0132728cc7175b39dbc796da5b54920d"}, + {file = "numpy-2.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74623f2ab5cc3f7c886add4f735d1031a1d2be4a4ae63c0546cfd74e7a31ddf6"}, + {file = "numpy-2.4.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0804a8e4ab070d1d35496e65ffd3cf8114c136a2b81f61dfab0de4b218aacfd5"}, + {file = "numpy-2.4.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:02a2038eb27f9443a8b266a66911e926566b5a6ffd1a689b588f7f35b81e7dc3"}, + {file = "numpy-2.4.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1889b3a3f47a7b5bee16bc25a2145bd7cb91897f815ce3499db64c7458b6d91d"}, + {file = "numpy-2.4.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85eef4cb5625c47ee6425c58a3502555e10f45ee973da878ac8248ad58c136f3"}, + {file = "numpy-2.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6dc8b7e2f4eb184b37655195f421836cfae6f58197b67e3ffc501f1333d993fa"}, + {file = "numpy-2.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:44aba2f0cafd287871a495fb3163408b0bd25bbce135c6f621534a07f4f7875c"}, + {file = "numpy-2.4.0-cp313-cp313t-win32.whl", hash = "sha256:20c115517513831860c573996e395707aa9fb691eb179200125c250e895fcd93"}, + {file = "numpy-2.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b48e35f4ab6f6a7597c46e301126ceba4c44cd3280e3750f85db48b082624fa4"}, + {file = "numpy-2.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:4d1cfce39e511069b11e67cd0bd78ceff31443b7c9e5c04db73c7a19f572967c"}, + {file = "numpy-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c95eb6db2884917d86cde0b4d4cf31adf485c8ec36bf8696dd66fa70de96f36b"}, + {file = "numpy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:65167da969cd1ec3a1df31cb221ca3a19a8aaa25370ecb17d428415e93c1935e"}, + {file = "numpy-2.4.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3de19cfecd1465d0dcf8a5b5ea8b3155b42ed0b639dba4b71e323d74f2a3be5e"}, + {file = "numpy-2.4.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6c05483c3136ac4c91b4e81903cb53a8707d316f488124d0398499a4f8e8ef51"}, + {file = "numpy-2.4.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36667db4d6c1cea79c8930ab72fadfb4060feb4bfe724141cd4bd064d2e5f8ce"}, + {file = "numpy-2.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a818668b674047fd88c4cddada7ab8f1c298812783e8328e956b78dc4807f9f"}, + {file = "numpy-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ee32359fb7543b7b7bd0b2f46294db27e29e7bbdf70541e81b190836cd83ded"}, + {file = "numpy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e493962256a38f58283de033d8af176c5c91c084ea30f15834f7545451c42059"}, + {file = "numpy-2.4.0-cp314-cp314-win32.whl", hash = "sha256:6bbaebf0d11567fa8926215ae731e1d58e6ec28a8a25235b8a47405d301332db"}, + {file = "numpy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d857f55e7fdf7c38ab96c4558c95b97d1c685be6b05c249f5fdafcbd6f9899e"}, + {file = "numpy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:bb50ce5fb202a26fd5404620e7ef820ad1ab3558b444cb0b55beb7ef66cd2d63"}, + {file = "numpy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:355354388cba60f2132df297e2d53053d4063f79077b67b481d21276d61fc4df"}, + {file = "numpy-2.4.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1d8f9fde5f6dc1b6fc34df8162f3b3079365468703fee7f31d4e0cc8c63baed9"}, + {file = "numpy-2.4.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e0434aa22c821f44eeb4c650b81c7fbdd8c0122c6c4b5a576a76d5a35625ecd9"}, + {file = "numpy-2.4.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40483b2f2d3ba7aad426443767ff5632ec3156ef09742b96913787d13c336471"}, + {file = "numpy-2.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6a7664ddd9746e20b7325351fe1a8408d0a2bf9c63b5e898290ddc8f09544"}, + {file = "numpy-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ecb0019d44f4cdb50b676c5d0cb4b1eae8e15d1ed3d3e6639f986fc92b2ec52c"}, + {file = "numpy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d0ffd9e2e4441c96a9c91ec1783285d80bf835b677853fc2770a89d50c1e48ac"}, + {file = "numpy-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:77f0d13fa87036d7553bf81f0e1fe3ce68d14c9976c9851744e4d3e91127e95f"}, + {file = "numpy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b1f5b45829ac1848893f0ddf5cb326110604d6df96cdc255b0bf9edd154104d4"}, + {file = "numpy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:23a3e9d1a6f360267e8fbb38ba5db355a6a7e9be71d7fce7ab3125e88bb646c8"}, + {file = "numpy-2.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b54c83f1c0c0f1d748dca0af516062b8829d53d1f0c402be24b4257a9c48ada6"}, + {file = "numpy-2.4.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:aabb081ca0ec5d39591fc33018cd4b3f96e1a2dd6756282029986d00a785fba4"}, + {file = "numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:8eafe7c36c8430b7794edeab3087dec7bf31d634d92f2af9949434b9d1964cba"}, + {file = "numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2f585f52b2baf07ff3356158d9268ea095e221371f1074fadea2f42544d58b4d"}, + {file = "numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ed06d0fe9cae27d8fb5f400c63ccee72370599c75e683a6358dd3a4fb50aaf"}, + {file = "numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57c540ed8fb1f05cb997c6761cd56db72395b0d6985e90571ff660452ade4f98"}, + {file = "numpy-2.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a39fb973a726e63223287adc6dafe444ce75af952d711e400f3bf2b36ef55a7b"}, + {file = "numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "patchelf" +version = "0.17.2.4" +description = "A small utility to modify the dynamic linker and RPATH of ELF executables." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "sys_platform == \"linux\" and (platform_machine == \"x86_64\" or platform_machine == \"i686\" or platform_machine == \"aarch64\" or platform_machine == \"armv7l\" or platform_machine == \"ppc64le\" or platform_machine == \"s390x\")" +files = [ + {file = "patchelf-0.17.2.4-py3-none-macosx_10_9_universal2.whl", hash = "sha256:343bb1b94e959f9070ca9607453b04390e36bbaa33c88640b989cefad0aa049e"}, + {file = "patchelf-0.17.2.4-py3-none-manylinux1_i686.manylinux_2_5_i686.musllinux_1_1_i686.whl", hash = "sha256:09fd848d625a165fc7b7e07745508c24077129b019c4415a882938781d43adf8"}, + {file = "patchelf-0.17.2.4-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:d9b35ebfada70c02679ad036407d9724ffe1255122ba4ac5e4be5868618a5689"}, + {file = "patchelf-0.17.2.4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2931a1b5b85f3549661898af7bf746afbda7903c7c9a967cfc998a3563f84fad"}, + {file = "patchelf-0.17.2.4-py3-none-manylinux2014_armv7l.manylinux_2_17_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ae44cb3c857d50f54b99e5697aa978726ada33a8a6129d4b8b7ffd28b996652d"}, + {file = "patchelf-0.17.2.4-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:680a266a70f60a7a4f4c448482c5bdba80cc8e6bb155a49dcc24238ba49927b0"}, + {file = "patchelf-0.17.2.4-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.musllinux_1_1_s390x.whl", hash = "sha256:d842b51f0401460f3b1f3a3a67d2c266a8f515a5adfbfa6e7b656cb3ac2ed8bc"}, + {file = "patchelf-0.17.2.4-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:7076d9e127230982e20a81a6e2358d3343004667ba510d9f822d4fdee29b0d71"}, + {file = "patchelf-0.17.2.4.tar.gz", hash = "sha256:970ee5cd8af33e5ea2099510b2f9013fa1b8d5cd763bf3fd3961281c18101a09"}, +] + +[package.extras] +test = ["importlib_metadata (>=2.0)", "pytest (>=6.0)"] + +[[package]] +name = "propcache" +version = "0.4.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, + {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, + {file = "propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c"}, + {file = "propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb"}, + {file = "propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}, + {file = "propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f"}, + {file = "propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1"}, + {file = "propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6"}, + {file = "propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75"}, + {file = "propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8"}, + {file = "propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db"}, + {file = "propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66"}, + {file = "propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81"}, + {file = "propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e"}, + {file = "propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1"}, + {file = "propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717"}, + {file = "propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37"}, + {file = "propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144"}, + {file = "propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f"}, + {file = "propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153"}, + {file = "propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455"}, + {file = "propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85"}, + {file = "propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1"}, + {file = "propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183"}, + {file = "propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19"}, + {file = "propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f"}, + {file = "propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938"}, + {file = "propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237"}, + {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"}, +] + +[[package]] +name = "psutil" +version = "7.2.1" +description = "Cross-platform lib for process and system monitoring." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d"}, + {file = "psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49"}, + {file = "psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc"}, + {file = "psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf"}, + {file = "psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f"}, + {file = "psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672"}, + {file = "psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679"}, + {file = "psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f"}, + {file = "psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129"}, + {file = "psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a"}, + {file = "psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79"}, + {file = "psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266"}, + {file = "psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42"}, + {file = "psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1"}, + {file = "psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8"}, + {file = "psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6"}, + {file = "psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8"}, + {file = "psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67"}, + {file = "psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17"}, + {file = "psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442"}, + {file = "psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3"}, +] + +[package.extras] +dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel"] +test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "setuptools"] + +[[package]] +name = "pycodec2" +version = "4.1.1" +description = "A Cython wrapper for codec2" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pycodec2-4.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1c6bf2e842bfd5a5ea3cbd45d79b6ba98d20dafa5fa5c3f93d8976c55ab1a0f"}, + {file = "pycodec2-4.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6457fc77448730fe1589d3642a71292abcb223558763457fa04d08732f0cad"}, + {file = "pycodec2-4.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe09344774b22051123881f1e97edb0fd38d32188f8ba3d7740fbc9d5adee699"}, + {file = "pycodec2-4.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a2bc6da5f35bb620c1b37c9f87a5f07c378a14eefbc872dae9e10bc594dbc256"}, + {file = "pycodec2-4.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:727384c2a1eedd4122ef66ae7c2cb7bf9c07bb310402d93467cd701f7f2c4e49"}, + {file = "pycodec2-4.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7789db06043daed4dfd29387aec3ecaf4a911bfbb1102117c507dacbc1bf0b52"}, + {file = "pycodec2-4.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9282abbc0b71d9a6b35038ed46ee5d8d4f8c33ddd354c2df37642c512132ce5b"}, + {file = "pycodec2-4.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6c2b496c70a5abb304951a068f4b7e20c1fbd8c44195b43a9c0d9fbdaa805f3"}, + {file = "pycodec2-4.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31a54cf0b9a97cb80aaf8dffacc8a79aed1c351abe42be29b91c879d02df28b6"}, + {file = "pycodec2-4.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dcc95a8e8824002279d180e42203425e7da7ccdebb62cfa91eece53b8946c6c8"}, + {file = "pycodec2-4.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9bd4380b849d896a401c57fa92ddb99b6706754d334fa6cce0fa32ae2e7d0d11"}, + {file = "pycodec2-4.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:80ef0c14cf9c0790821be1c2eeabcef6c8e1bfa63a0bf03dbea9f2c16c6d7f03"}, + {file = "pycodec2-4.1.1-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:6a730f8317b006c2407a609b42adbab62c1706fe3f1ea1779d4877dcdb7c5f0f"}, + {file = "pycodec2-4.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d37891c1a9b7cf494da4fa82979bb72d83389ebaf6188fc0578fcbc8ecc9134"}, + {file = "pycodec2-4.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcf861ff0de2d6fd52e13e8a60c5ac3de982f8b5192a7d4506a68fbdcfee8ebc"}, + {file = "pycodec2-4.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9afba61172a4fdee193dcb1480a4cfd11861ea2d468450d1f02846b83b72de17"}, + {file = "pycodec2-4.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e271437505f54b890f97dcb63b0afc36b66791ba2bcea45bc854371b2d11de"}, + {file = "pycodec2-4.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:f8d05d35ea1292b4b7d30bf54abcf62171aa4141114a221f2e42ad51db6bb247"}, + {file = "pycodec2-4.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c3600c5cbba169fd1431a92468f1cf66979f99845e91fd25b72d3bce8fd9c5dc"}, + {file = "pycodec2-4.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e56800578998ebe480fff9696364272d4e08be3c1dfd5eff374b4c4fa90b301"}, + {file = "pycodec2-4.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:010ad6ed4b571a5630ed06a0392b0ec12d5bcd8c73545cf9bb695aac4596a836"}, + {file = "pycodec2-4.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f25633101b2567a43a54116096d8e1c1617fcaa343b9a87a89083b2e5d52709f"}, + {file = "pycodec2-4.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ca5d1668ddf326cc8fbe07db2f1691889186e2d89ae6be7614c5f54b2e196163"}, + {file = "pycodec2-4.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:e9a775a4cef30cca0f477cbf832277efb0d99aa2d198d47eeb70e09124451183"}, + {file = "pycodec2-4.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d174750097e2928681aab2bd9cfe1e1504c60c0e1e93c7f09de54505b953b614"}, + {file = "pycodec2-4.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0b16b234eda0f08a11a48db624360dc9bc587c16a09b3e5aae2075ca6b9f9f92"}, + {file = "pycodec2-4.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a56556279baedd96b650de6d9eb770e602321a6811e425f52daa3fa0dba1b705"}, + {file = "pycodec2-4.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e5d11d015b3b2d509312faf5e466387b75687ff9b84e37da0dc2e62c4a400e1"}, + {file = "pycodec2-4.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:76e98b492ee361a55cb23142db98e6b87695a569cabe172d80f1ced01584c67a"}, + {file = "pycodec2-4.1.1.tar.gz", hash = "sha256:15a486b16eb582050e00f4b8de1e4cb855665a32b6d9496aa4cba3b1254b9eb7"}, +] + +[package.dependencies] +numpy = ">=2.00,<3.0.0" + +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + +[[package]] +name = "pyserial" +version = "3.5" +description = "Python Serial Port Extension" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, + {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, +] + +[package.extras] +cp2110 = ["hidapi"] + +[[package]] +name = "python-msilib" +version = "0.5.0" +description = "Read and write Microsoft Installer files" +optional = false +python-versions = ">=3.13" +groups = ["dev"] +markers = "sys_platform == \"win32\" and python_version >= \"3.13\"" +files = [ + {file = "python_msilib-0.5.0-cp313-cp313-win32.whl", hash = "sha256:240c9cebc402361e6fe32fb94020ce5a2674343122cb4dbd036fa59068d153c4"}, + {file = "python_msilib-0.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:cb1db6fa79cfff539a0f313c80f27760b3c452b8fb59878c4b1ce2959711541e"}, + {file = "python_msilib-0.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:3904771a9e31276da9d2de4e431e09b122a354b84b41746c6feaa14b91d2fd0d"}, + {file = "python_msilib-0.5.0-cp313-cp313t-win32.whl", hash = "sha256:67ab7fe107b6dbed42e609d0731ae9863c8aaf58e6015995e42671a9d215fe86"}, + {file = "python_msilib-0.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc3d895c1cb4f695344a7149909d460020d0e6f987d33ab40ea687d65dd1f2ad"}, + {file = "python_msilib-0.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:cc62a3d0500c1d6d21bea347c55215ae16c81bc749134c47e466f284a46b56fe"}, + {file = "python_msilib-0.5.0-cp314-cp314-win32.whl", hash = "sha256:e5023ab8da04a13769d1054e64415c02c198d442ce0f7cd9637204341d997379"}, + {file = "python_msilib-0.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:24cd942d097bafc97018698e1e66652143529eecaf8b8df55a624d580d81df7c"}, + {file = "python_msilib-0.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:f9da5045835d0de30b1087ff6fd6e0b6d076e248cd26405c0d399d8649ef9684"}, + {file = "python_msilib-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:821a164e72d77786d9e6d7cd28c060e6668665632a29f2c76abac85651f05f11"}, + {file = "python_msilib-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3fafe94d66f00bd3b26159b7ff43078421738288af5212744a1cb4a1cea52bc8"}, + {file = "python_msilib-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c0b7a595dd457f7df5bd102c4c5abe5337ba0362aab2ece31bea29e6e7d9f2d"}, + {file = "python_msilib-0.5.0.tar.gz", hash = "sha256:5d9d0c2af2cafbc50d484cce4cfab1cb8cfd65d705d7111d216bc525a059113b"}, +] + +[package.extras] +dev = ["bump-my-version (==1.2.4)", "cibuildwheel (==3.3.0)", "pre-commit (==4.5.0)"] +tests = ["coverage (==7.12.0)", "pytest (==9.0.2)"] + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rns" +version = "1.0.4" +description = "Self-configuring, encrypted and resilient mesh networking stack for LoRa, packet radio, WiFi and everything in between" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "rns-1.0.4-1-py3-none-any.whl", hash = "sha256:f1804f8b07a8b8e1c1b61889f929fdb5cfbd57f4c354108c417135f0d67c5ef6"}, + {file = "rns-1.0.4-py3-none-any.whl", hash = "sha256:7a2b7893410833b42c0fa7f9a9e3369cebb085cdd26bd83f3031fa6c1051653c"}, + {file = "rns-1.0.4.tar.gz", hash = "sha256:e70667a767fe523bab8e7ea0627447258c4e6763b7756fbba50c6556dbb84399"}, +] + +[package.dependencies] +cryptography = ">=3.4.7" +pyserial = ">=3.5" + +[[package]] +name = "setuptools" +version = "80.9.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] + +[[package]] +name = "striprtf" +version = "0.0.29" +description = "A simple library to convert rtf to text" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "striprtf-0.0.29-py3-none-any.whl", hash = "sha256:0fc6a41999d015358d19627776b616424dd501ad698105c81d76734d1e14d91b"}, + {file = "striprtf-0.0.29.tar.gz", hash = "sha256:5a822d075e17417934ed3add6fc79b5fc8fb544fe4370b2f894cdd28f0ddd78e"}, +] + +[package.extras] +dev = ["build (>=1.0.0)", "pytest (>=7.0.0)"] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version < \"3.13\"" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"}, + {file = "urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "websockets" +version = "15.0.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, + {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, + {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, + {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, + {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, + {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, + {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, + {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, + {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, + {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, + {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, + {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, + {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, +] + +[[package]] +name = "yarl" +version = "1.22.0" +description = "Yet another URL library" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e"}, + {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f"}, + {file = "yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf"}, + {file = "yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a"}, + {file = "yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c"}, + {file = "yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147"}, + {file = "yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb"}, + {file = "yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6"}, + {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0"}, + {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda"}, + {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc"}, + {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737"}, + {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467"}, + {file = "yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea"}, + {file = "yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca"}, + {file = "yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b"}, + {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511"}, + {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6"}, + {file = "yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028"}, + {file = "yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d"}, + {file = "yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503"}, + {file = "yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65"}, + {file = "yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e"}, + {file = "yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d"}, + {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7"}, + {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967"}, + {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed"}, + {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6"}, + {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e"}, + {file = "yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca"}, + {file = "yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b"}, + {file = "yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376"}, + {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f"}, + {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2"}, + {file = "yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74"}, + {file = "yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df"}, + {file = "yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb"}, + {file = "yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2"}, + {file = "yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82"}, + {file = "yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a"}, + {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124"}, + {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa"}, + {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7"}, + {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d"}, + {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520"}, + {file = "yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8"}, + {file = "yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c"}, + {file = "yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74"}, + {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53"}, + {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a"}, + {file = "yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c"}, + {file = "yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601"}, + {file = "yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a"}, + {file = "yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df"}, + {file = "yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2"}, + {file = "yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b"}, + {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273"}, + {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a"}, + {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d"}, + {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02"}, + {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67"}, + {file = "yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95"}, + {file = "yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d"}, + {file = "yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b"}, + {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10"}, + {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3"}, + {file = "yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9"}, + {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f"}, + {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0"}, + {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e"}, + {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708"}, + {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f"}, + {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d"}, + {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8"}, + {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5"}, + {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f"}, + {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62"}, + {file = "yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03"}, + {file = "yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249"}, + {file = "yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b"}, + {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4"}, + {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683"}, + {file = "yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b"}, + {file = "yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e"}, + {file = "yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590"}, + {file = "yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2"}, + {file = "yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da"}, + {file = "yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784"}, + {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b"}, + {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694"}, + {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d"}, + {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd"}, + {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da"}, + {file = "yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2"}, + {file = "yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79"}, + {file = "yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33"}, + {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1"}, + {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca"}, + {file = "yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53"}, + {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c"}, + {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf"}, + {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face"}, + {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b"}, + {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486"}, + {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138"}, + {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a"}, + {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529"}, + {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093"}, + {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c"}, + {file = "yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e"}, + {file = "yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27"}, + {file = "yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1"}, + {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748"}, + {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859"}, + {file = "yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9"}, + {file = "yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054"}, + {file = "yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b"}, + {file = "yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60"}, + {file = "yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890"}, + {file = "yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba"}, + {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca"}, + {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba"}, + {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b"}, + {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e"}, + {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8"}, + {file = "yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b"}, + {file = "yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed"}, + {file = "yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2"}, + {file = "yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff"}, + {file = "yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + +[metadata] +lock-version = "2.1" +python-versions = ">=3.11" +content-hash = "fc66bbe16d88af079264f801bc18fd10385c0e6af437fdf0e5ab960349971b21" diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..67cdf1a --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4eeb848 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +requires = ["poetry-core>=1.5"] +build-backend = "poetry.core.masonry.api" + +[project] +name = "reticulum-meshchatx" +dynamic = ["version"] +description = "A simple mesh network communications app powered by the Reticulum Network Stack" +readme = "README.md" +authors = [ + {name = "Sudo-Ivan"} +] +license = "MIT" +keywords = ["reticulum", "meshchat", "lxmf", "rns"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Other Audience", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Communications", + "Topic :: Software Development :: Build Tools", +] +requires-python = ">=3.11" +dependencies = [ + "aiohttp>=3.13.2", + "lxmf>=0.9.3", + "psutil>=7.1.3", + "rns>=1.0.4", + "websockets>=15.0.1", + "bcrypt (>=5.0.0,<6.0.0)", + "aiohttp-session (>=2.12.1,<3.0.0)", + "cryptography (>=46.0.3,<47.0.0)", + "requests (>=2.32.5,<3.0.0)", + "lxst (>=0.4.5,<0.5.0)", + "audioop-lts (>=0.2.2); python_version>='3.13'", +] + +[project.scripts] +meshchat = "meshchatx.meshchat:main" + +[tool.poetry] +version = "2.50.0" +packages = [{include = "meshchatx"}] + +[tool.poetry.group.dev.dependencies] +cx-freeze = ">=7.0.0" + +[[tool.poetry.include]] +path = "logo" + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2c2ba61 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +aiohttp>=3.12.14 +aiohttp-session>=2.12.0 +bcrypt>=4.0.1 +cryptography>=46.0.0 +cx_freeze>=7.0.0 +lxmf>=0.9.3 +lxst>=0.4.4 +psutil>=7.1.3 +rns>=1.0.4 +websockets>=14.2 +requests>=2.32.5 \ No newline at end of file diff --git a/screenshots/screenshot.png b/screenshots/screenshot.png new file mode 100644 index 0000000..672b8b7 Binary files /dev/null and b/screenshots/screenshot.png differ diff --git a/scripts/build-backend.js b/scripts/build-backend.js new file mode 100755 index 0000000..f43c168 --- /dev/null +++ b/scripts/build-backend.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +const { execSync } = require("child_process"); +try { + execSync(`poetry run python cx_setup.py build`, { stdio: "inherit" }); +} catch (error) { + console.error("Build failed:", error.message); + process.exit(1); +} diff --git a/scripts/move_wheels.py b/scripts/move_wheels.py new file mode 100644 index 0000000..43cf3a4 --- /dev/null +++ b/scripts/move_wheels.py @@ -0,0 +1,13 @@ +"""Move Poetry-built wheels from dist/ to python-dist/ to avoid conflicts +with Electron build artifacts. +""" + +import shutil +from pathlib import Path + +dist = Path("dist") +target = Path("python-dist") +target.mkdir(parents=True, exist_ok=True) + +for wheel in dist.glob("*.whl"): + shutil.move(str(wheel), target / wheel.name) diff --git a/scripts/prepare_frontend_dir.py b/scripts/prepare_frontend_dir.py new file mode 100644 index 0000000..2ee9a5a --- /dev/null +++ b/scripts/prepare_frontend_dir.py @@ -0,0 +1,16 @@ +import shutil +from pathlib import Path + +TARGET = Path("meshchatx") / "public" + +if not Path("pyproject.toml").exists(): + msg = "Must run from project root" + raise RuntimeError(msg) + +if TARGET.exists(): + if TARGET.is_symlink(): + msg = f"{TARGET} is a symlink, refusing to remove" + raise RuntimeError(msg) + shutil.rmtree(TARGET) + +TARGET.mkdir(parents=True, exist_ok=True) diff --git a/scripts/rename_legacy_artifacts.sh b/scripts/rename_legacy_artifacts.sh new file mode 100755 index 0000000..7d39aef --- /dev/null +++ b/scripts/rename_legacy_artifacts.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +shopt -s nullglob + +patterns=( + "dist/*-win-installer.exe" + "dist/*-win-portable.exe" + "dist/*-linux.AppImage" + "dist/*-linux.deb" +) + +for pattern in "${patterns[@]}"; do + for f in $pattern; do + dir=$(dirname "$f") + base=$(basename "$f") + ext="${base##*.}" + name="${base%.$ext}" + mv "$f" "$dir/${name}-legacy.${ext}" + done +done diff --git a/scripts/sync_version.py b/scripts/sync_version.py new file mode 100644 index 0000000..452790b --- /dev/null +++ b/scripts/sync_version.py @@ -0,0 +1,65 @@ +"""Update project version references to stay aligned with the Electron build. + +Reads `package.json`, writes the same version into `src/version.py`, and +updates the `[tool.poetry] version` field inside `pyproject.toml`. Run this +before any Python packaging commands so the wheel version matches the +Electron artifacts. +""" + +import json +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PACKAGE_JSON = ROOT / "package.json" +VERSION_PY = ROOT / "meshchatx" / "src" / "version.py" +PYPROJECT_TOML = ROOT / "pyproject.toml" + + +def read_package_version() -> str: + with PACKAGE_JSON.open() as handle: + return json.load(handle)["version"] + + +def write_version_module(version: str) -> None: + content = ( + '"""\n' + "Auto-generated helper so Python tooling and the Electron build\n" + "share the same version string.\n" + '"""\n\n' + f"__version__ = {version!r}\n" + ) + if VERSION_PY.exists() and VERSION_PY.read_text() == content: + return + VERSION_PY.write_text(content) + + +def update_poetry_version(version: str) -> None: + if not PYPROJECT_TOML.exists(): + return + content = PYPROJECT_TOML.read_text() + + def replacer(match): + return f"{match.group(1)}{version}{match.group(2)}" + + new_content, replaced = re.subn( + r'(?m)^(version\s*=\s*")[^"]*(")', + replacer, + content, + count=1, + ) + if replaced == 0: + msg = "failed to update version in pyproject.toml" + raise RuntimeError(msg) + if new_content != content: + PYPROJECT_TOML.write_text(new_content) + + +def main() -> None: + version = read_package_version() + write_version_module(version) + update_poetry_version(version) + + +if __name__ == "__main__": + main() diff --git a/scripts/test_wheel.sh b/scripts/test_wheel.sh new file mode 100755 index 0000000..3692bad --- /dev/null +++ b/scripts/test_wheel.sh @@ -0,0 +1,74 @@ +#!/bin/bash +set -e + +# Find wheel file dynamically +WHEEL_PATTERN="python-dist/reticulum_meshchatx-*-py3-none-any.whl" +WHEEL_FILES=($WHEEL_PATTERN) + +if [ ${#WHEEL_FILES[@]} -eq 0 ]; then + echo "Error: No wheel files found matching pattern: $WHEEL_PATTERN" + echo "Make sure to run 'poetry build' or similar to create the wheel first." + exit 1 +elif [ ${#WHEEL_FILES[@]} -gt 1 ]; then + echo "Error: Multiple wheel files found:" + printf ' %s\n' "${WHEEL_FILES[@]}" + echo "Please clean up old wheels or specify which one to use." + exit 1 +fi + +WHEEL_PATH="${WHEEL_FILES[0]}" + +if [ ! -f "$WHEEL_PATH" ]; then + echo "Error: Wheel not found at $WHEEL_PATH" + exit 1 +fi + +echo "Found wheel: $WHEEL_PATH" + +echo "Creating test virtual environment..." +TEST_VENV=$(mktemp -d)/test-venv +python3 -m venv "$TEST_VENV" + +echo "Installing wheel..." +"$TEST_VENV/bin/pip" install --upgrade pip +"$TEST_VENV/bin/pip" install "$WHEEL_PATH" + +echo "" +echo "Checking installation..." +"$TEST_VENV/bin/python" << 'PYTHON_SCRIPT' +import meshchatx.meshchat as meshchat +import os +from pathlib import Path + +# Check if meshchat module is importable +print(f'meshchat module location: {meshchat.__file__}') + +# Check if public directory exists +meshchat_dir = os.path.dirname(meshchat.__file__) +public_path = os.path.join(meshchat_dir, 'public') +print(f'Checking for public at: {public_path}') +print(f'Exists: {os.path.exists(public_path)}') + +# Try get_file_path +from meshchatx.meshchat import get_file_path +test_path = get_file_path('public') +print(f'get_file_path("public"): {test_path}') +print(f'Exists: {os.path.exists(test_path)}') + +if os.path.exists(test_path): + index_html = os.path.join(test_path, 'index.html') + print(f'index.html exists: {os.path.exists(index_html)}') +else: + print('WARNING: public directory not found!') + print('Checking parent directories...') + current = meshchat_dir + for i in range(3): + test = os.path.join(current, 'public') + print(f' {test}: {os.path.exists(test)}') + current = os.path.dirname(current) +PYTHON_SCRIPT + +echo "" +echo "Test complete. Virtual environment at: $TEST_VENV" +echo "To test running meshchat: $TEST_VENV/bin/meshchat --help" + diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..cc2178b --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,13 @@ +import formsPlugin from "@tailwindcss/forms"; + +const frontendRoot = "./meshchatx/src/frontend"; + +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: "selector", + content: [`${frontendRoot}/index.html`, `${frontendRoot}/**/*.{vue,js,ts,jsx,tsx,html}`], + theme: { + extend: {}, + }, + plugins: [formsPlugin], +}; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..7c84632 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,93 @@ +import path from "path"; +import vue from "@vitejs/plugin-vue"; +import vuetify from "vite-plugin-vuetify"; + +export default { + plugins: [vue(), vuetify()], + + // vite app is loaded from /meshchatx/src/frontend + root: path.join(__dirname, "meshchatx", "src", "frontend"), + + build: { + minify: "terser", + terserOptions: { + compress: { + drop_console: false, + pure_funcs: ["console.debug"], + }, + }, + + // we want to compile vite app to meshchatx/public which is bundled and served by the python executable + outDir: path.join(__dirname, "meshchatx", "public"), + emptyOutDir: true, + + rollupOptions: { + treeshake: { + moduleSideEffects: (id) => { + if (id.includes("@mdi/js")) { + return false; + } + return null; + }, + }, + input: { + // we want to use /meshchatx/src/frontend/index.html as the entrypoint for this vite app + app: path.join(__dirname, "meshchatx", "src", "frontend", "index.html"), + }, + output: { + manualChunks(id) { + if (id.includes("node_modules")) { + if (id.includes("vuetify")) { + return "vendor-vuetify"; + } + if (id.includes("vis-network") || id.includes("vis-data")) { + return "vendor-vis"; + } + if (id.includes("vue-router")) { + return "vendor-vue-router"; + } + if (id.includes("vue")) { + return "vendor-vue"; + } + if (id.includes("protobufjs") || id.includes("@protobufjs")) { + return "vendor-protobuf"; + } + if (id.includes("dayjs")) { + return "vendor-dayjs"; + } + if (id.includes("axios")) { + return "vendor-axios"; + } + if (id.includes("@mdi/js")) { + return "vendor-mdi"; + } + if (id.includes("compressorjs")) { + return "vendor-compressor"; + } + if (id.includes("click-outside-vue3")) { + return "vendor-click-outside"; + } + if (id.includes("mitt")) { + return "vendor-mitt"; + } + if (id.includes("micron-parser")) { + return "vendor-micron"; + } + if (id.includes("electron-prompt")) { + return "vendor-electron-prompt"; + } + return "vendor-other"; + } + }, + }, + }, + }, + + optimizeDeps: { + include: ["dayjs", "vue"], + }, + + resolve: { + dedupe: ["vue"], + }, +};